]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
✨ Add support for Pydantic v2 (#9816)
authorSebastián Ramírez <tiangolo@gmail.com>
Fri, 7 Jul 2023 17:12:13 +0000 (19:12 +0200)
committerGitHub <noreply@github.com>
Fri, 7 Jul 2023 17:12:13 +0000 (19:12 +0200)
* ✨ Pydantic v2 migration, initial implementation (#9500)

* ✨ 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 <35119617+dmontagu@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
* 👷 Add CI for both Pydantic v1 and v2 (#9688)

* 👷 Test and install Pydantic v1 and v2 in CI

* 💚 Tweak CI config for Pydantic v1 and v2

* 💚 Fix Pydantic v2 specification in CI

* 🐛 Fix type annotations for compatibility with Python 3.7

* 💚 Install Pydantic v2 for lints

* 🐛 Fix type annotations for Pydantic v2

* 💚 Re-use test cache for lint

* ♻️ Refactor internals for test coverage and performance (#9691)

* ♻️ Tweak import of Annotated from typing_extensions, they are installed anyway

* ♻️ Refactor _compat to define functions for Pydantic v1 or v2 once instead of checking inside

* ✅ Add test for UploadFile for Pydantic v2

* ♻️ Refactor types and remove logic for impossible cases

* ✅ Add missing tests from test refactor for path params

* ✅ Add tests for new decimal encoder

* 💡 Add TODO comment for decimals in encoders

* 🔥 Remove unneeded dummy function

* 🔥 Remove section of code in field_annotation_is_scalar covered by sub-call to field_annotation_is_complex

* ♻️ Refactor and tweak variables and types in _compat

* ✅ Add tests for corner cases and compat with Pydantic v1 and v2

* ♻️ Refactor type annotations

* 🔖 Release version 0.100.0-beta1

* ♻️ Refactor parts that use optional requirements to make them compatible with installations without them (#9707)

* ♻️ Refactor parts that use optional requirements to make them compatible with installations without them

* ♻️ Update JSON Schema for email field without email-validator installed

* 🐛 Fix support for Pydantic v2.0, small changes in their final release (#9771)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
* 🔖 Release version 0.100.0-beta2

* ✨ OpenAPI 3.1.0 with Pydantic v2, merge `master` (#9773)

* ➕ Add dirty-equals as a testing dependency (#9778)

➕ Add dirty-equals as a testing dependency, it seems it got lsot at some point

* 🔀 Merge master, fix valid JSON Schema accepting bools (#9782)

* ⏪️ Revert usage of custom logic for TypeAdapter JSON Schema, solved on the Pydantic side (#9787)

⏪️ Revert usage of custom logic for TypeAdapter JSON Schema, solved on Pydantic side

* ♻️ Deprecate parameter `regex`, use `pattern` instead (#9786)

* 📝 Update docs to deprecate regex, recommend pattern

* ♻️ Update examples to use new pattern instead of regex

* 📝 Add new example with deprecated regex

* ♻️ Add deprecation notes and warnings for regex

* ✅ Add tests for regex deprecation

* ✅ Update tests for compatibility with Pydantic v1

* ✨ Update docs to use Pydantic v2 settings and add note and example about v1 (#9788)

* ➕ Add pydantic-settings to all extras

* 📝 Update docs for Pydantic settings

* 📝 Update Settings source examples to use Pydantic v2, and add a Pydantic v1 version

* ✅ Add tests for settings with Pydantic v1 and v2

* 🔥 Remove solved TODO comment

* ♻️ Update conditional OpenAPI to use new Pydantic v2 settings

* ✅ Update tests to import Annotated from typing_extensions for Python < 3.9 (#9795)

* ➕ Add pydantic-extra-types to fastapi[extra]

* ➕ temp: Install Pydantic from source to test JSON Schema metadata fixes (#9777)

* ➕ Install Pydantic from source, from branch for JSON Schema with metadata

* ➕ Update dependencies, install Pydantic main

* ➕ Fix dependency URL for Pydantic from source

* ➕ Add pydantic-settings for test requirements

* 💡 Add TODO comments to re-enable Pydantic main (not from source) (#9796)

* ✨ Add new Pydantic Field param options to Query, Cookie, Body, etc. (#9797)

* 📝 Add docs for Pydantic v2 for `docs/en/docs/advanced/path-operation-advanced-configuration.md` (#9798)

* 📝 Update docs in examples for settings with Pydantic v2 (#9799)

* 📝 Update JSON Schema `examples` docs with Pydantic v2 (#9800)

* ♻️ Use new Pydantic v2 JSON Schema generator (#9813)

Co-authored-by: David Montague <35119617+dmontagu@users.noreply.github.com>
* ♻️ Tweak type annotations and Pydantic version range (#9801)

* 📌 Re-enable GA Pydantic, for v2, require minimum 2.0.2 (#9814)

* 🔖 Release version 0.100.0-beta3

* 🔥 Remove duplicate type declaration from merge conflicts (#9832)

* 👷‍♂️ Run tests with Pydantic v2 GA (#9830)

👷 Run tests for Pydantic v2 GA

* 📝 Add notes to docs expecting Pydantic v2 and future updates (#9833)

* 📝 Update index with new extras

* 📝 Update release notes

---------

Co-authored-by: David Montague <35119617+dmontagu@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Pastukhov Nikita <diementros@yandex.ru>
274 files changed:
.github/workflows/test.yml
README.md
docs/en/docs/advanced/async-sql-databases.md
docs/en/docs/advanced/nosql-databases.md
docs/en/docs/advanced/path-operation-advanced-configuration.md
docs/en/docs/advanced/settings.md
docs/en/docs/advanced/sql-databases-peewee.md
docs/en/docs/advanced/testing-database.md
docs/en/docs/index.md
docs/en/docs/release-notes.md
docs/en/docs/tutorial/query-params-str-validations.md
docs/en/docs/tutorial/schema-extra-example.md
docs/en/docs/tutorial/sql-databases.md
docs_src/conditional_openapi/tutorial001.py
docs_src/extra_models/tutorial003.py
docs_src/extra_models/tutorial003_py310.py
docs_src/path_operation_advanced_configuration/tutorial007.py
docs_src/path_operation_advanced_configuration/tutorial007_pv1.py [new file with mode: 0644]
docs_src/query_params_str_validations/tutorial004.py
docs_src/query_params_str_validations/tutorial004_an.py
docs_src/query_params_str_validations/tutorial004_an_py310.py
docs_src/query_params_str_validations/tutorial004_an_py310_regex.py [new file with mode: 0644]
docs_src/query_params_str_validations/tutorial004_an_py39.py
docs_src/query_params_str_validations/tutorial004_py310.py
docs_src/query_params_str_validations/tutorial010.py
docs_src/query_params_str_validations/tutorial010_an.py
docs_src/query_params_str_validations/tutorial010_an_py310.py
docs_src/query_params_str_validations/tutorial010_an_py39.py
docs_src/query_params_str_validations/tutorial010_py310.py
docs_src/schema_extra_example/tutorial001.py
docs_src/schema_extra_example/tutorial001_pv1.py [new file with mode: 0644]
docs_src/schema_extra_example/tutorial001_py310.py
docs_src/schema_extra_example/tutorial001_py310_pv1.py [new file with mode: 0644]
docs_src/settings/app01/config.py
docs_src/settings/app02/config.py
docs_src/settings/app02_an/config.py
docs_src/settings/app02_an_py39/config.py
docs_src/settings/app03/config.py
docs_src/settings/app03_an/config.py
docs_src/settings/app03_an/config_pv1.py [new file with mode: 0644]
docs_src/settings/app03_an_py39/config.py
docs_src/settings/tutorial001.py
docs_src/settings/tutorial001_pv1.py [new file with mode: 0644]
fastapi/__init__.py
fastapi/_compat.py [new file with mode: 0644]
fastapi/applications.py
fastapi/datastructures.py
fastapi/dependencies/models.py
fastapi/dependencies/utils.py
fastapi/encoders.py
fastapi/exceptions.py
fastapi/openapi/constants.py
fastapi/openapi/models.py
fastapi/openapi/utils.py
fastapi/param_functions.py
fastapi/params.py
fastapi/routing.py
fastapi/security/oauth2.py
fastapi/types.py
fastapi/utils.py
pyproject.toml
requirements-tests.txt
tests/test_additional_properties_bool.py
tests/test_additional_responses_custom_model_in_callback.py
tests/test_annotated.py
tests/test_application.py
tests/test_compat.py [new file with mode: 0644]
tests/test_custom_schema_fields.py
tests/test_datastructures.py
tests/test_datetime_custom_encoder.py
tests/test_dependency_duplicates.py
tests/test_dependency_overrides.py
tests/test_extra_routes.py
tests/test_filter_pydantic_sub_model/__init__.py [new file with mode: 0644]
tests/test_filter_pydantic_sub_model/app_pv1.py [new file with mode: 0644]
tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py [moved from tests/test_filter_pydantic_sub_model.py with 81% similarity]
tests/test_filter_pydantic_sub_model_pv2.py [new file with mode: 0644]
tests/test_infer_param_optionality.py
tests/test_inherited_custom_class.py
tests/test_jsonable_encoder.py
tests/test_multi_body_errors.py
tests/test_multi_query_errors.py
tests/test_openapi_query_parameter_extension.py
tests/test_openapi_servers.py
tests/test_params_repr.py
tests/test_path.py
tests/test_query.py
tests/test_read_with_orm_mode.py
tests/test_regex_deprecated_body.py [new file with mode: 0644]
tests/test_regex_deprecated_params.py [new file with mode: 0644]
tests/test_request_body_parameters_media_type.py
tests/test_response_by_alias.py
tests/test_response_model_as_return_annotation.py
tests/test_response_model_data_filter.py [new file with mode: 0644]
tests/test_response_model_data_filter_no_inheritance.py [new file with mode: 0644]
tests/test_schema_extra_examples.py
tests/test_security_oauth2.py
tests/test_security_oauth2_optional.py
tests/test_security_oauth2_optional_description.py
tests/test_skip_defaults.py
tests/test_sub_callbacks.py
tests/test_tuples.py
tests/test_tutorial/test_additional_responses/test_tutorial002.py
tests/test_tutorial/test_additional_responses/test_tutorial004.py
tests/test_tutorial/test_async_sql_databases/test_tutorial001.py
tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py
tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py
tests/test_tutorial/test_bigger_applications/test_main.py
tests/test_tutorial/test_bigger_applications/test_main_an.py
tests/test_tutorial/test_bigger_applications/test_main_an_py39.py
tests/test_tutorial/test_body/test_tutorial001.py
tests/test_tutorial/test_body/test_tutorial001_py310.py
tests/test_tutorial/test_body_fields/test_tutorial001.py
tests/test_tutorial/test_body_fields/test_tutorial001_an.py
tests/test_tutorial/test_body_fields/test_tutorial001_an_py310.py
tests/test_tutorial/test_body_fields/test_tutorial001_an_py39.py
tests/test_tutorial/test_body_fields/test_tutorial001_py310.py
tests/test_tutorial/test_body_multiple_params/test_tutorial001.py
tests/test_tutorial/test_body_multiple_params/test_tutorial001_an.py
tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py310.py
tests/test_tutorial/test_body_multiple_params/test_tutorial001_an_py39.py
tests/test_tutorial/test_body_multiple_params/test_tutorial001_py310.py
tests/test_tutorial/test_body_multiple_params/test_tutorial003.py
tests/test_tutorial/test_body_multiple_params/test_tutorial003_an.py
tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py310.py
tests/test_tutorial/test_body_multiple_params/test_tutorial003_an_py39.py
tests/test_tutorial/test_body_multiple_params/test_tutorial003_py310.py
tests/test_tutorial/test_body_nested_models/test_tutorial009.py
tests/test_tutorial/test_body_nested_models/test_tutorial009_py39.py
tests/test_tutorial/test_body_updates/test_tutorial001.py
tests/test_tutorial/test_body_updates/test_tutorial001_py310.py
tests/test_tutorial/test_body_updates/test_tutorial001_py39.py
tests/test_tutorial/test_conditional_openapi/test_tutorial001.py
tests/test_tutorial/test_cookie_params/test_tutorial001.py
tests/test_tutorial/test_cookie_params/test_tutorial001_an.py
tests/test_tutorial/test_cookie_params/test_tutorial001_an_py310.py
tests/test_tutorial/test_cookie_params/test_tutorial001_an_py39.py
tests/test_tutorial/test_cookie_params/test_tutorial001_py310.py
tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py
tests/test_tutorial/test_dataclasses/test_tutorial001.py
tests/test_tutorial/test_dataclasses/test_tutorial002.py
tests/test_tutorial/test_dataclasses/test_tutorial003.py
tests/test_tutorial/test_dependencies/test_tutorial001.py
tests/test_tutorial/test_dependencies/test_tutorial001_an.py
tests/test_tutorial/test_dependencies/test_tutorial001_an_py310.py
tests/test_tutorial/test_dependencies/test_tutorial001_an_py39.py
tests/test_tutorial/test_dependencies/test_tutorial001_py310.py
tests/test_tutorial/test_dependencies/test_tutorial004.py
tests/test_tutorial/test_dependencies/test_tutorial004_an.py
tests/test_tutorial/test_dependencies/test_tutorial004_an_py310.py
tests/test_tutorial/test_dependencies/test_tutorial004_an_py39.py
tests/test_tutorial/test_dependencies/test_tutorial004_py310.py
tests/test_tutorial/test_dependencies/test_tutorial006.py
tests/test_tutorial/test_dependencies/test_tutorial006_an.py
tests/test_tutorial/test_dependencies/test_tutorial006_an_py39.py
tests/test_tutorial/test_dependencies/test_tutorial012.py
tests/test_tutorial/test_dependencies/test_tutorial012_an.py
tests/test_tutorial/test_dependencies/test_tutorial012_an_py39.py
tests/test_tutorial/test_extra_data_types/test_tutorial001.py
tests/test_tutorial/test_extra_data_types/test_tutorial001_an.py
tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py310.py
tests/test_tutorial/test_extra_data_types/test_tutorial001_an_py39.py
tests/test_tutorial/test_extra_data_types/test_tutorial001_py310.py
tests/test_tutorial/test_handling_errors/test_tutorial004.py
tests/test_tutorial/test_handling_errors/test_tutorial005.py
tests/test_tutorial/test_handling_errors/test_tutorial006.py
tests/test_tutorial/test_header_params/test_tutorial001.py
tests/test_tutorial/test_header_params/test_tutorial001_an.py
tests/test_tutorial/test_header_params/test_tutorial001_an_py310.py
tests/test_tutorial/test_header_params/test_tutorial001_py310.py
tests/test_tutorial/test_header_params/test_tutorial002.py
tests/test_tutorial/test_header_params/test_tutorial002_an.py
tests/test_tutorial/test_header_params/test_tutorial002_an_py310.py
tests/test_tutorial/test_header_params/test_tutorial002_an_py39.py
tests/test_tutorial/test_header_params/test_tutorial002_py310.py
tests/test_tutorial/test_header_params/test_tutorial003.py
tests/test_tutorial/test_header_params/test_tutorial003_an.py
tests/test_tutorial/test_header_params/test_tutorial003_an_py310.py
tests/test_tutorial/test_header_params/test_tutorial003_an_py39.py
tests/test_tutorial/test_header_params/test_tutorial003_py310.py
tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py
tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial004.py
tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007.py
tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py [new file with mode: 0644]
tests/test_tutorial/test_path_operation_configurations/test_tutorial005.py
tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py310.py
tests/test_tutorial/test_path_operation_configurations/test_tutorial005_py39.py
tests/test_tutorial/test_path_params/test_tutorial005.py
tests/test_tutorial/test_query_params/test_tutorial005.py
tests/test_tutorial/test_query_params/test_tutorial006.py
tests/test_tutorial/test_query_params/test_tutorial006_py310.py
tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py
tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an.py
tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py310.py
tests/test_tutorial/test_query_params_str_validations/test_tutorial010_an_py39.py
tests/test_tutorial/test_query_params_str_validations/test_tutorial010_py310.py
tests/test_tutorial/test_query_params_str_validations/test_tutorial011.py
tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an.py
tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py310.py
tests/test_tutorial/test_query_params_str_validations/test_tutorial011_an_py39.py
tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py310.py
tests/test_tutorial/test_query_params_str_validations/test_tutorial011_py39.py
tests/test_tutorial/test_request_files/test_tutorial001.py
tests/test_tutorial/test_request_files/test_tutorial001_02.py
tests/test_tutorial/test_request_files/test_tutorial001_02_an.py
tests/test_tutorial/test_request_files/test_tutorial001_02_an_py310.py
tests/test_tutorial/test_request_files/test_tutorial001_02_an_py39.py
tests/test_tutorial/test_request_files/test_tutorial001_02_py310.py
tests/test_tutorial/test_request_files/test_tutorial001_an.py
tests/test_tutorial/test_request_files/test_tutorial001_an_py39.py
tests/test_tutorial/test_request_files/test_tutorial002.py
tests/test_tutorial/test_request_files/test_tutorial002_an.py
tests/test_tutorial/test_request_files/test_tutorial002_an_py39.py
tests/test_tutorial/test_request_files/test_tutorial002_py39.py
tests/test_tutorial/test_request_forms/test_tutorial001.py
tests/test_tutorial/test_request_forms/test_tutorial001_an.py
tests/test_tutorial/test_request_forms/test_tutorial001_an_py39.py
tests/test_tutorial/test_request_forms_and_files/test_tutorial001.py
tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an.py
tests/test_tutorial/test_request_forms_and_files/test_tutorial001_an_py39.py
tests/test_tutorial/test_response_model/test_tutorial003.py
tests/test_tutorial/test_response_model/test_tutorial003_01.py
tests/test_tutorial/test_response_model/test_tutorial003_01_py310.py
tests/test_tutorial/test_response_model/test_tutorial003_py310.py
tests/test_tutorial/test_response_model/test_tutorial004.py
tests/test_tutorial/test_response_model/test_tutorial004_py310.py
tests/test_tutorial/test_response_model/test_tutorial004_py39.py
tests/test_tutorial/test_response_model/test_tutorial005.py
tests/test_tutorial/test_response_model/test_tutorial005_py310.py
tests/test_tutorial/test_response_model/test_tutorial006.py
tests/test_tutorial/test_response_model/test_tutorial006_py310.py
tests/test_tutorial/test_schema_extra_example/test_tutorial001.py [new file with mode: 0644]
tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py [new file with mode: 0644]
tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310.py [new file with mode: 0644]
tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310_pv1.py [new file with mode: 0644]
tests/test_tutorial/test_schema_extra_example/test_tutorial004.py
tests/test_tutorial/test_schema_extra_example/test_tutorial004_an.py
tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py310.py
tests/test_tutorial/test_schema_extra_example/test_tutorial004_an_py39.py
tests/test_tutorial/test_schema_extra_example/test_tutorial004_py310.py
tests/test_tutorial/test_security/test_tutorial003.py
tests/test_tutorial/test_security/test_tutorial003_an.py
tests/test_tutorial/test_security/test_tutorial003_an_py310.py
tests/test_tutorial/test_security/test_tutorial003_an_py39.py
tests/test_tutorial/test_security/test_tutorial003_py310.py
tests/test_tutorial/test_security/test_tutorial005.py
tests/test_tutorial/test_security/test_tutorial005_an.py
tests/test_tutorial/test_security/test_tutorial005_an_py310.py
tests/test_tutorial/test_security/test_tutorial005_an_py39.py
tests/test_tutorial/test_security/test_tutorial005_py310.py
tests/test_tutorial/test_security/test_tutorial005_py39.py
tests/test_tutorial/test_settings/test_app02.py
tests/test_tutorial/test_settings/test_tutorial001.py [new file with mode: 0644]
tests/test_tutorial/test_settings/test_tutorial001_pv1.py [new file with mode: 0644]
tests/test_tutorial/test_sql_databases/test_sql_databases.py
tests/test_tutorial/test_sql_databases/test_sql_databases_middleware.py
tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py310.py
tests/test_tutorial/test_sql_databases/test_sql_databases_middleware_py39.py
tests/test_tutorial/test_sql_databases/test_sql_databases_py310.py
tests/test_tutorial/test_sql_databases/test_sql_databases_py39.py
tests/test_tutorial/test_sql_databases/test_testing_databases.py
tests/test_tutorial/test_sql_databases/test_testing_databases_py310.py
tests/test_tutorial/test_sql_databases/test_testing_databases_py39.py
tests/test_tutorial/test_sql_databases_peewee/test_sql_databases_peewee.py
tests/test_union_body.py
tests/test_union_inherited_body.py
tests/test_validate_response.py
tests/test_validate_response_dataclass.py
tests/test_validate_response_recursive/__init__.py [new file with mode: 0644]
tests/test_validate_response_recursive/app_pv1.py [moved from tests/test_validate_response_recursive.py with 58% similarity]
tests/test_validate_response_recursive/app_pv2.py [new file with mode: 0644]
tests/test_validate_response_recursive/test_validate_response_recursive_pv1.py [new file with mode: 0644]
tests/test_validate_response_recursive/test_validate_response_recursive_pv2.py [new file with mode: 0644]
tests/utils.py

index 84f101424ecc6d84cf65fcfdb9caea58407fba33..b95358d0136d4d628be502302a703792745ed046 100644 (file)
@@ -25,10 +25,12 @@ jobs:
         id: cache
         with:
           path: ${{ env.pythonLocation }}
-          key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
+          key: ${{ runner.os }}-python-${{ env.pythonLocation }}-pydantic-v2-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
       - name: Install Dependencies
         if: steps.cache.outputs.cache-hit != 'true'
         run: pip install -r requirements-tests.txt
+      - name: Install Pydantic v2
+        run: pip install "pydantic>=2.0.2,<3.0.0"
       - name: Lint
         run: bash scripts/lint.sh
 
@@ -37,6 +39,7 @@ jobs:
     strategy:
       matrix:
         python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
+        pydantic-version: ["pydantic-v1", "pydantic-v2"]
       fail-fast: false
     steps:
       - uses: actions/checkout@v3
@@ -51,10 +54,16 @@ jobs:
         id: cache
         with:
           path: ${{ env.pythonLocation }}
-          key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
+          key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ matrix.pydantic-version }}-${{ hashFiles('pyproject.toml', 'requirements-tests.txt') }}-test-v03
       - name: Install Dependencies
         if: steps.cache.outputs.cache-hit != 'true'
         run: pip install -r requirements-tests.txt
+      - name: Install Pydantic v1
+        if: matrix.pydantic-version == 'pydantic-v1'
+        run: pip install "pydantic>=1.10.0,<2.0.0"
+      - name: Install Pydantic v2
+        if: matrix.pydantic-version == 'pydantic-v2'
+        run: pip install "pydantic>=2.0.2,<3.0.0"
       - run: mkdir coverage
       - name: Test
         run: bash scripts/test.sh
index 7dc199367e905883149cd27d63610715cbb46555..36c71081eedfc27a7e9d2181ba047db8dfbe0486 100644 (file)
--- a/README.md
+++ b/README.md
@@ -447,6 +447,8 @@ To understand more about it, see the section <a href="https://fastapi.tiangolo.c
 Used by Pydantic:
 
 * <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - for email validation.
+* <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" target="_blank"><code>pydantic-settings</code></a> - for settings management.
+* <a href="https://docs.pydantic.dev/latest/usage/types/extra_types/extra_types/" target="_blank"><code>pydantic-extra-types</code></a> - for extra types to be used with Pydantic.
 
 Used by Starlette:
 
index 93c288e1b4f563e687fb97f7db81794afb42df01..12549a1903cf7933137e62552e9da2a093957bb2 100644 (file)
@@ -1,5 +1,12 @@
 # Async SQL (Relational) Databases
 
+!!! info
+    These docs are about to be updated. 🎉
+
+    The current version assumes Pydantic v1.
+
+    The new docs will include Pydantic v2 and will use <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel</a> once it is updated to use Pydantic v2 as well.
+
 You can also use <a href="https://github.com/encode/databases" class="external-link" target="_blank">`encode/databases`</a> with **FastAPI** to connect to databases using `async` and `await`.
 
 It is compatible with:
index 6cc5a938575d2c769451d2aea374b211486ba255..606db35c7512bcff7301a56088f3bfef2a32eda4 100644 (file)
@@ -1,5 +1,12 @@
 # NoSQL (Distributed / Big Data) Databases
 
+!!! info
+    These docs are about to be updated. 🎉
+
+    The current version assumes Pydantic v1.
+
+    The new docs will hopefully use Pydantic v2 and will use <a href="https://art049.github.io/odmantic/" class="external-link" target="_blank">ODMantic</a> with MongoDB.
+
 **FastAPI** can also be integrated with any <abbr title="Distributed database (Big Data), also 'Not Only SQL'">NoSQL</abbr>.
 
 Here we'll see an example using **<a href="https://www.couchbase.com/" class="external-link" target="_blank">Couchbase</a>**, a <abbr title="Document here refers to a JSON object (a dict), with keys and values, and those values can also be other JSON objects, arrays (lists), numbers, strings, booleans, etc.">document</abbr> based NoSQL database.
index 6d9a5fe708c27831706e772cc5fc6b2c5a4854c8..7ca88d43ed27797cd952464b07619c725228544d 100644 (file)
@@ -150,9 +150,20 @@ And you could do this even if the data type in the request is not JSON.
 
 For example, in this application we don't use FastAPI's integrated functionality to extract the JSON Schema from Pydantic models nor the automatic validation for JSON. In fact, we are declaring the request content type as YAML, not JSON:
 
-```Python hl_lines="17-22  24"
-{!../../../docs_src/path_operation_advanced_configuration/tutorial007.py!}
-```
+=== "Pydantic v2"
+
+    ```Python hl_lines="17-22  24"
+    {!> ../../../docs_src/path_operation_advanced_configuration/tutorial007.py!}
+    ```
+
+=== "Pydantic v1"
+
+    ```Python hl_lines="17-22  24"
+    {!> ../../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py!}
+    ```
+
+!!! info
+    In Pydantic version 1 the method to get the JSON Schema for a model was called `Item.schema()`, in Pydantic version 2, the method is called `Item.model_schema_json()`.
 
 Nevertheless, although we are not using the default integrated functionality, we are still using a Pydantic model to manually generate the JSON Schema for the data that we want to receive in YAML.
 
@@ -160,9 +171,20 @@ Then we use the request directly, and extract the body as `bytes`. This means th
 
 And then in our code, we parse that YAML content directly, and then we are again using the same Pydantic model to validate the YAML content:
 
-```Python hl_lines="26-33"
-{!../../../docs_src/path_operation_advanced_configuration/tutorial007.py!}
-```
+=== "Pydantic v2"
+
+    ```Python hl_lines="26-33"
+    {!> ../../../docs_src/path_operation_advanced_configuration/tutorial007.py!}
+    ```
+
+=== "Pydantic v1"
+
+    ```Python hl_lines="26-33"
+    {!> ../../../docs_src/path_operation_advanced_configuration/tutorial007_pv1.py!}
+    ```
+
+!!! info
+    In Pydantic version 1 the method to parse and validate an object was `Item.parse_obj()`, in Pydantic version 2, the method is called `Item.model_validate()`.
 
 !!! tip
     Here we re-use the same Pydantic model.
index 60ec9c92c620883131e50748b35b44fa65357f9f..8f6c7da93ae47eea13c27e9b673279193ff388cb 100644 (file)
@@ -125,7 +125,34 @@ That means that any value read in Python from an environment variable will be a
 
 ## Pydantic `Settings`
 
-Fortunately, Pydantic provides a great utility to handle these settings coming from environment variables with <a href="https://pydantic-docs.helpmanual.io/usage/settings/" class="external-link" target="_blank">Pydantic: Settings management</a>.
+Fortunately, Pydantic provides a great utility to handle these settings coming from environment variables with <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" class="external-link" target="_blank">Pydantic: Settings management</a>.
+
+### Install `pydantic-settings`
+
+First, install the `pydantic-settings` package:
+
+<div class="termy">
+
+```console
+$ pip install pydantic-settings
+---> 100%
+```
+
+</div>
+
+It also comes included when you install the `all` extras with:
+
+<div class="termy">
+
+```console
+$ pip install "fastapi[all]"
+---> 100%
+```
+
+</div>
+
+!!! info
+    In Pydantic v1 it came included with the main package. Now it is distributed as this independent package so that you can choose to install it or not if you don't need that functionality.
 
 ### Create the `Settings` object
 
@@ -135,9 +162,20 @@ The same way as with Pydantic models, you declare class attributes with type ann
 
 You can use all the same validation features and tools you use for Pydantic models, like different data types and additional validations with `Field()`.
 
-```Python hl_lines="2  5-8  11"
-{!../../../docs_src/settings/tutorial001.py!}
-```
+=== "Pydantic v2"
+
+    ```Python hl_lines="2  5-8  11"
+    {!> ../../../docs_src/settings/tutorial001.py!}
+    ```
+
+=== "Pydantic v1"
+
+    !!! info
+        In Pydantic v1 you would import `BaseSettings` directly from `pydantic` instead of from `pydantic_settings`.
+
+    ```Python hl_lines="2  5-8  11"
+    {!> ../../../docs_src/settings/tutorial001_pv1.py!}
+    ```
 
 !!! tip
     If you want something quick to copy and paste, don't use this example, use the last one below.
@@ -306,14 +344,28 @@ APP_NAME="ChimichangApp"
 
 And then update your `config.py` with:
 
-```Python hl_lines="9-10"
-{!../../../docs_src/settings/app03/config.py!}
-```
+=== "Pydantic v2"
+
+    ```Python hl_lines="9"
+    {!> ../../../docs_src/settings/app03_an/config.py!}
+    ```
 
-Here we create a class `Config` inside of your Pydantic `Settings` class, and set the `env_file` to the filename with the dotenv file we want to use.
+    !!! tip
+        The `model_config` attribute is used just for Pydantic configuration. You can read more at <a href="https://docs.pydantic.dev/latest/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>.
 
-!!! tip
-    The `Config` class is used just for Pydantic configuration. You can read more at <a href="https://pydantic-docs.helpmanual.io/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>
+=== "Pydantic v1"
+
+    ```Python hl_lines="9-10"
+    {!> ../../../docs_src/settings/app03_an/config_pv1.py!}
+    ```
+
+    !!! tip
+        The `Config` class is used just for Pydantic configuration. You can read more at <a href="https://docs.pydantic.dev/1.10/usage/model_config/" class="external-link" target="_blank">Pydantic Model Config</a>.
+
+!!! info
+    In Pydantic version 1 the configuration was done in an internal class `Config`, in Pydantic version 2 it's done in an attribute `model_config`. This attribute takes a `dict`, and to get autocompletion and inline errors you can import and use `SettingsConfigDict` to define that `dict`.
+
+Here we define the config `env_file` inside of your Pydantic `Settings` class, and set the value to the filename with the dotenv file we want to use.
 
 ### Creating the `Settings` only once with `lru_cache`
 
index b4ea61367e8200855a7d32308a24f36f92705141..6a469634fa4bb4345ebf044152bb0b4e4084a429 100644 (file)
@@ -5,6 +5,13 @@
 
     Feel free to skip this.
 
+    Peewee is not recommended with FastAPI as it doesn't play well with anything async Python. There are several better alternatives.
+
+!!! info
+    These docs assume Pydantic v1.
+
+    Because Pewee doesn't play well with anything async and there are better alternatives, I won't update these docs for Pydantic v2, they are kept for now only for historical purposes.
+
 If you are starting a project from scratch, you are probably better off with SQLAlchemy ORM ([SQL (Relational) Databases](../tutorial/sql-databases.md){.internal-link target=_blank}), or any other async ORM.
 
 If you already have a code base that uses <a href="https://docs.peewee-orm.com/en/latest/" class="external-link" target="_blank">Peewee ORM</a>, you can check here how to use it with **FastAPI**.
index 13a6959b6fca8d7d87812066513f6dbbcdd9d037..1c0669b9ce575919ee36dea386f7bed2f433769b 100644 (file)
@@ -1,5 +1,12 @@
 # Testing a Database
 
+!!! info
+    These docs are about to be updated. 🎉
+
+    The current version assumes Pydantic v1, and SQLAlchemy versions less than 2.0.
+
+    The new docs will include Pydantic v2 and will use <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel</a> (which is also based on SQLAlchemy) once it is updated to use Pydantic v2 as well.
+
 You can use the same dependency overrides from [Testing Dependencies with Overrides](testing-dependencies.md){.internal-link target=_blank} to alter a database for testing.
 
 You could want to set up a different database for testing, rollback the data after the tests, pre-fill it with some testing data, etc.
index afd6d7138f0a817e4facff8b047deef99f69b81d..ebd74bc8f00d2e4a9bd161236518f6d0a022ae72 100644 (file)
@@ -446,6 +446,8 @@ To understand more about it, see the section <a href="https://fastapi.tiangolo.c
 Used by Pydantic:
 
 * <a href="https://github.com/JoshData/python-email-validator" target="_blank"><code>email_validator</code></a> - for email validation.
+* <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" target="_blank"><code>pydantic-settings</code></a> - for settings management.
+* <a href="https://docs.pydantic.dev/latest/usage/types/extra_types/extra_types/" target="_blank"><code>pydantic-extra-types</code></a> - for extra types to be used with Pydantic.
 
 Used by Starlette:
 
index f22146f4bd19fae37592d61e6865df2d4f9a1d3d..f4ce74404fa4a9840682b4235916cb8df2f557bb 100644 (file)
@@ -2,6 +2,79 @@
 
 ## Latest Changes
 
+✨ Support for **Pydantic v2** ✨
+
+Pydantic version 2 has the **core** re-written in **Rust** and includes a lot of improvements and features, for example:
+
+* Improved **correctness** in corner cases.
+* **Safer** types.
+* Better **performance** and **less energy** consumption.
+* Better **extensibility**.
+* etc.
+
+...all this while keeping the **same Python API**. In most of the cases, for simple models, you can simply upgrade the Pydantic version and get all the benefits. 🚀
+
+In some cases, for pure data validation and processing, you can get performance improvements of **20x** or more. This means 2,000% or more. 🤯
+
+When you use **FastAPI**, there's a lot more going on, processing the request and response, handling dependencies, executing **your own code**, and particularly, **waiting for the network**. But you will probably still get some nice performance improvements just from the upgrade.
+
+The focus of this release is **compatibility** with Pydantic v1 and v2, to make sure your current apps keep working. Later there will be more focus on refactors, correctness, code improvements, and then **performance** improvements. Some third-party early beta testers that ran benchmarks on the beta releases of FastAPI reported improvements of **2x - 3x**. Which is not bad for just doing `pip install --upgrade fastapi pydantic`. This was not an official benchmark and I didn't check it myself, but it's a good sign.
+
+### Migration
+
+Check out the [Pydantic migration guide](https://docs.pydantic.dev/2.0/migration/).
+
+For the things that need changes in your Pydantic models, the Pydantic team built [`bump-pydantic`](https://github.com/pydantic/bump-pydantic).
+
+A command line tool that will **process your code** and update most of the things **automatically** for you. Make sure you have your code in git first, and review each of the changes to make sure everything is correct before committing the changes.
+
+### Pydantic v1
+
+**This version of FastAPI still supports Pydantic v1**. And although Pydantic v1 will be deprecated at some point, ti will still be supported for a while.
+
+This means that you can install the new Pydantic v2, and if something fails, you can install Pydantic v1 while you fix any problems you might have, but having the latest FastAPI.
+
+There are **tests for both Pydantic v1 and v2**, and test **coverage** is kept at **100%**.
+
+### Changes
+
+* There are **new parameter** fields supported by Pydantic `Field()` for:
+
+    * `Path()`
+    * `Query()`
+    * `Header()`
+    * `Cookie()`
+    * `Body()`
+    * `Form()`
+    * `File()`
+
+* The new parameter fields are:
+
+    * `default_factory`
+    * `alias_priority`
+    * `validation_alias`
+    * `serialization_alias`
+    * `discriminator`
+    * `strict`
+    * `multiple_of`
+    * `allow_inf_nan`
+    * `max_digits`
+    * `decimal_places`
+    * `json_schema_extra`
+
+...you can read about them in the Pydantic docs.
+
+* The parameter `regex` has been deprecated and replaced by `pattern`.
+    * You can read more about it in the docs for [Query Parameters and String Validations: Add regular expressions](https://fastapi.tiangolo.com/tutorial/query-params-str-validations/#add-regular-expressions).
+* New Pydantic models use an improved and simplified attribute `model_config` that takes a simple dict instead of an internal class `Config` for their configuration.
+    * You can read more about it in the docs for [Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/).
+* The attribute `schema_extra` for the internal class `Config` has been replaced by the key `json_schema_extra` in the new `model_config` dict.
+    * You can read more about it in the docs for [Declare Request Example Data](https://fastapi.tiangolo.com/tutorial/schema-extra-example/).
+* When you install `"fastapi[all]"` it now also includes:
+    * <a href="https://docs.pydantic.dev/latest/usage/pydantic_settings/" target="_blank"><code>pydantic-settings</code></a> - for settings management.
+    * <a href="https://docs.pydantic.dev/latest/usage/types/extra_types/extra_types/" target="_blank"><code>pydantic-extra-types</code></a> - for extra types to be used with Pydantic.
+* Now Pydantic Settings is an additional optional package (included in `"fastapi[all]"`). To use settings you should now import `from pydantic_settings import BaseSettings` instead of importing from `pydantic` directly.
+    * You can read more about it in the docs for [Settings and Environment Variables](https://fastapi.tiangolo.com/advanced/settings/).
 
 ## 0.99.1
 
index 549e6c75b58a1ae0215c250723a0c0cf4dfbd06a..f87adddcb2ddcc0ddd10cff40ee79bd9ed644912 100644 (file)
@@ -277,7 +277,7 @@ You can also add a parameter `min_length`:
 
 ## Add regular expressions
 
-You can define a <abbr title="A regular expression, regex or regexp is a sequence of characters that define a search pattern for strings.">regular expression</abbr> that the parameter should match:
+You can define a <abbr title="A regular expression, regex or regexp is a sequence of characters that define a search pattern for strings.">regular expression</abbr> `pattern` that the parameter should match:
 
 === "Python 3.10+"
 
@@ -315,7 +315,7 @@ You can define a <abbr title="A regular expression, regex or regexp is a sequenc
     {!> ../../../docs_src/query_params_str_validations/tutorial004.py!}
     ```
 
-This specific regular expression checks that the received parameter value:
+This specific regular expression pattern checks that the received parameter value:
 
 * `^`: starts with the following characters, doesn't have characters before.
 * `fixedquery`: has the exact value `fixedquery`.
@@ -325,6 +325,20 @@ If you feel lost with all these **"regular expression"** ideas, don't worry. The
 
 But whenever you need them and go and learn them, know that you can already use them directly in **FastAPI**.
 
+### Pydantic v1 `regex` instead of `pattern`
+
+Before Pydantic version 2 and before FastAPI 0.100.0, the parameter was called `regex` instead of `pattern`, but it's now deprecated.
+
+You could still see some code using it:
+
+=== "Python 3.10+ Pydantic v1"
+
+    ```Python hl_lines="11"
+    {!> ../../../docs_src/query_params_str_validations/tutorial004_an_py310_regex.py!}
+    ```
+
+But know that this is deprecated and it should be updated to use the new parameter `pattern`. 🤓
+
 ## Default values
 
 You can, of course, use default values other than `None`.
index 86ccb1f5adb1f89700f379082d81a97783ab631d..39d184763fef3a2565680ea4b1672483b16a9a7b 100644 (file)
@@ -4,24 +4,48 @@ You can declare examples of the data your app can receive.
 
 Here are several ways to do it.
 
-## Pydantic `schema_extra`
+## Extra JSON Schema data in Pydantic models
 
-You can declare `examples` for a Pydantic model using `Config` and `schema_extra`, as described in <a href="https://pydantic-docs.helpmanual.io/usage/schema/#schema-customization" class="external-link" target="_blank">Pydantic's docs: Schema customization</a>:
+You can declare `examples` for a Pydantic model that will be added to the generated JSON Schema.
 
-=== "Python 3.10+"
+=== "Python 3.10+ Pydantic v2"
 
-    ```Python hl_lines="13-23"
+    ```Python hl_lines="13-24"
     {!> ../../../docs_src/schema_extra_example/tutorial001_py310.py!}
     ```
 
-=== "Python 3.6+"
+=== "Python 3.10+ Pydantic v1"
 
-    ```Python hl_lines="15-25"
+    ```Python hl_lines="13-23"
+    {!> ../../../docs_src/schema_extra_example/tutorial001_py310_pv1.py!}
+    ```
+
+=== "Python 3.6+ Pydantic v2"
+
+    ```Python hl_lines="15-26"
     {!> ../../../docs_src/schema_extra_example/tutorial001.py!}
     ```
 
+=== "Python 3.6+ Pydantic v1"
+
+    ```Python hl_lines="15-25"
+    {!> ../../../docs_src/schema_extra_example/tutorial001_pv1.py!}
+    ```
+
 That extra info will be added as-is to the output **JSON Schema** for that model, and it will be used in the API docs.
 
+=== "Pydantic v2"
+
+    In Pydantic version 2, you would use the attribute `model_config`, that takes a `dict` as described in <a href="https://docs.pydantic.dev/latest/usage/model_config/" class="external-link" target="_blank">Pydantic's docs: Model Config</a>.
+
+    You can set `"json_schema_extra"` with a `dict` containing any additonal data you would like to show up in the generated JSON Schema, including `examples`.
+
+=== "Pydantic v1"
+
+    In Pydantic version 1, you would use an internal class `Config` and `schema_extra`, as described in <a href="https://docs.pydantic.dev/1.10/usage/schema/#schema-customization" class="external-link" target="_blank">Pydantic's docs: Schema customization</a>.
+
+    You can set `schema_extra` with a `dict` containing any additonal data you would like to show up in the generated JSON Schema, including `examples`.
+
 !!! tip
     You could use the same technique to extend the JSON Schema and add your own custom extra info.
 
index fd66c5add072020675b5a03be23b28c82a14eb15..6e0e5dc06eaa1c85554c87f9700fcffd8f4e11d1 100644 (file)
@@ -1,5 +1,12 @@
 # SQL (Relational) Databases
 
+!!! info
+    These docs are about to be updated. 🎉
+
+    The current version assumes Pydantic v1, and SQLAlchemy versions less than 2.0.
+
+    The new docs will include Pydantic v2 and will use <a href="https://sqlmodel.tiangolo.com/" class="external-link" target="_blank">SQLModel</a> (which is also based on SQLAlchemy) once it is updated to use Pydantic v2 as well.
+
 **FastAPI** doesn't require you to use a SQL (relational) database.
 
 But you can use any relational database that you want.
index 717e723e83a89e10ec80fb0e8276ea400a4cb7e5..eedb0d274281615aeb94610fefdf57f837fc6927 100644 (file)
@@ -1,5 +1,5 @@
 from fastapi import FastAPI
-from pydantic import BaseSettings
+from pydantic_settings import BaseSettings
 
 
 class Settings(BaseSettings):
index 065439acceb7dec558776433d57d8499dfa0b2f3..06675cbc09808a020cdbc0f6d1b1efc2793de7a8 100644 (file)
@@ -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
 
 
index 065439acceb7dec558776433d57d8499dfa0b2f3..06675cbc09808a020cdbc0f6d1b1efc2793de7a8 100644 (file)
@@ -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
 
 
index d51752bb875ccca282f100672e2eed6891f048dd..972ddbd2cc918757e41c1417480d2d4a4039390b 100644 (file)
@@ -16,7 +16,7 @@ class Item(BaseModel):
     "/items/",
     openapi_extra={
         "requestBody": {
-            "content": {"application/x-yaml": {"schema": Item.schema()}},
+            "content": {"application/x-yaml": {"schema": Item.model_json_schema()}},
             "required": True,
         },
     },
@@ -28,7 +28,7 @@ async def create_item(request: Request):
     except yaml.YAMLError:
         raise HTTPException(status_code=422, detail="Invalid YAML")
     try:
-        item = Item.parse_obj(data)
+        item = Item.model_validate(data)
     except ValidationError as e:
         raise HTTPException(status_code=422, detail=e.errors())
     return item
diff --git a/docs_src/path_operation_advanced_configuration/tutorial007_pv1.py b/docs_src/path_operation_advanced_configuration/tutorial007_pv1.py
new file mode 100644 (file)
index 0000000..d51752b
--- /dev/null
@@ -0,0 +1,34 @@
+from typing import List
+
+import yaml
+from fastapi import FastAPI, HTTPException, Request
+from pydantic import BaseModel, ValidationError
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+    name: str
+    tags: List[str]
+
+
+@app.post(
+    "/items/",
+    openapi_extra={
+        "requestBody": {
+            "content": {"application/x-yaml": {"schema": Item.schema()}},
+            "required": True,
+        },
+    },
+)
+async def create_item(request: Request):
+    raw_body = await request.body()
+    try:
+        data = yaml.safe_load(raw_body)
+    except yaml.YAMLError:
+        raise HTTPException(status_code=422, detail="Invalid YAML")
+    try:
+        item = Item.parse_obj(data)
+    except ValidationError as e:
+        raise HTTPException(status_code=422, detail=e.errors())
+    return item
index 5a7129816c4a602e9dc239ff50753484ba8bedb6..3639b6c38fe7593c53ad96b7e1769fc652ee7aa2 100644 (file)
@@ -8,7 +8,7 @@ app = FastAPI()
 @app.get("/items/")
 async def read_items(
     q: Union[str, None] = Query(
-        default=None, min_length=3, max_length=50, regex="^fixedquery$"
+        default=None, min_length=3, max_length=50, pattern="^fixedquery$"
     )
 ):
     results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
index 5346b997bd09d28911198b7566bf2df8fbca5f5d..24698c7b34fac0fe7cd651fb7d12cf5eb0540c50 100644 (file)
@@ -9,7 +9,7 @@ app = FastAPI()
 @app.get("/items/")
 async def read_items(
     q: Annotated[
-        Union[str, None], Query(min_length=3, max_length=50, regex="^fixedquery$")
+        Union[str, None], Query(min_length=3, max_length=50, pattern="^fixedquery$")
     ] = None
 ):
     results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
index 8fd375b3d549f3f4e891fffce7a8f02d58b4b940..b7b629ee8acec17e8815e4d521525f332b0beef0 100644 (file)
@@ -8,7 +8,7 @@ app = FastAPI()
 @app.get("/items/")
 async def read_items(
     q: Annotated[
-        str | None, Query(min_length=3, max_length=50, regex="^fixedquery$")
+        str | None, Query(min_length=3, max_length=50, pattern="^fixedquery$")
     ] = None
 ):
     results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
diff --git a/docs_src/query_params_str_validations/tutorial004_an_py310_regex.py b/docs_src/query_params_str_validations/tutorial004_an_py310_regex.py
new file mode 100644 (file)
index 0000000..8fd375b
--- /dev/null
@@ -0,0 +1,17 @@
+from typing import Annotated
+
+from fastapi import FastAPI, Query
+
+app = FastAPI()
+
+
+@app.get("/items/")
+async def read_items(
+    q: Annotated[
+        str | None, Query(min_length=3, max_length=50, regex="^fixedquery$")
+    ] = None
+):
+    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
+    if q:
+        results.update({"q": q})
+    return results
index 2fd82db75463654571071c756d3899091f879490..8e9a6fc32d88fc53becfd488c4f34ac217c65106 100644 (file)
@@ -8,7 +8,7 @@ app = FastAPI()
 @app.get("/items/")
 async def read_items(
     q: Annotated[
-        Union[str, None], Query(min_length=3, max_length=50, regex="^fixedquery$")
+        Union[str, None], Query(min_length=3, max_length=50, pattern="^fixedquery$")
     ] = None
 ):
     results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
index 180a2e5112a0bb94dcbc78f3d82220ff42995ace..f80798bcb8f8f64bdc3e9e55f4a94b6be37b3f80 100644 (file)
@@ -6,7 +6,7 @@ app = FastAPI()
 @app.get("/items/")
 async def read_items(
     q: str
-    | None = Query(default=None, min_length=3, max_length=50, regex="^fixedquery$")
+    | None = Query(default=None, min_length=3, max_length=50, pattern="^fixedquery$")
 ):
     results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
     if q:
index 35443d19470614a20268334a4bb07401f7fb0b98..3314f8b6d6440ae26733599daa0ce6b2239fabb9 100644 (file)
@@ -14,7 +14,7 @@ async def read_items(
         description="Query string for the items to search in the database that have a good match",
         min_length=3,
         max_length=50,
-        regex="^fixedquery$",
+        pattern="^fixedquery$",
         deprecated=True,
     )
 ):
index 8995f3f57a34592b9173a57aceb76ad99c71c848..c5df00897f988140611d832dd0fa448459547a45 100644 (file)
@@ -16,7 +16,7 @@ async def read_items(
             description="Query string for the items to search in the database that have a good match",
             min_length=3,
             max_length=50,
-            regex="^fixedquery$",
+            pattern="^fixedquery$",
             deprecated=True,
         ),
     ] = None
index cfa81926cf72253caae3c78f6077cb191174bb6f..a8e8c099b5e0a3f7f4346e5c27613f4eb6407546 100644 (file)
@@ -15,7 +15,7 @@ async def read_items(
             description="Query string for the items to search in the database that have a good match",
             min_length=3,
             max_length=50,
-            regex="^fixedquery$",
+            pattern="^fixedquery$",
             deprecated=True,
         ),
     ] = None
index 220eaabf477ff98afafd3a374a857eb5198fb094..955880dd6a65b59568d1b3e3bf7fe16eb968662f 100644 (file)
@@ -15,7 +15,7 @@ async def read_items(
             description="Query string for the items to search in the database that have a good match",
             min_length=3,
             max_length=50,
-            regex="^fixedquery$",
+            pattern="^fixedquery$",
             deprecated=True,
         ),
     ] = None
index f2839516e64466b270dba73cb78f25e92fa74301..9ea7b3c49f81f1d1101e117e4615a6d21123a620 100644 (file)
@@ -13,7 +13,7 @@ async def read_items(
         description="Query string for the items to search in the database that have a good match",
         min_length=3,
         max_length=50,
-        regex="^fixedquery$",
+        pattern="^fixedquery$",
         deprecated=True,
     )
 ):
index 6ab96ff859d0d9c3cd47ac01b9fed22a788e4f32..32a66db3a97b826ca1f9b96f3a13d952cfd598f3 100644 (file)
@@ -12,8 +12,8 @@ class Item(BaseModel):
     price: float
     tax: Union[float, None] = None
 
-    class Config:
-        schema_extra = {
+    model_config = {
+        "json_schema_extra": {
             "examples": [
                 {
                     "name": "Foo",
@@ -23,6 +23,7 @@ class Item(BaseModel):
                 }
             ]
         }
+    }
 
 
 @app.put("/items/{item_id}")
diff --git a/docs_src/schema_extra_example/tutorial001_pv1.py b/docs_src/schema_extra_example/tutorial001_pv1.py
new file mode 100644 (file)
index 0000000..6ab96ff
--- /dev/null
@@ -0,0 +1,31 @@
+from typing import Union
+
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+    name: str
+    description: Union[str, None] = None
+    price: float
+    tax: Union[float, None] = None
+
+    class Config:
+        schema_extra = {
+            "examples": [
+                {
+                    "name": "Foo",
+                    "description": "A very nice Item",
+                    "price": 35.4,
+                    "tax": 3.2,
+                }
+            ]
+        }
+
+
+@app.put("/items/{item_id}")
+async def update_item(item_id: int, item: Item):
+    results = {"item_id": item_id, "item": item}
+    return results
index ec83f1112f20fb80798d67ff575e2e60f899bf7c..84aa5fc122c4f02314a7463e5f8daacfb0b76497 100644 (file)
@@ -10,8 +10,8 @@ class Item(BaseModel):
     price: float
     tax: float | None = None
 
-    class Config:
-        schema_extra = {
+    model_config = {
+        "json_schema_extra": {
             "examples": [
                 {
                     "name": "Foo",
@@ -21,6 +21,7 @@ class Item(BaseModel):
                 }
             ]
         }
+    }
 
 
 @app.put("/items/{item_id}")
diff --git a/docs_src/schema_extra_example/tutorial001_py310_pv1.py b/docs_src/schema_extra_example/tutorial001_py310_pv1.py
new file mode 100644 (file)
index 0000000..ec83f11
--- /dev/null
@@ -0,0 +1,29 @@
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+    name: str
+    description: str | None = None
+    price: float
+    tax: float | None = None
+
+    class Config:
+        schema_extra = {
+            "examples": [
+                {
+                    "name": "Foo",
+                    "description": "A very nice Item",
+                    "price": 35.4,
+                    "tax": 3.2,
+                }
+            ]
+        }
+
+
+@app.put("/items/{item_id}")
+async def update_item(item_id: int, item: Item):
+    results = {"item_id": item_id, "item": item}
+    return results
index defede9db401eb15cd6c8a1eb84bf4324e979b21..b31b8811d6539ac1d8e09872ab1cb7772673db61 100644 (file)
@@ -1,4 +1,4 @@
-from pydantic import BaseSettings
+from pydantic_settings import BaseSettings
 
 
 class Settings(BaseSettings):
index 9a7829135690d814a7344e75966739dcdf610858..e17b5035dcc77f53a3c87213eed9f77a44506440 100644 (file)
@@ -1,4 +1,4 @@
-from pydantic import BaseSettings
+from pydantic_settings import BaseSettings
 
 
 class Settings(BaseSettings):
index 9a7829135690d814a7344e75966739dcdf610858..e17b5035dcc77f53a3c87213eed9f77a44506440 100644 (file)
@@ -1,4 +1,4 @@
-from pydantic import BaseSettings
+from pydantic_settings import BaseSettings
 
 
 class Settings(BaseSettings):
index 9a7829135690d814a7344e75966739dcdf610858..e17b5035dcc77f53a3c87213eed9f77a44506440 100644 (file)
@@ -1,4 +1,4 @@
-from pydantic import BaseSettings
+from pydantic_settings import BaseSettings
 
 
 class Settings(BaseSettings):
index e1c3ee30063b744f5ea4c0d0990ee6855219d419..942aea3e58b70090cd8c9bdc7e8071b92fb491dc 100644 (file)
@@ -1,4 +1,4 @@
-from pydantic import BaseSettings
+from pydantic_settings import BaseSettings
 
 
 class Settings(BaseSettings):
index e1c3ee30063b744f5ea4c0d0990ee6855219d419..08f8f88c280ac1e61428b797322b290c2eefd3fb 100644 (file)
@@ -1,4 +1,4 @@
-from pydantic import BaseSettings
+from pydantic_settings import BaseSettings, SettingsConfigDict
 
 
 class Settings(BaseSettings):
@@ -6,5 +6,4 @@ class Settings(BaseSettings):
     admin_email: str
     items_per_user: int = 50
 
-    class Config:
-        env_file = ".env"
+    model_config = SettingsConfigDict(env_file=".env")
diff --git a/docs_src/settings/app03_an/config_pv1.py b/docs_src/settings/app03_an/config_pv1.py
new file mode 100644 (file)
index 0000000..e1c3ee3
--- /dev/null
@@ -0,0 +1,10 @@
+from pydantic import BaseSettings
+
+
+class Settings(BaseSettings):
+    app_name: str = "Awesome API"
+    admin_email: str
+    items_per_user: int = 50
+
+    class Config:
+        env_file = ".env"
index e1c3ee30063b744f5ea4c0d0990ee6855219d419..942aea3e58b70090cd8c9bdc7e8071b92fb491dc 100644 (file)
@@ -1,4 +1,4 @@
-from pydantic import BaseSettings
+from pydantic_settings import BaseSettings
 
 
 class Settings(BaseSettings):
index 0cfd1b6632f0bad274578dd028a3b592f5cb8b02..d48c4c060cfd1f70dba25f69aee6f2ba0cf3448c 100644 (file)
@@ -1,5 +1,5 @@
 from fastapi import FastAPI
-from pydantic import BaseSettings
+from pydantic_settings import BaseSettings
 
 
 class Settings(BaseSettings):
diff --git a/docs_src/settings/tutorial001_pv1.py b/docs_src/settings/tutorial001_pv1.py
new file mode 100644 (file)
index 0000000..0cfd1b6
--- /dev/null
@@ -0,0 +1,21 @@
+from fastapi import FastAPI
+from pydantic import BaseSettings
+
+
+class Settings(BaseSettings):
+    app_name: str = "Awesome API"
+    admin_email: str
+    items_per_user: int = 50
+
+
+settings = Settings()
+app = FastAPI()
+
+
+@app.get("/info")
+async def info():
+    return {
+        "app_name": settings.app_name,
+        "admin_email": settings.admin_email,
+        "items_per_user": settings.items_per_user,
+    }
index 2d1bac2e13c710d17b12077e41864ecca2860865..5eb3c4de2756b20c685f8a816c954514e34a6667 100644 (file)
@@ -1,6 +1,6 @@
 """FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
 
-__version__ = "0.99.1"
+__version__ = "0.100.0-beta3"
 
 from starlette import status as status
 
diff --git a/fastapi/_compat.py b/fastapi/_compat.py
new file mode 100644 (file)
index 0000000..2233fe3
--- /dev/null
@@ -0,0 +1,616 @@
+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._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 MultiHostUrl as MultiHostUrl
+    from pydantic_core import PydanticUndefined, PydanticUndefinedType
+    from pydantic_core import Url as Url
+    from pydantic_core.core_schema import (
+        general_plain_validator_function as general_plain_validator_function,
+    )
+
+    Required = PydanticUndefined
+    Undefined = PydanticUndefined
+    UndefinedType = PydanticUndefinedType
+    evaluate_forwardref = eval_type_lenient
+    Validator = Any
+
+    class BaseConfig:
+        pass
+
+    class ErrorWrapper(Exception):
+        pass
+
+    @dataclass
+    class ModelField:
+        field_info: FieldInfo
+        name: str
+        mode: Literal["validation", "serialization"] = "validation"
+
+        @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: Tuple[Union[int, str], ...] = (),
+        ) -> Tuple[Any, Union[List[Dict[str, Any]], None]]:
+            try:
+                return (
+                    self._type_adapter.validate_python(value, from_attributes=True),
+                    None,
+                )
+            except ValidationError as exc:
+                return None, _regenerate_error_with_loc(
+                    errors=exc.errors(), loc_prefix=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_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]
+
+    def _model_rebuild(model: Type[BaseModel]) -> None:
+        model.model_rebuild()
+
+    def _model_dump(
+        model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
+    ) -> Any:
+        return model.model_dump(mode=mode, **kwargs)
+
+    def _get_model_config(model: BaseModel) -> Any:
+        return model.model_config
+
+    def get_schema_from_model_field(
+        *,
+        field: ModelField,
+        schema_generator: GenerateJsonSchema,
+        model_name_map: ModelNameMap,
+        field_mapping: Dict[
+            Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
+        ],
+    ) -> Dict[str, Any]:
+        # This expects that GenerateJsonSchema was already used to generate the definitions
+        json_schema = field_mapping[(field, field.mode)]
+        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
+
+    def get_compat_model_name_map(fields: List[ModelField]) -> ModelNameMap:
+        return {}
+
+    def get_definitions(
+        *,
+        fields: List[ModelField],
+        schema_generator: GenerateJsonSchema,
+        model_name_map: ModelNameMap,
+    ) -> Tuple[
+        Dict[
+            Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
+        ],
+        Dict[str, Dict[str, Any]],
+    ]:
+        inputs = [
+            (field, field.mode, field._type_adapter.core_schema) for field in fields
+        ]
+        field_mapping, definitions = schema_generator.generate_definitions(
+            inputs=inputs
+        )
+        return field_mapping, definitions  # type: ignore[return-value]
+
+    def is_scalar_field(field: ModelField) -> bool:
+        from fastapi import params
+
+        return field_annotation_is_scalar(
+            field.field_info.annotation
+        ) and not isinstance(field.field_info, params.Body)
+
+    def is_sequence_field(field: ModelField) -> bool:
+        return field_annotation_is_sequence(field.field_info.annotation)
+
+    def is_scalar_sequence_field(field: ModelField) -> bool:
+        return field_annotation_is_scalar_sequence(field.field_info.annotation)
+
+    def is_bytes_field(field: ModelField) -> bool:
+        return is_bytes_or_nonable_bytes_annotation(field.type_)
+
+    def is_bytes_sequence_field(field: ModelField) -> bool:
+        return is_bytes_sequence_annotation(field.type_)
+
+    def copy_field_info(*, field_info: FieldInfo, annotation: Any) -> FieldInfo:
+        return type(field_info).from_annotation(annotation)
+
+    def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
+        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]
+
+    def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]:
+        error = ValidationError.from_exception_data(
+            "Field required", [{"type": "missing", "loc": loc, "input": {}}]
+        ).errors()[0]
+        error["input"] = None
+        return error  # type: ignore[return-value]
+
+    def create_body_model(
+        *, fields: Sequence[ModelField], model_name: str
+    ) -> Type[BaseModel]:
+        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:
+    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
+    )
+
+    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 _model_rebuild(model: Type[BaseModel]) -> None:
+        model.update_forward_refs()
+
+    def _model_dump(
+        model: BaseModel, mode: Literal["json", "python"] = "json", **kwargs: Any
+    ) -> Any:
+        return model.dict(**kwargs)
+
+    def _get_model_config(model: BaseModel) -> Any:
+        return model.__config__  # type: ignore[attr-defined]
+
+    def get_schema_from_model_field(
+        *,
+        field: ModelField,
+        schema_generator: GenerateJsonSchema,
+        model_name_map: ModelNameMap,
+        field_mapping: Dict[
+            Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
+        ],
+    ) -> Dict[str, Any]:
+        # This expects that GenerateJsonSchema was already used to generate the definitions
+        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:
+        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,
+    ) -> Tuple[
+        Dict[
+            Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
+        ],
+        Dict[str, Dict[str, Any]],
+    ]:
+        models = get_flat_models_from_fields(fields, known_models=set())
+        return {}, get_model_definitions(
+            flat_models=models, model_name_map=model_name_map
+        )
+
+    def is_scalar_field(field: ModelField) -> bool:
+        return is_pv1_scalar_field(field)
+
+    def is_sequence_field(field: ModelField) -> bool:
+        return field.shape in sequence_shapes or _annotation_is_sequence(field.type_)  # type: ignore[attr-defined]
+
+    def is_scalar_sequence_field(field: ModelField) -> bool:
+        return is_pv1_scalar_sequence_field(field)
+
+    def is_bytes_field(field: ModelField) -> bool:
+        return lenient_issubclass(field.type_, bytes)
+
+    def is_bytes_sequence_field(field: ModelField) -> bool:
+        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:
+        return copy(field_info)
+
+    def serialize_sequence_value(*, field: ModelField, value: Any) -> Sequence[Any]:
+        return sequence_shape_to_type[field.shape](value)  # type: ignore[no-any-return,attr-defined]
+
+    def get_missing_field_error(loc: Tuple[str, ...]) -> Dict[str, Any]:
+        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]:
+        BodyModel = create_model(model_name)
+        for f in fields:
+            BodyModel.__fields__[f.name] = f  # type: ignore[index]
+        return BodyModel
+
+
+def _regenerate_error_with_loc(
+    *, errors: Sequence[Any], loc_prefix: Tuple[Union[str, int], ...]
+) -> List[Dict[str, Any]]:
+    updated_loc_errors: List[Any] = [
+        {**err, "loc": loc_prefix + err.get("loc", ())}
+        for err in _normalize_errors(errors)
+    ]
+
+    return updated_loc_errors
+
+
+def _annotation_is_sequence(annotation: Union[Type[Any], None]) -> bool:
+    if lenient_issubclass(annotation, (str, bytes)):
+        return False
+    return lenient_issubclass(annotation, sequence_types)
+
+
+def field_annotation_is_sequence(annotation: Union[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: Union[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: Union[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:
+    # 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: Union[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_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: Any) -> bool:
+    origin = get_origin(annotation)
+    if origin is Union or origin is UnionType:
+        at_least_one = False
+        for arg in get_args(annotation):
+            if is_bytes_sequence_annotation(arg):
+                at_least_one = True
+                continue
+        return at_least_one
+    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: Any) -> bool:
+    origin = get_origin(annotation)
+    if origin is Union or origin is UnionType:
+        at_least_one = False
+        for arg in get_args(annotation):
+            if is_uploadfile_sequence_annotation(arg):
+                at_least_one = True
+                continue
+        return at_least_one
+    return field_annotation_is_sequence(annotation) and all(
+        is_uploadfile_or_nonable_uploadfile_annotation(sub_annotation)
+        for sub_annotation in get_args(annotation)
+    )
index 88f861c1ef0702cd8c0608cb565113a3261050d2..e32cfa03d20cbfd8ee588b943d15cf1b38e2b951 100644 (file)
@@ -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
@@ -305,8 +304,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,
@@ -363,8 +362,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,
@@ -484,8 +483,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,
@@ -539,8 +538,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,
@@ -594,8 +593,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,
@@ -649,8 +648,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,
@@ -704,8 +703,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,
@@ -759,8 +758,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,
@@ -814,8 +813,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,
@@ -869,8 +868,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,
index b20a25ab6ed090cdee112830b5510659425d9a2f..3c96c56c70e1170cb5eaf0296d63daac8deaea7e 100644 (file)
@@ -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:
index 443590b9c82b70e66b699c30c7c9d755d6818de7..61ef006387781b81c55fb8222449435c851097fa 100644 (file)
@@ -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:
index f131001ce2c9f103f3e4df10ccd7518d62dc9e69..e2915268c00a39f976ec493a254301618a6c93a7 100644 (file)
@@ -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,31 @@ from typing import (
 
 import anyio
 from fastapi import params
+from fastapi._compat import (
+    PYDANTIC_V2,
+    ErrorWrapper,
+    ModelField,
+    Required,
+    Undefined,
+    _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 +55,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 +204,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 +322,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 +341,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 +374,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 +394,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 +416,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 +425,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 +516,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 +636,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 +650,19 @@ 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)
-        )
+        v_, errors_ = field.validate(value, values, loc=loc)
         if 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 +671,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[Dict[str, Any]]]:
     values = {}
-    errors = []
+    errors: List[Dict[str, Any]] = []
     if required_params:
         field = required_params[0]
         field_info = field.field_info
@@ -733,9 +691,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 +704,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 +715,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 +737,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 +767,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):
index 94f41bfa18cac86390b0824b3c84580124d4aea2..b542749f250a313f01fe3a0fcffd1897c9fec90c 100644 (file)
@@ -1,15 +1,87 @@
 import dataclasses
+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
+# TODO: pv2 should this return strings instead?
+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 +100,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 +122,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 +144,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):
index cac5330a229104ee2e43324bda00559cd75b1920..c1692f396127a4cb5ffa38568be70ad67192fd59 100644 (file)
@@ -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
index 1897ad750915ebc9043cbc2b9421d7ad8a3bcb0d..d724ee3cfdbcda1c39f39511046c7a884186ca98 100644 (file)
@@ -1,2 +1,3 @@
 METHODS_WITH_BODY = {"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"}
 REF_PREFIX = "#/components/schemas/"
+REF_TEMPLATE = "#/components/schemas/{model}"
index a2ea53607330100957e2f002e8dc9cd986e20c15..2268dd229091d10dd0535bd21515b40409b8ce1b 100644 (file)
@@ -1,13 +1,21 @@
 from enum import Enum
-from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Union
-
+from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Type, Union
+
+from fastapi._compat import (
+    PYDANTIC_V2,
+    CoreSchema,
+    GetJsonSchemaHandler,
+    JsonSchemaValue,
+    _model_rebuild,
+    general_plain_validator_function,
+)
 from fastapi.logger import logger
 from pydantic import AnyUrl, BaseModel, Field
 from typing_extensions import Annotated, Literal
 from typing_extensions import deprecated as typing_deprecated
 
 try:
-    import email_validator  # type: ignore
+    import email_validator
 
     assert email_validator  # make autoflake ignore the unused import
     from pydantic import EmailStr
@@ -26,14 +34,39 @@ except ImportError:  # pragma: no cover
             )
             return str(v)
 
+        @classmethod
+        def _validate(cls, __input_value: Any, _: Any) -> str:
+            logger.warning(
+                "email-validator not installed, email fields will be treated as str.\n"
+                "To install, run: pip install email-validator"
+            )
+            return str(__input_value)
+
+        @classmethod
+        def __get_pydantic_json_schema__(
+            cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
+        ) -> JsonSchemaValue:
+            return {"type": "string", "format": "email"}
+
+        @classmethod
+        def __get_pydantic_core_schema__(
+            cls, source: Type[Any], handler: Callable[[Any], CoreSchema]
+        ) -> CoreSchema:
+            return general_plain_validator_function(cls._validate)
+
 
 class Contact(BaseModel):
     name: Optional[str] = None
     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):
@@ -41,8 +74,13 @@ class License(BaseModel):
     identifier: Optional[str] = None
     url: Optional[AnyUrl] = None
 
-    class Config:
-        extra = "allow"
+    if PYDANTIC_V2:
+        model_config = {"extra": "allow"}
+
+    else:
+
+        class Config:
+            extra = "allow"
 
 
 class Info(BaseModel):
@@ -54,17 +92,27 @@ 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):
-    enum: Annotated[Optional[List[str]], Field(min_items=1)] = None
+    enum: Annotated[Optional[List[str]], Field(min_length=1)] = None
     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):
@@ -72,8 +120,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):
@@ -92,16 +145,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):
@@ -190,8 +253,13 @@ class Schema(BaseModel):
         ),
     ] = None
 
-    class Config:
-        extra: str = "allow"
+    if PYDANTIC_V2:
+        model_config = {"extra": "allow"}
+
+    else:
+
+        class Config:
+            extra = "allow"
 
 
 # Ref: https://json-schema.org/draft/2020-12/json-schema-core.html#name-json-schema-documents
@@ -205,8 +273,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):
@@ -223,8 +296,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):
@@ -233,8 +311,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):
@@ -251,8 +334,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):
@@ -269,8 +357,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):
@@ -281,8 +374,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):
@@ -291,8 +389,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):
@@ -310,8 +413,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):
@@ -329,8 +437,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):
@@ -344,8 +457,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):
@@ -374,8 +492,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):
@@ -401,8 +524,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):
@@ -433,8 +561,13 @@ class Components(BaseModel):
     callbacks: Optional[Dict[str, Union[Dict[str, PathItem], Reference, Any]]] = None
     pathItems: Optional[Dict[str, Union[PathItem, Reference]]] = None
 
-    class Config:
-        extra = "allow"
+    if PYDANTIC_V2:
+        model_config = {"extra": "allow"}
+
+    else:
+
+        class Config:
+            extra = "allow"
 
 
 class Tag(BaseModel):
@@ -442,8 +575,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):
@@ -459,10 +597,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)
index 609fe4389f2bb18c35dfa8c32a9c4aa985f37acd..e295361e6a9a1483722095ad5558c2d977200408 100644 (file)
@@ -1,35 +1,37 @@
 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,
+    JsonSchemaValue,
+    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
+from typing_extensions import Literal
 
 validation_error_definition = {
     "title": "ValidationError",
@@ -88,7 +90,11 @@ 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,
+    field_mapping: Dict[
+        Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
+    ],
 ) -> List[Dict[str, Any]]:
     parameters = []
     for param in all_route_params:
@@ -96,13 +102,17 @@ 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,
+            field_mapping=field_mapping,
+        )
         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
@@ -117,13 +127,20 @@ 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,
+    field_mapping: Dict[
+        Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
+    ],
 ) -> 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_mapping=field_mapping,
     )
     field_info = cast(Body, body_field.field_info)
     request_media_type = field_info.media_type
@@ -186,7 +203,14 @@ 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,
+    field_mapping: Dict[
+        Tuple[ModelField, Literal["validation", "serialization"]], JsonSchemaValue
+    ],
 ) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]:
     path = {}
     security_schemes: Dict[str, Any] = {}
@@ -214,7 +238,10 @@ 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,
+                field_mapping=field_mapping,
             )
             parameters.extend(operation_parameters)
             if parameters:
@@ -232,7 +259,10 @@ 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,
+                    field_mapping=field_mapping,
                 )
                 if request_body_oai:
                     operation["requestBody"] = request_body_oai
@@ -246,8 +276,10 @@ 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,
+                            field_mapping=field_mapping,
                         )
                         callbacks[callback.name] = {callback.path: cb_path}
                 operation["callbacks"] = callbacks
@@ -273,10 +305,11 @@ 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,
+                            field_mapping=field_mapping,
                         )
                     else:
                         response_schema = {}
@@ -305,8 +338,11 @@ 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,
+                            field_mapping=field_mapping,
                         )
                         media_type = route_response_media_type or "application/json"
                         additional_schema = (
@@ -352,13 +388,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
@@ -373,13 +409,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
 
@@ -417,15 +452,22 @@ def get_openapi(
     paths: Dict[str, Dict[str, Any]] = {}
     webhook_paths: Dict[str, Dict[str, Any]] = {}
     operation_ids: Set[str] = set()
-    flat_models = get_flat_models_from_routes(list(routes or []) + list(webhooks or []))
-    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(list(routes or []) + list(webhooks or []))
+    model_name_map = get_compat_model_name_map(all_fields)
+    schema_generator = GenerateJsonSchema(ref_template=REF_TEMPLATE)
+    field_mapping, definitions = get_definitions(
+        fields=all_fields,
+        schema_generator=schema_generator,
+        model_name_map=model_name_map,
     )
     for route in routes or []:
         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,
+                field_mapping=field_mapping,
             )
             if result:
                 path, security_schemes, path_definitions = result
@@ -441,8 +483,10 @@ def get_openapi(
         if isinstance(webhook, routing.APIRoute):
             result = get_openapi_path(
                 route=webhook,
-                model_name_map=model_name_map,
                 operation_ids=operation_ids,
+                schema_generator=schema_generator,
+                model_name_map=model_name_map,
+                field_mapping=field_mapping,
             )
             if result:
                 path, security_schemes, path_definitions = result
index 2f5818c85c3803baeb76fd9bec1eb74d8b42f47c..a43afaf311798ebde5fb265e1d47d584d807152d 100644 (file)
@@ -1,14 +1,22 @@
-from typing import Any, Callable, List, Optional, Sequence
+from typing import Any, Callable, Dict, List, Optional, Sequence, Union
 
 from fastapi import params
-from pydantic.fields import Undefined
+from fastapi._compat import Undefined
 from typing_extensions import Annotated, deprecated
 
+_Unset: Any = Undefined
+
 
 def Path(  # noqa: N802
     default: Any = ...,
     *,
+    default_factory: Union[Callable[[], Any], None] = _Unset,
     alias: Optional[str] = None,
+    alias_priority: Union[int, None] = _Unset,
+    # TODO: update when deprecating Pydantic v1, import these types
+    # validation_alias: str | AliasPath | AliasChoices | None
+    validation_alias: Union[str, None] = None,
+    serialization_alias: Union[str, None] = None,
     title: Optional[str] = None,
     description: Optional[str] = None,
     gt: Optional[float] = None,
@@ -17,7 +25,19 @@ def Path(  # noqa: N802
     le: Optional[float] = None,
     min_length: Optional[int] = None,
     max_length: Optional[int] = None,
-    regex: Optional[str] = None,
+    pattern: Optional[str] = None,
+    regex: Annotated[
+        Optional[str],
+        deprecated(
+            "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+        ),
+    ] = None,
+    discriminator: Union[str, None] = None,
+    strict: Union[bool, None] = _Unset,
+    multiple_of: Union[float, None] = _Unset,
+    allow_inf_nan: Union[bool, None] = _Unset,
+    max_digits: Union[int, None] = _Unset,
+    decimal_places: Union[int, None] = _Unset,
     examples: Optional[List[Any]] = None,
     example: Annotated[
         Optional[Any],
@@ -25,14 +45,19 @@ def Path(  # noqa: N802
             "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
             "although still supported. Use examples instead."
         ),
-    ] = Undefined,
+    ] = _Unset,
     deprecated: Optional[bool] = None,
     include_in_schema: bool = True,
+    json_schema_extra: Union[Dict[str, Any], None] = None,
     **extra: Any,
 ) -> Any:
     return params.Path(
         default=default,
+        default_factory=default_factory,
         alias=alias,
+        alias_priority=alias_priority,
+        validation_alias=validation_alias,
+        serialization_alias=serialization_alias,
         title=title,
         description=description,
         gt=gt,
@@ -41,11 +66,19 @@ def Path(  # noqa: N802
         le=le,
         min_length=min_length,
         max_length=max_length,
+        pattern=pattern,
         regex=regex,
+        discriminator=discriminator,
+        strict=strict,
+        multiple_of=multiple_of,
+        allow_inf_nan=allow_inf_nan,
+        max_digits=max_digits,
+        decimal_places=decimal_places,
         example=example,
         examples=examples,
         deprecated=deprecated,
         include_in_schema=include_in_schema,
+        json_schema_extra=json_schema_extra,
         **extra,
     )
 
@@ -53,7 +86,13 @@ def Path(  # noqa: N802
 def Query(  # noqa: N802
     default: Any = Undefined,
     *,
+    default_factory: Union[Callable[[], Any], None] = _Unset,
     alias: Optional[str] = None,
+    alias_priority: Union[int, None] = _Unset,
+    # TODO: update when deprecating Pydantic v1, import these types
+    # validation_alias: str | AliasPath | AliasChoices | None
+    validation_alias: Union[str, None] = None,
+    serialization_alias: Union[str, None] = None,
     title: Optional[str] = None,
     description: Optional[str] = None,
     gt: Optional[float] = None,
@@ -62,7 +101,19 @@ def Query(  # noqa: N802
     le: Optional[float] = None,
     min_length: Optional[int] = None,
     max_length: Optional[int] = None,
-    regex: Optional[str] = None,
+    pattern: Optional[str] = None,
+    regex: Annotated[
+        Optional[str],
+        deprecated(
+            "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+        ),
+    ] = None,
+    discriminator: Union[str, None] = None,
+    strict: Union[bool, None] = _Unset,
+    multiple_of: Union[float, None] = _Unset,
+    allow_inf_nan: Union[bool, None] = _Unset,
+    max_digits: Union[int, None] = _Unset,
+    decimal_places: Union[int, None] = _Unset,
     examples: Optional[List[Any]] = None,
     example: Annotated[
         Optional[Any],
@@ -70,14 +121,19 @@ def Query(  # noqa: N802
             "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
             "although still supported. Use examples instead."
         ),
-    ] = Undefined,
+    ] = _Unset,
     deprecated: Optional[bool] = None,
     include_in_schema: bool = True,
+    json_schema_extra: Union[Dict[str, Any], None] = None,
     **extra: Any,
 ) -> Any:
     return params.Query(
         default=default,
+        default_factory=default_factory,
         alias=alias,
+        alias_priority=alias_priority,
+        validation_alias=validation_alias,
+        serialization_alias=serialization_alias,
         title=title,
         description=description,
         gt=gt,
@@ -86,11 +142,19 @@ def Query(  # noqa: N802
         le=le,
         min_length=min_length,
         max_length=max_length,
+        pattern=pattern,
         regex=regex,
+        discriminator=discriminator,
+        strict=strict,
+        multiple_of=multiple_of,
+        allow_inf_nan=allow_inf_nan,
+        max_digits=max_digits,
+        decimal_places=decimal_places,
         example=example,
         examples=examples,
         deprecated=deprecated,
         include_in_schema=include_in_schema,
+        json_schema_extra=json_schema_extra,
         **extra,
     )
 
@@ -98,7 +162,13 @@ def Query(  # noqa: N802
 def Header(  # noqa: N802
     default: Any = Undefined,
     *,
+    default_factory: Union[Callable[[], Any], None] = _Unset,
     alias: Optional[str] = None,
+    alias_priority: Union[int, None] = _Unset,
+    # TODO: update when deprecating Pydantic v1, import these types
+    # validation_alias: str | AliasPath | AliasChoices | None
+    validation_alias: Union[str, None] = None,
+    serialization_alias: Union[str, None] = None,
     convert_underscores: bool = True,
     title: Optional[str] = None,
     description: Optional[str] = None,
@@ -108,7 +178,19 @@ def Header(  # noqa: N802
     le: Optional[float] = None,
     min_length: Optional[int] = None,
     max_length: Optional[int] = None,
-    regex: Optional[str] = None,
+    pattern: Optional[str] = None,
+    regex: Annotated[
+        Optional[str],
+        deprecated(
+            "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+        ),
+    ] = None,
+    discriminator: Union[str, None] = None,
+    strict: Union[bool, None] = _Unset,
+    multiple_of: Union[float, None] = _Unset,
+    allow_inf_nan: Union[bool, None] = _Unset,
+    max_digits: Union[int, None] = _Unset,
+    decimal_places: Union[int, None] = _Unset,
     examples: Optional[List[Any]] = None,
     example: Annotated[
         Optional[Any],
@@ -116,14 +198,19 @@ def Header(  # noqa: N802
             "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
             "although still supported. Use examples instead."
         ),
-    ] = Undefined,
+    ] = _Unset,
     deprecated: Optional[bool] = None,
     include_in_schema: bool = True,
+    json_schema_extra: Union[Dict[str, Any], None] = None,
     **extra: Any,
 ) -> Any:
     return params.Header(
         default=default,
+        default_factory=default_factory,
         alias=alias,
+        alias_priority=alias_priority,
+        validation_alias=validation_alias,
+        serialization_alias=serialization_alias,
         convert_underscores=convert_underscores,
         title=title,
         description=description,
@@ -133,11 +220,19 @@ def Header(  # noqa: N802
         le=le,
         min_length=min_length,
         max_length=max_length,
+        pattern=pattern,
         regex=regex,
+        discriminator=discriminator,
+        strict=strict,
+        multiple_of=multiple_of,
+        allow_inf_nan=allow_inf_nan,
+        max_digits=max_digits,
+        decimal_places=decimal_places,
         example=example,
         examples=examples,
         deprecated=deprecated,
         include_in_schema=include_in_schema,
+        json_schema_extra=json_schema_extra,
         **extra,
     )
 
@@ -145,7 +240,13 @@ def Header(  # noqa: N802
 def Cookie(  # noqa: N802
     default: Any = Undefined,
     *,
+    default_factory: Union[Callable[[], Any], None] = _Unset,
     alias: Optional[str] = None,
+    alias_priority: Union[int, None] = _Unset,
+    # TODO: update when deprecating Pydantic v1, import these types
+    # validation_alias: str | AliasPath | AliasChoices | None
+    validation_alias: Union[str, None] = None,
+    serialization_alias: Union[str, None] = None,
     title: Optional[str] = None,
     description: Optional[str] = None,
     gt: Optional[float] = None,
@@ -154,7 +255,19 @@ def Cookie(  # noqa: N802
     le: Optional[float] = None,
     min_length: Optional[int] = None,
     max_length: Optional[int] = None,
-    regex: Optional[str] = None,
+    pattern: Optional[str] = None,
+    regex: Annotated[
+        Optional[str],
+        deprecated(
+            "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+        ),
+    ] = None,
+    discriminator: Union[str, None] = None,
+    strict: Union[bool, None] = _Unset,
+    multiple_of: Union[float, None] = _Unset,
+    allow_inf_nan: Union[bool, None] = _Unset,
+    max_digits: Union[int, None] = _Unset,
+    decimal_places: Union[int, None] = _Unset,
     examples: Optional[List[Any]] = None,
     example: Annotated[
         Optional[Any],
@@ -162,14 +275,19 @@ def Cookie(  # noqa: N802
             "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
             "although still supported. Use examples instead."
         ),
-    ] = Undefined,
+    ] = _Unset,
     deprecated: Optional[bool] = None,
     include_in_schema: bool = True,
+    json_schema_extra: Union[Dict[str, Any], None] = None,
     **extra: Any,
 ) -> Any:
     return params.Cookie(
         default=default,
+        default_factory=default_factory,
         alias=alias,
+        alias_priority=alias_priority,
+        validation_alias=validation_alias,
+        serialization_alias=serialization_alias,
         title=title,
         description=description,
         gt=gt,
@@ -178,11 +296,19 @@ def Cookie(  # noqa: N802
         le=le,
         min_length=min_length,
         max_length=max_length,
+        pattern=pattern,
         regex=regex,
+        discriminator=discriminator,
+        strict=strict,
+        multiple_of=multiple_of,
+        allow_inf_nan=allow_inf_nan,
+        max_digits=max_digits,
+        decimal_places=decimal_places,
         example=example,
         examples=examples,
         deprecated=deprecated,
         include_in_schema=include_in_schema,
+        json_schema_extra=json_schema_extra,
         **extra,
     )
 
@@ -190,9 +316,15 @@ def Cookie(  # noqa: N802
 def Body(  # noqa: N802
     default: Any = Undefined,
     *,
+    default_factory: Union[Callable[[], Any], None] = _Unset,
     embed: bool = False,
     media_type: str = "application/json",
     alias: Optional[str] = None,
+    alias_priority: Union[int, None] = _Unset,
+    # TODO: update when deprecating Pydantic v1, import these types
+    # validation_alias: str | AliasPath | AliasChoices | None
+    validation_alias: Union[str, None] = None,
+    serialization_alias: Union[str, None] = None,
     title: Optional[str] = None,
     description: Optional[str] = None,
     gt: Optional[float] = None,
@@ -201,7 +333,19 @@ def Body(  # noqa: N802
     le: Optional[float] = None,
     min_length: Optional[int] = None,
     max_length: Optional[int] = None,
-    regex: Optional[str] = None,
+    pattern: Optional[str] = None,
+    regex: Annotated[
+        Optional[str],
+        deprecated(
+            "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+        ),
+    ] = None,
+    discriminator: Union[str, None] = None,
+    strict: Union[bool, None] = _Unset,
+    multiple_of: Union[float, None] = _Unset,
+    allow_inf_nan: Union[bool, None] = _Unset,
+    max_digits: Union[int, None] = _Unset,
+    decimal_places: Union[int, None] = _Unset,
     examples: Optional[List[Any]] = None,
     example: Annotated[
         Optional[Any],
@@ -209,14 +353,21 @@ def Body(  # noqa: N802
             "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
             "although still supported. Use examples instead."
         ),
-    ] = Undefined,
+    ] = _Unset,
+    deprecated: Optional[bool] = None,
+    include_in_schema: bool = True,
+    json_schema_extra: Union[Dict[str, Any], None] = None,
     **extra: Any,
 ) -> Any:
     return params.Body(
         default=default,
+        default_factory=default_factory,
         embed=embed,
         media_type=media_type,
         alias=alias,
+        alias_priority=alias_priority,
+        validation_alias=validation_alias,
+        serialization_alias=serialization_alias,
         title=title,
         description=description,
         gt=gt,
@@ -225,9 +376,19 @@ def Body(  # noqa: N802
         le=le,
         min_length=min_length,
         max_length=max_length,
+        pattern=pattern,
         regex=regex,
+        discriminator=discriminator,
+        strict=strict,
+        multiple_of=multiple_of,
+        allow_inf_nan=allow_inf_nan,
+        max_digits=max_digits,
+        decimal_places=decimal_places,
         example=example,
         examples=examples,
+        deprecated=deprecated,
+        include_in_schema=include_in_schema,
+        json_schema_extra=json_schema_extra,
         **extra,
     )
 
@@ -235,8 +396,14 @@ def Body(  # noqa: N802
 def Form(  # noqa: N802
     default: Any = Undefined,
     *,
+    default_factory: Union[Callable[[], Any], None] = _Unset,
     media_type: str = "application/x-www-form-urlencoded",
     alias: Optional[str] = None,
+    alias_priority: Union[int, None] = _Unset,
+    # TODO: update when deprecating Pydantic v1, import these types
+    # validation_alias: str | AliasPath | AliasChoices | None
+    validation_alias: Union[str, None] = None,
+    serialization_alias: Union[str, None] = None,
     title: Optional[str] = None,
     description: Optional[str] = None,
     gt: Optional[float] = None,
@@ -245,7 +412,19 @@ def Form(  # noqa: N802
     le: Optional[float] = None,
     min_length: Optional[int] = None,
     max_length: Optional[int] = None,
-    regex: Optional[str] = None,
+    pattern: Optional[str] = None,
+    regex: Annotated[
+        Optional[str],
+        deprecated(
+            "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+        ),
+    ] = None,
+    discriminator: Union[str, None] = None,
+    strict: Union[bool, None] = _Unset,
+    multiple_of: Union[float, None] = _Unset,
+    allow_inf_nan: Union[bool, None] = _Unset,
+    max_digits: Union[int, None] = _Unset,
+    decimal_places: Union[int, None] = _Unset,
     examples: Optional[List[Any]] = None,
     example: Annotated[
         Optional[Any],
@@ -253,13 +432,20 @@ def Form(  # noqa: N802
             "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
             "although still supported. Use examples instead."
         ),
-    ] = Undefined,
+    ] = _Unset,
+    deprecated: Optional[bool] = None,
+    include_in_schema: bool = True,
+    json_schema_extra: Union[Dict[str, Any], None] = None,
     **extra: Any,
 ) -> Any:
     return params.Form(
         default=default,
+        default_factory=default_factory,
         media_type=media_type,
         alias=alias,
+        alias_priority=alias_priority,
+        validation_alias=validation_alias,
+        serialization_alias=serialization_alias,
         title=title,
         description=description,
         gt=gt,
@@ -268,9 +454,19 @@ def Form(  # noqa: N802
         le=le,
         min_length=min_length,
         max_length=max_length,
+        pattern=pattern,
         regex=regex,
+        discriminator=discriminator,
+        strict=strict,
+        multiple_of=multiple_of,
+        allow_inf_nan=allow_inf_nan,
+        max_digits=max_digits,
+        decimal_places=decimal_places,
         example=example,
         examples=examples,
+        deprecated=deprecated,
+        include_in_schema=include_in_schema,
+        json_schema_extra=json_schema_extra,
         **extra,
     )
 
@@ -278,8 +474,14 @@ def Form(  # noqa: N802
 def File(  # noqa: N802
     default: Any = Undefined,
     *,
+    default_factory: Union[Callable[[], Any], None] = _Unset,
     media_type: str = "multipart/form-data",
     alias: Optional[str] = None,
+    alias_priority: Union[int, None] = _Unset,
+    # TODO: update when deprecating Pydantic v1, import these types
+    # validation_alias: str | AliasPath | AliasChoices | None
+    validation_alias: Union[str, None] = None,
+    serialization_alias: Union[str, None] = None,
     title: Optional[str] = None,
     description: Optional[str] = None,
     gt: Optional[float] = None,
@@ -288,7 +490,19 @@ def File(  # noqa: N802
     le: Optional[float] = None,
     min_length: Optional[int] = None,
     max_length: Optional[int] = None,
-    regex: Optional[str] = None,
+    pattern: Optional[str] = None,
+    regex: Annotated[
+        Optional[str],
+        deprecated(
+            "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+        ),
+    ] = None,
+    discriminator: Union[str, None] = None,
+    strict: Union[bool, None] = _Unset,
+    multiple_of: Union[float, None] = _Unset,
+    allow_inf_nan: Union[bool, None] = _Unset,
+    max_digits: Union[int, None] = _Unset,
+    decimal_places: Union[int, None] = _Unset,
     examples: Optional[List[Any]] = None,
     example: Annotated[
         Optional[Any],
@@ -296,13 +510,20 @@ def File(  # noqa: N802
             "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
             "although still supported. Use examples instead."
         ),
-    ] = Undefined,
+    ] = _Unset,
+    deprecated: Optional[bool] = None,
+    include_in_schema: bool = True,
+    json_schema_extra: Union[Dict[str, Any], None] = None,
     **extra: Any,
 ) -> Any:
     return params.File(
         default=default,
+        default_factory=default_factory,
         media_type=media_type,
         alias=alias,
+        alias_priority=alias_priority,
+        validation_alias=validation_alias,
+        serialization_alias=serialization_alias,
         title=title,
         description=description,
         gt=gt,
@@ -311,9 +532,19 @@ def File(  # noqa: N802
         le=le,
         min_length=min_length,
         max_length=max_length,
+        pattern=pattern,
         regex=regex,
+        discriminator=discriminator,
+        strict=strict,
+        multiple_of=multiple_of,
+        allow_inf_nan=allow_inf_nan,
+        max_digits=max_digits,
+        decimal_places=decimal_places,
         example=example,
         examples=examples,
+        deprecated=deprecated,
+        include_in_schema=include_in_schema,
+        json_schema_extra=json_schema_extra,
         **extra,
     )
 
index 4069f2cda67e2a1d1c9dacb4895aac803f83966c..30af5713e73e47ae4949c369a1fa35581028e556 100644 (file)
@@ -1,10 +1,14 @@
 import warnings
 from enum import Enum
-from typing import Any, Callable, List, Optional, Sequence
+from typing import Any, Callable, Dict, List, Optional, Sequence, Union
 
-from pydantic.fields import FieldInfo, Undefined
+from pydantic.fields import FieldInfo
 from typing_extensions import Annotated, deprecated
 
+from ._compat import PYDANTIC_V2, Undefined
+
+_Unset: Any = Undefined
+
 
 class ParamTypes(Enum):
     query = "query"
@@ -20,7 +24,14 @@ class Param(FieldInfo):
         self,
         default: Any = Undefined,
         *,
+        default_factory: Union[Callable[[], Any], None] = _Unset,
+        annotation: Optional[Any] = None,
         alias: Optional[str] = None,
+        alias_priority: Union[int, None] = _Unset,
+        # TODO: update when deprecating Pydantic v1, import these types
+        # validation_alias: str | AliasPath | AliasChoices | None
+        validation_alias: Union[str, None] = None,
+        serialization_alias: Union[str, None] = None,
         title: Optional[str] = None,
         description: Optional[str] = None,
         gt: Optional[float] = None,
@@ -29,7 +40,19 @@ class Param(FieldInfo):
         le: Optional[float] = None,
         min_length: Optional[int] = None,
         max_length: Optional[int] = None,
-        regex: Optional[str] = None,
+        pattern: Optional[str] = None,
+        regex: Annotated[
+            Optional[str],
+            deprecated(
+                "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+            ),
+        ] = None,
+        discriminator: Union[str, None] = None,
+        strict: Union[bool, None] = _Unset,
+        multiple_of: Union[float, None] = _Unset,
+        allow_inf_nan: Union[bool, None] = _Unset,
+        max_digits: Union[int, None] = _Unset,
+        decimal_places: Union[int, None] = _Unset,
         examples: Optional[List[Any]] = None,
         example: Annotated[
             Optional[Any],
@@ -37,25 +60,24 @@ class Param(FieldInfo):
                 "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
                 "although still supported. Use examples instead."
             ),
-        ] = Undefined,
+        ] = _Unset,
         deprecated: Optional[bool] = None,
         include_in_schema: bool = True,
+        json_schema_extra: Union[Dict[str, Any], None] = None,
         **extra: Any,
     ):
         self.deprecated = deprecated
-        if example is not Undefined:
+        if example is not _Unset:
             warnings.warn(
                 "`example` has been depreacated, please use `examples` instead",
                 category=DeprecationWarning,
-                stacklevel=1,
+                stacklevel=4,
             )
         self.example = example
         self.include_in_schema = include_in_schema
-        extra_kwargs = {**extra}
-        if examples:
-            extra_kwargs["examples"] = examples
-        super().__init__(
+        kwargs = dict(
             default=default,
+            default_factory=default_factory,
             alias=alias,
             title=title,
             description=description,
@@ -65,9 +87,40 @@ class Param(FieldInfo):
             le=le,
             min_length=min_length,
             max_length=max_length,
-            regex=regex,
-            **extra_kwargs,
+            discriminator=discriminator,
+            multiple_of=multiple_of,
+            allow_nan=allow_inf_nan,
+            max_digits=max_digits,
+            decimal_places=decimal_places,
+            **extra,
         )
+        if examples is not None:
+            kwargs["examples"] = examples
+        if regex is not None:
+            warnings.warn(
+                "`regex` has been depreacated, please use `pattern` instead",
+                category=DeprecationWarning,
+                stacklevel=4,
+            )
+        current_json_schema_extra = json_schema_extra or extra
+        if PYDANTIC_V2:
+            kwargs.update(
+                {
+                    "annotation": annotation,
+                    "alias_priority": alias_priority,
+                    "validation_alias": validation_alias,
+                    "serialization_alias": serialization_alias,
+                    "strict": strict,
+                    "json_schema_extra": current_json_schema_extra,
+                }
+            )
+            kwargs["pattern"] = pattern or regex
+        else:
+            kwargs["regex"] = pattern or regex
+            kwargs.update(**current_json_schema_extra)
+        use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset}
+
+        super().__init__(**use_kwargs)
 
     def __repr__(self) -> str:
         return f"{self.__class__.__name__}({self.default})"
@@ -80,7 +133,14 @@ class Path(Param):
         self,
         default: Any = ...,
         *,
+        default_factory: Union[Callable[[], Any], None] = _Unset,
+        annotation: Optional[Any] = None,
         alias: Optional[str] = None,
+        alias_priority: Union[int, None] = _Unset,
+        # TODO: update when deprecating Pydantic v1, import these types
+        # validation_alias: str | AliasPath | AliasChoices | None
+        validation_alias: Union[str, None] = None,
+        serialization_alias: Union[str, None] = None,
         title: Optional[str] = None,
         description: Optional[str] = None,
         gt: Optional[float] = None,
@@ -89,7 +149,19 @@ class Path(Param):
         le: Optional[float] = None,
         min_length: Optional[int] = None,
         max_length: Optional[int] = None,
-        regex: Optional[str] = None,
+        pattern: Optional[str] = None,
+        regex: Annotated[
+            Optional[str],
+            deprecated(
+                "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+            ),
+        ] = None,
+        discriminator: Union[str, None] = None,
+        strict: Union[bool, None] = _Unset,
+        multiple_of: Union[float, None] = _Unset,
+        allow_inf_nan: Union[bool, None] = _Unset,
+        max_digits: Union[int, None] = _Unset,
+        decimal_places: Union[int, None] = _Unset,
         examples: Optional[List[Any]] = None,
         example: Annotated[
             Optional[Any],
@@ -97,16 +169,22 @@ class Path(Param):
                 "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
                 "although still supported. Use examples instead."
             ),
-        ] = Undefined,
+        ] = _Unset,
         deprecated: Optional[bool] = None,
         include_in_schema: bool = True,
+        json_schema_extra: Union[Dict[str, Any], None] = None,
         **extra: Any,
     ):
         assert default is ..., "Path parameters cannot have a default value"
         self.in_ = self.in_
         super().__init__(
             default=default,
+            default_factory=default_factory,
+            annotation=annotation,
             alias=alias,
+            alias_priority=alias_priority,
+            validation_alias=validation_alias,
+            serialization_alias=serialization_alias,
             title=title,
             description=description,
             gt=gt,
@@ -115,11 +193,19 @@ class Path(Param):
             le=le,
             min_length=min_length,
             max_length=max_length,
+            pattern=pattern,
             regex=regex,
+            discriminator=discriminator,
+            strict=strict,
+            multiple_of=multiple_of,
+            allow_inf_nan=allow_inf_nan,
+            max_digits=max_digits,
+            decimal_places=decimal_places,
             deprecated=deprecated,
             example=example,
             examples=examples,
             include_in_schema=include_in_schema,
+            json_schema_extra=json_schema_extra,
             **extra,
         )
 
@@ -131,7 +217,14 @@ class Query(Param):
         self,
         default: Any = Undefined,
         *,
+        default_factory: Union[Callable[[], Any], None] = _Unset,
+        annotation: Optional[Any] = None,
         alias: Optional[str] = None,
+        alias_priority: Union[int, None] = _Unset,
+        # TODO: update when deprecating Pydantic v1, import these types
+        # validation_alias: str | AliasPath | AliasChoices | None
+        validation_alias: Union[str, None] = None,
+        serialization_alias: Union[str, None] = None,
         title: Optional[str] = None,
         description: Optional[str] = None,
         gt: Optional[float] = None,
@@ -140,7 +233,19 @@ class Query(Param):
         le: Optional[float] = None,
         min_length: Optional[int] = None,
         max_length: Optional[int] = None,
-        regex: Optional[str] = None,
+        pattern: Optional[str] = None,
+        regex: Annotated[
+            Optional[str],
+            deprecated(
+                "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+            ),
+        ] = None,
+        discriminator: Union[str, None] = None,
+        strict: Union[bool, None] = _Unset,
+        multiple_of: Union[float, None] = _Unset,
+        allow_inf_nan: Union[bool, None] = _Unset,
+        max_digits: Union[int, None] = _Unset,
+        decimal_places: Union[int, None] = _Unset,
         examples: Optional[List[Any]] = None,
         example: Annotated[
             Optional[Any],
@@ -148,14 +253,20 @@ class Query(Param):
                 "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
                 "although still supported. Use examples instead."
             ),
-        ] = Undefined,
+        ] = _Unset,
         deprecated: Optional[bool] = None,
         include_in_schema: bool = True,
+        json_schema_extra: Union[Dict[str, Any], None] = None,
         **extra: Any,
     ):
         super().__init__(
             default=default,
+            default_factory=default_factory,
+            annotation=annotation,
             alias=alias,
+            alias_priority=alias_priority,
+            validation_alias=validation_alias,
+            serialization_alias=serialization_alias,
             title=title,
             description=description,
             gt=gt,
@@ -164,11 +275,19 @@ class Query(Param):
             le=le,
             min_length=min_length,
             max_length=max_length,
+            pattern=pattern,
             regex=regex,
+            discriminator=discriminator,
+            strict=strict,
+            multiple_of=multiple_of,
+            allow_inf_nan=allow_inf_nan,
+            max_digits=max_digits,
+            decimal_places=decimal_places,
             deprecated=deprecated,
             example=example,
             examples=examples,
             include_in_schema=include_in_schema,
+            json_schema_extra=json_schema_extra,
             **extra,
         )
 
@@ -180,7 +299,14 @@ class Header(Param):
         self,
         default: Any = Undefined,
         *,
+        default_factory: Union[Callable[[], Any], None] = _Unset,
+        annotation: Optional[Any] = None,
         alias: Optional[str] = None,
+        alias_priority: Union[int, None] = _Unset,
+        # TODO: update when deprecating Pydantic v1, import these types
+        # validation_alias: str | AliasPath | AliasChoices | None
+        validation_alias: Union[str, None] = None,
+        serialization_alias: Union[str, None] = None,
         convert_underscores: bool = True,
         title: Optional[str] = None,
         description: Optional[str] = None,
@@ -190,7 +316,19 @@ class Header(Param):
         le: Optional[float] = None,
         min_length: Optional[int] = None,
         max_length: Optional[int] = None,
-        regex: Optional[str] = None,
+        pattern: Optional[str] = None,
+        regex: Annotated[
+            Optional[str],
+            deprecated(
+                "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+            ),
+        ] = None,
+        discriminator: Union[str, None] = None,
+        strict: Union[bool, None] = _Unset,
+        multiple_of: Union[float, None] = _Unset,
+        allow_inf_nan: Union[bool, None] = _Unset,
+        max_digits: Union[int, None] = _Unset,
+        decimal_places: Union[int, None] = _Unset,
         examples: Optional[List[Any]] = None,
         example: Annotated[
             Optional[Any],
@@ -198,15 +336,21 @@ class Header(Param):
                 "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
                 "although still supported. Use examples instead."
             ),
-        ] = Undefined,
+        ] = _Unset,
         deprecated: Optional[bool] = None,
         include_in_schema: bool = True,
+        json_schema_extra: Union[Dict[str, Any], None] = None,
         **extra: Any,
     ):
         self.convert_underscores = convert_underscores
         super().__init__(
             default=default,
+            default_factory=default_factory,
+            annotation=annotation,
             alias=alias,
+            alias_priority=alias_priority,
+            validation_alias=validation_alias,
+            serialization_alias=serialization_alias,
             title=title,
             description=description,
             gt=gt,
@@ -215,11 +359,19 @@ class Header(Param):
             le=le,
             min_length=min_length,
             max_length=max_length,
+            pattern=pattern,
             regex=regex,
+            discriminator=discriminator,
+            strict=strict,
+            multiple_of=multiple_of,
+            allow_inf_nan=allow_inf_nan,
+            max_digits=max_digits,
+            decimal_places=decimal_places,
             deprecated=deprecated,
             example=example,
             examples=examples,
             include_in_schema=include_in_schema,
+            json_schema_extra=json_schema_extra,
             **extra,
         )
 
@@ -231,7 +383,14 @@ class Cookie(Param):
         self,
         default: Any = Undefined,
         *,
+        default_factory: Union[Callable[[], Any], None] = _Unset,
+        annotation: Optional[Any] = None,
         alias: Optional[str] = None,
+        alias_priority: Union[int, None] = _Unset,
+        # TODO: update when deprecating Pydantic v1, import these types
+        # validation_alias: str | AliasPath | AliasChoices | None
+        validation_alias: Union[str, None] = None,
+        serialization_alias: Union[str, None] = None,
         title: Optional[str] = None,
         description: Optional[str] = None,
         gt: Optional[float] = None,
@@ -240,7 +399,19 @@ class Cookie(Param):
         le: Optional[float] = None,
         min_length: Optional[int] = None,
         max_length: Optional[int] = None,
-        regex: Optional[str] = None,
+        pattern: Optional[str] = None,
+        regex: Annotated[
+            Optional[str],
+            deprecated(
+                "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+            ),
+        ] = None,
+        discriminator: Union[str, None] = None,
+        strict: Union[bool, None] = _Unset,
+        multiple_of: Union[float, None] = _Unset,
+        allow_inf_nan: Union[bool, None] = _Unset,
+        max_digits: Union[int, None] = _Unset,
+        decimal_places: Union[int, None] = _Unset,
         examples: Optional[List[Any]] = None,
         example: Annotated[
             Optional[Any],
@@ -248,14 +419,20 @@ class Cookie(Param):
                 "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
                 "although still supported. Use examples instead."
             ),
-        ] = Undefined,
+        ] = _Unset,
         deprecated: Optional[bool] = None,
         include_in_schema: bool = True,
+        json_schema_extra: Union[Dict[str, Any], None] = None,
         **extra: Any,
     ):
         super().__init__(
             default=default,
+            default_factory=default_factory,
+            annotation=annotation,
             alias=alias,
+            alias_priority=alias_priority,
+            validation_alias=validation_alias,
+            serialization_alias=serialization_alias,
             title=title,
             description=description,
             gt=gt,
@@ -264,11 +441,19 @@ class Cookie(Param):
             le=le,
             min_length=min_length,
             max_length=max_length,
+            pattern=pattern,
             regex=regex,
+            discriminator=discriminator,
+            strict=strict,
+            multiple_of=multiple_of,
+            allow_inf_nan=allow_inf_nan,
+            max_digits=max_digits,
+            decimal_places=decimal_places,
             deprecated=deprecated,
             example=example,
             examples=examples,
             include_in_schema=include_in_schema,
+            json_schema_extra=json_schema_extra,
             **extra,
         )
 
@@ -278,9 +463,16 @@ class Body(FieldInfo):
         self,
         default: Any = Undefined,
         *,
+        default_factory: Union[Callable[[], Any], None] = _Unset,
+        annotation: Optional[Any] = None,
         embed: bool = False,
         media_type: str = "application/json",
         alias: Optional[str] = None,
+        alias_priority: Union[int, None] = _Unset,
+        # TODO: update when deprecating Pydantic v1, import these types
+        # validation_alias: str | AliasPath | AliasChoices | None
+        validation_alias: Union[str, None] = None,
+        serialization_alias: Union[str, None] = None,
         title: Optional[str] = None,
         description: Optional[str] = None,
         gt: Optional[float] = None,
@@ -289,7 +481,19 @@ class Body(FieldInfo):
         le: Optional[float] = None,
         min_length: Optional[int] = None,
         max_length: Optional[int] = None,
-        regex: Optional[str] = None,
+        pattern: Optional[str] = None,
+        regex: Annotated[
+            Optional[str],
+            deprecated(
+                "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+            ),
+        ] = None,
+        discriminator: Union[str, None] = None,
+        strict: Union[bool, None] = _Unset,
+        multiple_of: Union[float, None] = _Unset,
+        allow_inf_nan: Union[bool, None] = _Unset,
+        max_digits: Union[int, None] = _Unset,
+        decimal_places: Union[int, None] = _Unset,
         examples: Optional[List[Any]] = None,
         example: Annotated[
             Optional[Any],
@@ -297,23 +501,26 @@ class Body(FieldInfo):
                 "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
                 "although still supported. Use examples instead."
             ),
-        ] = Undefined,
+        ] = _Unset,
+        deprecated: Optional[bool] = None,
+        include_in_schema: bool = True,
+        json_schema_extra: Union[Dict[str, Any], None] = None,
         **extra: Any,
     ):
         self.embed = embed
         self.media_type = media_type
-        if example is not Undefined:
+        self.deprecated = deprecated
+        if example is not _Unset:
             warnings.warn(
                 "`example` has been depreacated, please use `examples` instead",
                 category=DeprecationWarning,
-                stacklevel=1,
+                stacklevel=4,
             )
         self.example = example
-        extra_kwargs = {**extra}
-        if examples is not None:
-            extra_kwargs["examples"] = examples
-        super().__init__(
+        self.include_in_schema = include_in_schema
+        kwargs = dict(
             default=default,
+            default_factory=default_factory,
             alias=alias,
             title=title,
             description=description,
@@ -323,9 +530,41 @@ class Body(FieldInfo):
             le=le,
             min_length=min_length,
             max_length=max_length,
-            regex=regex,
-            **extra_kwargs,
+            discriminator=discriminator,
+            multiple_of=multiple_of,
+            allow_nan=allow_inf_nan,
+            max_digits=max_digits,
+            decimal_places=decimal_places,
+            **extra,
         )
+        if examples is not None:
+            kwargs["examples"] = examples
+        if regex is not None:
+            warnings.warn(
+                "`regex` has been depreacated, please use `pattern` instead",
+                category=DeprecationWarning,
+                stacklevel=4,
+            )
+        current_json_schema_extra = json_schema_extra or extra
+        if PYDANTIC_V2:
+            kwargs.update(
+                {
+                    "annotation": annotation,
+                    "alias_priority": alias_priority,
+                    "validation_alias": validation_alias,
+                    "serialization_alias": serialization_alias,
+                    "strict": strict,
+                    "json_schema_extra": current_json_schema_extra,
+                }
+            )
+            kwargs["pattern"] = pattern or regex
+        else:
+            kwargs["regex"] = pattern or regex
+            kwargs.update(**current_json_schema_extra)
+
+        use_kwargs = {k: v for k, v in kwargs.items() if v is not _Unset}
+
+        super().__init__(**use_kwargs)
 
     def __repr__(self) -> str:
         return f"{self.__class__.__name__}({self.default})"
@@ -336,8 +575,15 @@ class Form(Body):
         self,
         default: Any = Undefined,
         *,
+        default_factory: Union[Callable[[], Any], None] = _Unset,
+        annotation: Optional[Any] = None,
         media_type: str = "application/x-www-form-urlencoded",
         alias: Optional[str] = None,
+        alias_priority: Union[int, None] = _Unset,
+        # TODO: update when deprecating Pydantic v1, import these types
+        # validation_alias: str | AliasPath | AliasChoices | None
+        validation_alias: Union[str, None] = None,
+        serialization_alias: Union[str, None] = None,
         title: Optional[str] = None,
         description: Optional[str] = None,
         gt: Optional[float] = None,
@@ -346,7 +592,19 @@ class Form(Body):
         le: Optional[float] = None,
         min_length: Optional[int] = None,
         max_length: Optional[int] = None,
-        regex: Optional[str] = None,
+        pattern: Optional[str] = None,
+        regex: Annotated[
+            Optional[str],
+            deprecated(
+                "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+            ),
+        ] = None,
+        discriminator: Union[str, None] = None,
+        strict: Union[bool, None] = _Unset,
+        multiple_of: Union[float, None] = _Unset,
+        allow_inf_nan: Union[bool, None] = _Unset,
+        max_digits: Union[int, None] = _Unset,
+        decimal_places: Union[int, None] = _Unset,
         examples: Optional[List[Any]] = None,
         example: Annotated[
             Optional[Any],
@@ -354,14 +612,22 @@ class Form(Body):
                 "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
                 "although still supported. Use examples instead."
             ),
-        ] = Undefined,
+        ] = _Unset,
+        deprecated: Optional[bool] = None,
+        include_in_schema: bool = True,
+        json_schema_extra: Union[Dict[str, Any], None] = None,
         **extra: Any,
     ):
         super().__init__(
             default=default,
+            default_factory=default_factory,
+            annotation=annotation,
             embed=True,
             media_type=media_type,
             alias=alias,
+            alias_priority=alias_priority,
+            validation_alias=validation_alias,
+            serialization_alias=serialization_alias,
             title=title,
             description=description,
             gt=gt,
@@ -370,9 +636,19 @@ class Form(Body):
             le=le,
             min_length=min_length,
             max_length=max_length,
+            pattern=pattern,
             regex=regex,
+            discriminator=discriminator,
+            strict=strict,
+            multiple_of=multiple_of,
+            allow_inf_nan=allow_inf_nan,
+            max_digits=max_digits,
+            decimal_places=decimal_places,
+            deprecated=deprecated,
             example=example,
             examples=examples,
+            include_in_schema=include_in_schema,
+            json_schema_extra=json_schema_extra,
             **extra,
         )
 
@@ -382,8 +658,15 @@ class File(Form):
         self,
         default: Any = Undefined,
         *,
+        default_factory: Union[Callable[[], Any], None] = _Unset,
+        annotation: Optional[Any] = None,
         media_type: str = "multipart/form-data",
         alias: Optional[str] = None,
+        alias_priority: Union[int, None] = _Unset,
+        # TODO: update when deprecating Pydantic v1, import these types
+        # validation_alias: str | AliasPath | AliasChoices | None
+        validation_alias: Union[str, None] = None,
+        serialization_alias: Union[str, None] = None,
         title: Optional[str] = None,
         description: Optional[str] = None,
         gt: Optional[float] = None,
@@ -392,7 +675,19 @@ class File(Form):
         le: Optional[float] = None,
         min_length: Optional[int] = None,
         max_length: Optional[int] = None,
-        regex: Optional[str] = None,
+        pattern: Optional[str] = None,
+        regex: Annotated[
+            Optional[str],
+            deprecated(
+                "Deprecated in FastAPI 0.100.0 and Pydantic v2, use `pattern` instead."
+            ),
+        ] = None,
+        discriminator: Union[str, None] = None,
+        strict: Union[bool, None] = _Unset,
+        multiple_of: Union[float, None] = _Unset,
+        allow_inf_nan: Union[bool, None] = _Unset,
+        max_digits: Union[int, None] = _Unset,
+        decimal_places: Union[int, None] = _Unset,
         examples: Optional[List[Any]] = None,
         example: Annotated[
             Optional[Any],
@@ -400,13 +695,21 @@ class File(Form):
                 "Deprecated in OpenAPI 3.1.0 that now uses JSON Schema 2020-12, "
                 "although still supported. Use examples instead."
             ),
-        ] = Undefined,
+        ] = _Unset,
+        deprecated: Optional[bool] = None,
+        include_in_schema: bool = True,
+        json_schema_extra: Union[Dict[str, Any], None] = None,
         **extra: Any,
     ):
         super().__init__(
             default=default,
+            default_factory=default_factory,
+            annotation=annotation,
             media_type=media_type,
             alias=alias,
+            alias_priority=alias_priority,
+            validation_alias=validation_alias,
+            serialization_alias=serialization_alias,
             title=title,
             description=description,
             gt=gt,
@@ -415,9 +718,19 @@ class File(Form):
             le=le,
             min_length=min_length,
             max_length=max_length,
+            pattern=pattern,
             regex=regex,
+            discriminator=discriminator,
+            strict=strict,
+            multiple_of=multiple_of,
+            allow_inf_nan=allow_inf_nan,
+            max_digits=max_digits,
+            decimal_places=decimal_places,
+            deprecated=deprecated,
             example=example,
             examples=examples,
+            include_in_schema=include_in_schema,
+            json_schema_extra=json_schema_extra,
             **extra,
         )
 
index ec8af99b3a29045f3617f4190d293bbe3dab9730..d8ff0579cb81382e25bd322744a2aedcea0e2d16 100644 (file)
@@ -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,
@@ -414,7 +446,11 @@ class APIRoute(routing.Route):
             ), f"Status code {status_code} must not have a response body"
             response_name = "Response_" + self.unique_id
             self.response_field = create_response_field(
-                name=response_name, type_=self.response_model
+                name=response_name,
+                type_=self.response_model,
+                # TODO: This should actually set mode='serialization', just, that changes the schemas
+                # mode="serialization",
+                mode="validation",
             )
             # Create a clone of the field, so that a Pydantic submodel is not returned
             # as is just because it's an instance of a subclass of a more limited class
@@ -423,6 +459,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 +606,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 +687,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 +914,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 +970,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 +1026,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 +1082,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 +1138,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 +1194,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 +1250,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 +1306,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,
index 938dec37cd677b10bdafbb87a8474711fa4e39d2..e4c4357e7303aad2bb7e4b86fb08ac34d37dbad2 100644 (file)
@@ -9,6 +9,9 @@ from fastapi.security.utils import get_authorization_scheme_param
 from starlette.requests import Request
 from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
 
+# TODO: import from typing when deprecating Python 3.9
+from typing_extensions import Annotated
+
 
 class OAuth2PasswordRequestForm:
     """
@@ -45,12 +48,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 +99,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,
index e0bca46320b23a04b7d3ce9507c4b6b05215fdce..7adf565a7b6b7d4f1eed3adf6a96faab66fe517c 100644 (file)
@@ -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]]
index 9b9ebcb852ee0264300ba1efba68c055ba6db6bf..267d64ce8aee9eaf5462d9cbd47deca44cfdef28 100644 (file)
@@ -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 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
+from typing_extensions import Literal
 
 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,30 +64,40 @@ 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,
+    mode: Literal["validation", "serialization"] = "validation",
 ) -> ModelField:
     """
     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 PYDANTIC_V2:
+        kwargs.update({"mode": mode})
+    else:
+        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 +114,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 +136,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 +220,9 @@ def get_value_or_default(
         if not isinstance(item, DefaultPlaceholder):
             return item
     return first_item
+
+
+def match_pydantic_error_url(error_type: str) -> Any:
+    from dirty_equals import IsStr
+
+    return IsStr(regex=rf"^https://errors\.pydantic\.dev/.*/v/{error_type}")
index 61dbf76298a88936789691d6d01d045faa65c19b..f0917578f9d1ddf8a522f2d41eb0ab64ce26925d 100644 (file)
@@ -42,8 +42,8 @@ classifiers = [
 ]
 dependencies = [
     "starlette>=0.27.0,<0.28.0",
-    "pydantic>=1.7.4,!=1.8,!=1.8.1,<2.0.0",
-    "typing-extensions>=4.5.0"
+    "pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,<3.0.0",
+    "typing-extensions>=4.5.0",
 ]
 dynamic = ["version"]
 
@@ -61,8 +61,10 @@ 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",
+    "pydantic-settings >=2.0.0",
+    "pydantic-extra-types >=2.0.0",
 ]
 
 [tool.hatch.version]
@@ -85,6 +87,7 @@ check_untyped_defs = true
 addopts = [
   "--strict-config",
   "--strict-markers",
+  "--ignore=docs_src",
 ]
 xfail_strict = true
 junit_family = "xunit2"
@@ -142,6 +145,7 @@ ignore = [
 "docs_src/custom_response/tutorial007.py" = ["B007"]
 "docs_src/dataclasses/tutorial003.py" = ["I001"]
 "docs_src/path_operation_advanced_configuration/tutorial007.py" = ["B904"]
+"docs_src/path_operation_advanced_configuration/tutorial007_pv1.py" = ["B904"]
 "docs_src/custom_request_and_route/tutorial002.py" = ["B904"]
 "docs_src/dependencies/tutorial008_an.py" = ["F821"]
 "docs_src/dependencies/tutorial008_an_py39.py" = ["F821"]
index 4b34fcc2c5c9ec177286588736ab8e712e8c090d..abefac6852f3e305b50ec76efa51cdf97abd05f3 100644 (file)
@@ -1,11 +1,13 @@
 -e .
+pydantic-settings >=2.0.0
 pytest >=7.1.3,<8.0.0
 coverage[toml] >= 6.5.0,< 8.0
 mypy ==1.4.0
 ruff ==0.0.275
 black == 23.3.0
 httpx >=0.23.0,<0.25.0
-email_validator >=1.1.1,<2.0.0
+email_validator >=1.1.1,<3.0.0
+dirty-equals ==0.6.0
 # TODO: once removing databases from tutorial, upgrade SQLAlchemy
 # probably when including SQLModel
 sqlalchemy >=1.3.18,<1.4.43
index e35c2634206272886932e3db5cc00ef67d91ebf0..de59e48ce749301db133d1b8f8ec43f1eb42d280 100644 (file)
@@ -1,13 +1,19 @@
 from typing import Union
 
+from dirty_equals import IsDict
 from fastapi import FastAPI
+from fastapi._compat import PYDANTIC_V2
 from fastapi.testclient import TestClient
-from pydantic import BaseModel
+from pydantic import BaseModel, ConfigDict
 
 
 class FooBaseModel(BaseModel):
-    class Config:
-        extra = "forbid"
+    if PYDANTIC_V2:
+        model_config = ConfigDict(extra="forbid")
+    else:
+
+        class Config:
+            extra = "forbid"
 
 
 class Foo(FooBaseModel):
@@ -52,7 +58,19 @@ def test_openapi_schema():
                     "requestBody": {
                         "content": {
                             "application/json": {
-                                "schema": {"$ref": "#/components/schemas/Foo"}
+                                "schema": IsDict(
+                                    {
+                                        "anyOf": [
+                                            {"$ref": "#/components/schemas/Foo"},
+                                            {"type": "null"},
+                                        ],
+                                        "title": "Foo",
+                                    }
+                                )
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {"$ref": "#/components/schemas/Foo"}
+                                )
                             }
                         }
                     },
index 397159142783d576e0b31e5ec7ef3242613a497f..2ad57545510a7109d0e520871d384dcc13d501a5 100644 (file)
@@ -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",
                         }
index 5a70c45418cfc8b18b9ad87a41f69ed2278241b0..541f84bca1ca7c82cc9eca3af3f08498fad4522d 100644 (file)
@@ -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",
+            }
+        )
     ]
 }
 
index b036e67afe690ff4919d01b5952c177b021202ac..ea7a80128fd68b3b8b835690a94cd59cf954cb30 100644 (file)
@@ -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"}),
                         }
                     ],
                 }
diff --git a/tests/test_compat.py b/tests/test_compat.py
new file mode 100644 (file)
index 0000000..47160ee
--- /dev/null
@@ -0,0 +1,93 @@
+from typing import List, Union
+
+from fastapi import FastAPI, UploadFile
+from fastapi._compat import (
+    ModelField,
+    Undefined,
+    _get_model_config,
+    is_bytes_sequence_annotation,
+    is_uploadfile_sequence_annotation,
+)
+from fastapi.testclient import TestClient
+from pydantic import BaseConfig, BaseModel, ConfigDict
+from pydantic.fields import FieldInfo
+
+from .utils import needs_pydanticv1, needs_pydanticv2
+
+
+@needs_pydanticv2
+def test_model_field_default_required():
+    # For coverage
+    field_info = FieldInfo(annotation=str)
+    field = ModelField(name="foo", field_info=field_info)
+    assert field.default is Undefined
+
+
+@needs_pydanticv1
+def test_upload_file_dummy_general_plain_validator_function():
+    # For coverage
+    assert UploadFile.__get_pydantic_core_schema__(str, lambda x: None) == {}
+
+
+@needs_pydanticv1
+def test_union_scalar_list():
+    # For coverage
+    # TODO: there might not be a current valid code path that uses this, it would
+    # potentially enable query parameters defined as both a scalar and a list
+    # but that would require more refactors, also not sure it's really useful
+    from fastapi._compat import is_pv1_scalar_field
+
+    field_info = FieldInfo()
+    field = ModelField(
+        name="foo",
+        field_info=field_info,
+        type_=Union[str, List[int]],
+        class_validators={},
+        model_config=BaseConfig,
+    )
+    assert not is_pv1_scalar_field(field)
+
+
+@needs_pydanticv2
+def test_get_model_config():
+    # For coverage in Pydantic v2
+    class Foo(BaseModel):
+        model_config = ConfigDict(from_attributes=True)
+
+    foo = Foo()
+    config = _get_model_config(foo)
+    assert config == {"from_attributes": True}
+
+
+def test_complex():
+    app = FastAPI()
+
+    @app.post("/")
+    def foo(foo: Union[str, List[int]]):
+        return foo
+
+    client = TestClient(app)
+
+    response = client.post("/", json="bar")
+    assert response.status_code == 200, response.text
+    assert response.json() == "bar"
+
+    response2 = client.post("/", json=[1, 2])
+    assert response2.status_code == 200, response2.text
+    assert response2.json() == [1, 2]
+
+
+def test_is_bytes_sequence_annotation_union():
+    # For coverage
+    # TODO: in theory this would allow declaring types that could be lists of bytes
+    # to be read from files and other types, but I'm not even sure it's a good idea
+    # to support it as a first class "feature"
+    assert is_bytes_sequence_annotation(Union[List[str], List[bytes]])
+
+
+def test_is_uploadfile_sequence_annotation():
+    # For coverage
+    # TODO: in theory this would allow declaring types that could be lists of UploadFile
+    # and other types, but I'm not even sure it's a good idea to support it as a first
+    # class "feature"
+    assert is_uploadfile_sequence_annotation(Union[List[str], List[UploadFile]])
index 10b02608c3ee4d024795abc072d095f6c994586d..ee51fc7ff5e9b18737b9c40a55bc52106a2ed791 100644 (file)
@@ -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)
index 2e6217d34eb08999abc567005554f1ec7deb402a..b91467265a2f4ae588c85b075996c7b6a71d6d8f 100644 (file)
@@ -7,11 +7,17 @@ from fastapi.datastructures import Default
 from fastapi.testclient import TestClient
 
 
+# TODO: remove when deprecating Pydantic v1
 def test_upload_file_invalid():
     with pytest.raises(ValueError):
         UploadFile.validate("not a Starlette UploadFile")
 
 
+def test_upload_file_invalid_pydantic_v2():
+    with pytest.raises(ValueError):
+        UploadFile._validate("not a Starlette UploadFile", {})
+
+
 def test_default_placeholder_equals():
     placeholder_1 = Default("a")
     placeholder_2 = Default("a")
index 5c1833eb4cea62fee637b1b0739a7cc742208999..3aa77c0b1db47902d98d1bccd8dd9abced4c4ba6 100644 (file)
@@ -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"}
index 4f4f3166ca4c765c0bec0c049cbab21bf9a40c53..0882cc41d7abff336830f8929e787a20e2f7159a 100644 (file)
@@ -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():
index 8bb307971bd559932ad55075d5bb3e826f2e2254..21cff998d18c42381bb9e101422ba19a72a3908e 100644 (file)
@@ -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 = {}
index fa95d061c4dff6d1db793afc2b3d1e7322678c5c..bd16fe9254cc341e0f0907676a0033694abc33e4 100644 (file)
@@ -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": {
diff --git a/tests/test_filter_pydantic_sub_model/__init__.py b/tests/test_filter_pydantic_sub_model/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/test_filter_pydantic_sub_model/app_pv1.py b/tests/test_filter_pydantic_sub_model/app_pv1.py
new file mode 100644 (file)
index 0000000..657e8c5
--- /dev/null
@@ -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}
similarity index 81%
rename from tests/test_filter_pydantic_sub_model.py
rename to tests/test_filter_pydantic_sub_model/test_filter_pydantic_sub_model_pv1.py
index 6ee928d07f8717db04e0926456493ff74080e72f..48732dbf06ea528f6144d8476d62e641bcfdb5df 100644 (file)
@@ -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() == {
diff --git a/tests/test_filter_pydantic_sub_model_pv2.py b/tests/test_filter_pydantic_sub_model_pv2.py
new file mode 100644 (file)
index 0000000..656332a
--- /dev/null
@@ -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.1.0",
+        "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"},
+                    },
+                },
+            }
+        },
+    }
index 5e673d9c4d796cf9fb6266eef04f409351ac873b..e3d57bb428bf2e789040fdda1c039ba62cff1b1b 100644 (file)
@@ -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.1.0",
+        "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"]
index bac7eec1b08869ed48dc5bf8147884e8aa0ee453..42b249211ddc9ee6a44c306d0b1fc99a85a4a7d1 100644 (file)
@@ -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")
index 1f43c33c75609cc8ec79d824fbae317234584007..ff3033ecdf427d6749ab8fe1dd13c500b8ca4c62 100644 (file)
@@ -1,13 +1,17 @@
 from collections import deque
 from dataclasses import dataclass
 from datetime import datetime, timezone
+from decimal import Decimal
 from enum import Enum
 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:
@@ -46,22 +50,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"
@@ -70,8 +58,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):
@@ -84,23 +76,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"}}
@@ -154,14 +129,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"}
+
+
+# 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()
+            }
 
-def test_encode_custom_json_encoders_model_subclass():
-    model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8))
+    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():
@@ -197,6 +205,7 @@ def test_encode_model_with_default():
     }
 
 
+@needs_pydanticv1
 def test_custom_encoders():
     class safe_datetime(datetime):
         pass
@@ -227,19 +236,72 @@ 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"
 
 
+@needs_pydanticv2
+def test_decimal_encoder_float():
+    data = {"value": Decimal(1.23)}
+    assert jsonable_encoder(data) == {"value": 1.23}
+
+
+@needs_pydanticv2
+def test_decimal_encoder_int():
+    data = {"value": Decimal(2)}
+    assert jsonable_encoder(data) == {"value": 2}
+
+
 def test_encode_deque_encodes_child_models():
     class Model(BaseModel):
         test: str
index 96043aa35f44d10ca3469f888404250b9e2638b6..aa989c612874f404e189d3e169ab0e975a4f879b 100644 (file)
@@ -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": {
index c1f0678d0130b7c9b589c010b664fc1136f299b0..470a3580834687b4cc8906746bccd83358231fcb 100644 (file)
@@ -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():
index 6f62e67265101dbbf06eb349d2e75ff7577b11f1..dc7147c712463b4736964d91669b743bea2f8939 100644 (file)
@@ -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",
                         },
index 11cd795ac584657e04008e71dab7811d5c7e18a8..8697c8438b7c7c11763526af870e59a4c4788f68 100644 (file)
@@ -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": {
index d8dca1ea42abd3ed03119550cf20f8f27aa2b683..bfc7bed0964baed4fb26e52a82d9a4933dbb51ac 100644 (file)
@@ -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():
index 03b93623a97e9b9e48f188df3277960bb8762b70..848b245e2c4d4d13b7bfd72820bcfea1282e7373 100644 (file)
@@ -1,5 +1,6 @@
-import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
 
 from .main import app
 
@@ -18,235 +19,1259 @@ def test_nonexistent():
     assert response.json() == {"detail": "Not Found"}
 
 
-response_not_valid_bool = {
-    "detail": [
+def test_path_foobar():
+    response = client.get("/path/foobar")
+    assert response.status_code == 200
+    assert response.json() == "foobar"
+
+
+def test_path_str_foobar():
+    response = client.get("/path/str/foobar")
+    assert response.status_code == 200
+    assert response.json() == "foobar"
+
+
+def test_path_str_42():
+    response = client.get("/path/str/42")
+    assert response.status_code == 200
+    assert response.json() == "42"
+
+
+def test_path_str_True():
+    response = client.get("/path/str/True")
+    assert response.status_code == 200
+    assert response.json() == "True"
+
+
+def test_path_int_foobar():
+    response = client.get("/path/int/foobar")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
         {
-            "loc": ["path", "item_id"],
-            "msg": "value could not be parsed to a boolean",
-            "type": "type_error.bool",
+            "detail": [
+                {
+                    "type": "int_parsing",
+                    "loc": ["path", "item_id"],
+                    "msg": "Input should be a valid integer, unable to parse string as an integer",
+                    "input": "foobar",
+                    "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",
+                }
+            ]
+        }
+    )
 
-response_not_valid_int = {
-    "detail": [
+
+def test_path_int_True():
+    response = client.get("/path/int/True")
+    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": "True",
+                    "url": match_pydantic_error_url("int_parsing"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
         {
-            "loc": ["path", "item_id"],
-            "msg": "value is not a valid integer",
-            "type": "type_error.integer",
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "value is not a valid integer",
+                    "type": "type_error.integer",
+                }
+            ]
         }
-    ]
-}
+    )
+
+
+def test_path_int_42():
+    response = client.get("/path/int/42")
+    assert response.status_code == 200
+    assert response.json() == 42
+
 
-response_not_valid_float = {
-    "detail": [
+def test_path_int_42_5():
+    response = client.get("/path/int/42.5")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
         {
-            "loc": ["path", "item_id"],
-            "msg": "value is not a valid float",
-            "type": "type_error.float",
+            "detail": [
+                {
+                    "type": "int_parsing",
+                    "loc": ["path", "item_id"],
+                    "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": ["path", "item_id"],
+                    "msg": "value is not a valid integer",
+                    "type": "type_error.integer",
+                }
+            ]
+        }
+    )
 
-response_at_least_3 = {
-    "detail": [
+
+def test_path_float_foobar():
+    response = client.get("/path/float/foobar")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "float_parsing",
+                    "loc": ["path", "item_id"],
+                    "msg": "Input should be a valid number, unable to parse string as a number",
+                    "input": "foobar",
+                    "url": match_pydantic_error_url("float_parsing"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
         {
-            "loc": ["path", "item_id"],
-            "msg": "ensure this value has at least 3 characters",
-            "type": "value_error.any_str.min_length",
-            "ctx": {"limit_value": 3},
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "value is not a valid float",
+                    "type": "type_error.float",
+                }
+            ]
         }
-    ]
-}
+    )
 
 
-response_at_least_2 = {
-    "detail": [
+def test_path_float_True():
+    response = client.get("/path/float/True")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "float_parsing",
+                    "loc": ["path", "item_id"],
+                    "msg": "Input should be a valid number, unable to parse string as a number",
+                    "input": "True",
+                    "url": match_pydantic_error_url("float_parsing"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
         {
-            "loc": ["path", "item_id"],
-            "msg": "ensure this value has at least 2 characters",
-            "type": "value_error.any_str.min_length",
-            "ctx": {"limit_value": 2},
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "value is not a valid float",
+                    "type": "type_error.float",
+                }
+            ]
         }
-    ]
-}
+    )
+
 
+def test_path_float_42():
+    response = client.get("/path/float/42")
+    assert response.status_code == 200
+    assert response.json() == 42
 
-response_maximum_3 = {
-    "detail": [
+
+def test_path_float_42_5():
+    response = client.get("/path/float/42.5")
+    assert response.status_code == 200
+    assert response.json() == 42.5
+
+
+def test_path_bool_foobar():
+    response = client.get("/path/bool/foobar")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "bool_parsing",
+                    "loc": ["path", "item_id"],
+                    "msg": "Input should be a valid boolean, unable to interpret input",
+                    "input": "foobar",
+                    "url": match_pydantic_error_url("bool_parsing"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
         {
-            "loc": ["path", "item_id"],
-            "msg": "ensure this value has at most 3 characters",
-            "type": "value_error.any_str.max_length",
-            "ctx": {"limit_value": 3},
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "value could not be parsed to a boolean",
+                    "type": "type_error.bool",
+                }
+            ]
         }
-    ]
-}
+    )
+
 
+def test_path_bool_True():
+    response = client.get("/path/bool/True")
+    assert response.status_code == 200
+    assert response.json() is True
 
-response_greater_than_3 = {
-    "detail": [
+
+def test_path_bool_42():
+    response = client.get("/path/bool/42")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
         {
-            "loc": ["path", "item_id"],
-            "msg": "ensure this value is greater than 3",
-            "type": "value_error.number.not_gt",
-            "ctx": {"limit_value": 3},
+            "detail": [
+                {
+                    "type": "bool_parsing",
+                    "loc": ["path", "item_id"],
+                    "msg": "Input should be a valid boolean, unable to interpret input",
+                    "input": "42",
+                    "url": match_pydantic_error_url("bool_parsing"),
+                }
+            ]
         }
-    ]
-}
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "value could not be parsed to a boolean",
+                    "type": "type_error.bool",
+                }
+            ]
+        }
+    )
+
+
+def test_path_bool_42_5():
+    response = client.get("/path/bool/42.5")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "bool_parsing",
+                    "loc": ["path", "item_id"],
+                    "msg": "Input should be a valid boolean, unable to interpret input",
+                    "input": "42.5",
+                    "url": match_pydantic_error_url("bool_parsing"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "value could not be parsed to a boolean",
+                    "type": "type_error.bool",
+                }
+            ]
+        }
+    )
+
+
+def test_path_bool_1():
+    response = client.get("/path/bool/1")
+    assert response.status_code == 200
+    assert response.json() is True
+
+
+def test_path_bool_0():
+    response = client.get("/path/bool/0")
+    assert response.status_code == 200
+    assert response.json() is False
+
+
+def test_path_bool_true():
+    response = client.get("/path/bool/true")
+    assert response.status_code == 200
+    assert response.json() is True
+
+
+def test_path_bool_False():
+    response = client.get("/path/bool/False")
+    assert response.status_code == 200
+    assert response.json() is False
+
 
+def test_path_bool_false():
+    response = client.get("/path/bool/false")
+    assert response.status_code == 200
+    assert response.json() is False
 
-response_greater_than_0 = {
-    "detail": [
+
+def test_path_param_foo():
+    response = client.get("/path/param/foo")
+    assert response.status_code == 200
+    assert response.json() == "foo"
+
+
+def test_path_param_minlength_foo():
+    response = client.get("/path/param-minlength/foo")
+    assert response.status_code == 200
+    assert response.json() == "foo"
+
+
+def test_path_param_minlength_fo():
+    response = client.get("/path/param-minlength/fo")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "string_too_short",
+                    "loc": ["path", "item_id"],
+                    "msg": "String should have at least 3 characters",
+                    "input": "fo",
+                    "ctx": {"min_length": 3},
+                    "url": match_pydantic_error_url("string_too_short"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "ensure this value has at least 3 characters",
+                    "type": "value_error.any_str.min_length",
+                    "ctx": {"limit_value": 3},
+                }
+            ]
+        }
+    )
+
+
+def test_path_param_maxlength_foo():
+    response = client.get("/path/param-maxlength/foo")
+    assert response.status_code == 200
+    assert response.json() == "foo"
+
+
+def test_path_param_maxlength_foobar():
+    response = client.get("/path/param-maxlength/foobar")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "string_too_long",
+                    "loc": ["path", "item_id"],
+                    "msg": "String should have at most 3 characters",
+                    "input": "foobar",
+                    "ctx": {"max_length": 3},
+                    "url": match_pydantic_error_url("string_too_long"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "ensure this value has at most 3 characters",
+                    "type": "value_error.any_str.max_length",
+                    "ctx": {"limit_value": 3},
+                }
+            ]
+        }
+    )
+
+
+def test_path_param_min_maxlength_foo():
+    response = client.get("/path/param-min_maxlength/foo")
+    assert response.status_code == 200
+    assert response.json() == "foo"
+
+
+def test_path_param_min_maxlength_foobar():
+    response = client.get("/path/param-min_maxlength/foobar")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "string_too_long",
+                    "loc": ["path", "item_id"],
+                    "msg": "String should have at most 3 characters",
+                    "input": "foobar",
+                    "ctx": {"max_length": 3},
+                    "url": match_pydantic_error_url("string_too_long"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "ensure this value has at most 3 characters",
+                    "type": "value_error.any_str.max_length",
+                    "ctx": {"limit_value": 3},
+                }
+            ]
+        }
+    )
+
+
+def test_path_param_min_maxlength_f():
+    response = client.get("/path/param-min_maxlength/f")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "string_too_short",
+                    "loc": ["path", "item_id"],
+                    "msg": "String should have at least 2 characters",
+                    "input": "f",
+                    "ctx": {"min_length": 2},
+                    "url": match_pydantic_error_url("string_too_short"),
+                }
+            ]
+        }
+    ) | IsDict(
+        {
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "ensure this value has at least 2 characters",
+                    "type": "value_error.any_str.min_length",
+                    "ctx": {"limit_value": 2},
+                }
+            ]
+        }
+    )
+
+
+def test_path_param_gt_42():
+    response = client.get("/path/param-gt/42")
+    assert response.status_code == 200
+    assert response.json() == 42
+
+
+def test_path_param_gt_2():
+    response = client.get("/path/param-gt/2")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "greater_than",
+                    "loc": ["path", "item_id"],
+                    "msg": "Input should be greater than 3",
+                    "input": "2",
+                    "ctx": {"gt": 3.0},
+                    "url": match_pydantic_error_url("greater_than"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "ensure this value is greater than 3",
+                    "type": "value_error.number.not_gt",
+                    "ctx": {"limit_value": 3},
+                }
+            ]
+        }
+    )
+
+
+def test_path_param_gt0_0_05():
+    response = client.get("/path/param-gt0/0.05")
+    assert response.status_code == 200
+    assert response.json() == 0.05
+
+
+def test_path_param_gt0_0():
+    response = client.get("/path/param-gt0/0")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "greater_than",
+                    "loc": ["path", "item_id"],
+                    "msg": "Input should be greater than 0",
+                    "input": "0",
+                    "ctx": {"gt": 0.0},
+                    "url": match_pydantic_error_url("greater_than"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "ensure this value is greater than 0",
+                    "type": "value_error.number.not_gt",
+                    "ctx": {"limit_value": 0},
+                }
+            ]
+        }
+    )
+
+
+def test_path_param_ge_42():
+    response = client.get("/path/param-ge/42")
+    assert response.status_code == 200
+    assert response.json() == 42
+
+
+def test_path_param_ge_3():
+    response = client.get("/path/param-ge/3")
+    assert response.status_code == 200
+    assert response.json() == 3
+
+
+def test_path_param_ge_2():
+    response = client.get("/path/param-ge/2")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "greater_than_equal",
+                    "loc": ["path", "item_id"],
+                    "msg": "Input should be greater than or equal to 3",
+                    "input": "2",
+                    "ctx": {"ge": 3.0},
+                    "url": match_pydantic_error_url("greater_than_equal"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "ensure this value is greater than or equal to 3",
+                    "type": "value_error.number.not_ge",
+                    "ctx": {"limit_value": 3},
+                }
+            ]
+        }
+    )
+
+
+def test_path_param_lt_42():
+    response = client.get("/path/param-lt/42")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "less_than",
+                    "loc": ["path", "item_id"],
+                    "msg": "Input should be less than 3",
+                    "input": "42",
+                    "ctx": {"lt": 3.0},
+                    "url": match_pydantic_error_url("less_than"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "ensure this value is less than 3",
+                    "type": "value_error.number.not_lt",
+                    "ctx": {"limit_value": 3},
+                }
+            ]
+        }
+    )
+
+
+def test_path_param_lt_2():
+    response = client.get("/path/param-lt/2")
+    assert response.status_code == 200
+    assert response.json() == 2
+
+
+def test_path_param_lt0__1():
+    response = client.get("/path/param-lt0/-1")
+    assert response.status_code == 200
+    assert response.json() == -1
+
+
+def test_path_param_lt0_0():
+    response = client.get("/path/param-lt0/0")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "less_than",
+                    "loc": ["path", "item_id"],
+                    "msg": "Input should be less than 0",
+                    "input": "0",
+                    "ctx": {"lt": 0.0},
+                    "url": match_pydantic_error_url("less_than"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "ensure this value is less than 0",
+                    "type": "value_error.number.not_lt",
+                    "ctx": {"limit_value": 0},
+                }
+            ]
+        }
+    )
+
+
+def test_path_param_le_42():
+    response = client.get("/path/param-le/42")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "less_than_equal",
+                    "loc": ["path", "item_id"],
+                    "msg": "Input should be less than or equal to 3",
+                    "input": "42",
+                    "ctx": {"le": 3.0},
+                    "url": match_pydantic_error_url("less_than_equal"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
         {
-            "loc": ["path", "item_id"],
-            "msg": "ensure this value is greater than 0",
-            "type": "value_error.number.not_gt",
-            "ctx": {"limit_value": 0},
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "ensure this value is less than or equal to 3",
+                    "type": "value_error.number.not_le",
+                    "ctx": {"limit_value": 3},
+                }
+            ]
         }
-    ]
-}
+    )
 
 
-response_greater_than_1 = {
-    "detail": [
+def test_path_param_le_3():
+    response = client.get("/path/param-le/3")
+    assert response.status_code == 200
+    assert response.json() == 3
+
+
+def test_path_param_le_2():
+    response = client.get("/path/param-le/2")
+    assert response.status_code == 200
+    assert response.json() == 2
+
+
+def test_path_param_lt_gt_2():
+    response = client.get("/path/param-lt-gt/2")
+    assert response.status_code == 200
+    assert response.json() == 2
+
+
+def test_path_param_lt_gt_4():
+    response = client.get("/path/param-lt-gt/4")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "less_than",
+                    "loc": ["path", "item_id"],
+                    "msg": "Input should be less than 3",
+                    "input": "4",
+                    "ctx": {"lt": 3.0},
+                    "url": match_pydantic_error_url("less_than"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "ensure this value is less than 3",
+                    "type": "value_error.number.not_lt",
+                    "ctx": {"limit_value": 3},
+                }
+            ]
+        }
+    )
+
+
+def test_path_param_lt_gt_0():
+    response = client.get("/path/param-lt-gt/0")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "greater_than",
+                    "loc": ["path", "item_id"],
+                    "msg": "Input should be greater than 1",
+                    "input": "0",
+                    "ctx": {"gt": 1.0},
+                    "url": match_pydantic_error_url("greater_than"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "ensure this value is greater than 1",
+                    "type": "value_error.number.not_gt",
+                    "ctx": {"limit_value": 1},
+                }
+            ]
+        }
+    )
+
+
+def test_path_param_le_ge_2():
+    response = client.get("/path/param-le-ge/2")
+    assert response.status_code == 200
+    assert response.json() == 2
+
+
+def test_path_param_le_ge_1():
+    response = client.get("/path/param-le-ge/1")
+    assert response.status_code == 200
+
+
+def test_path_param_le_ge_3():
+    response = client.get("/path/param-le-ge/3")
+    assert response.status_code == 200
+    assert response.json() == 3
+
+
+def test_path_param_le_ge_4():
+    response = client.get("/path/param-le-ge/4")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "less_than_equal",
+                    "loc": ["path", "item_id"],
+                    "msg": "Input should be less than or equal to 3",
+                    "input": "4",
+                    "ctx": {"le": 3.0},
+                    "url": match_pydantic_error_url("less_than_equal"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "ensure this value is less than or equal to 3",
+                    "type": "value_error.number.not_le",
+                    "ctx": {"limit_value": 3},
+                }
+            ]
+        }
+    )
+
+
+def test_path_param_lt_int_2():
+    response = client.get("/path/param-lt-int/2")
+    assert response.status_code == 200
+    assert response.json() == 2
+
+
+def test_path_param_lt_int_42():
+    response = client.get("/path/param-lt-int/42")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "less_than",
+                    "loc": ["path", "item_id"],
+                    "msg": "Input should be less than 3",
+                    "input": "42",
+                    "ctx": {"lt": 3},
+                    "url": match_pydantic_error_url("less_than"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "ensure this value is less than 3",
+                    "type": "value_error.number.not_lt",
+                    "ctx": {"limit_value": 3},
+                }
+            ]
+        }
+    )
+
+
+def test_path_param_lt_int_2_7():
+    response = client.get("/path/param-lt-int/2.7")
+    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": "2.7",
+                    "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_path_param_gt_int_42():
+    response = client.get("/path/param-gt-int/42")
+    assert response.status_code == 200
+    assert response.json() == 42
+
+
+def test_path_param_gt_int_2():
+    response = client.get("/path/param-gt-int/2")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "greater_than",
+                    "loc": ["path", "item_id"],
+                    "msg": "Input should be greater than 3",
+                    "input": "2",
+                    "ctx": {"gt": 3},
+                    "url": match_pydantic_error_url("greater_than"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "ensure this value is greater than 3",
+                    "type": "value_error.number.not_gt",
+                    "ctx": {"limit_value": 3},
+                }
+            ]
+        }
+    )
+
+
+def test_path_param_gt_int_2_7():
+    response = client.get("/path/param-gt-int/2.7")
+    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": "2.7",
+                    "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_path_param_le_int_42():
+    response = client.get("/path/param-le-int/42")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "less_than_equal",
+                    "loc": ["path", "item_id"],
+                    "msg": "Input should be less than or equal to 3",
+                    "input": "42",
+                    "ctx": {"le": 3},
+                    "url": match_pydantic_error_url("less_than_equal"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "ensure this value is less than or equal to 3",
+                    "type": "value_error.number.not_le",
+                    "ctx": {"limit_value": 3},
+                }
+            ]
+        }
+    )
+
+
+def test_path_param_le_int_3():
+    response = client.get("/path/param-le-int/3")
+    assert response.status_code == 200
+    assert response.json() == 3
+
+
+def test_path_param_le_int_2():
+    response = client.get("/path/param-le-int/2")
+    assert response.status_code == 200
+    assert response.json() == 2
+
+
+def test_path_param_le_int_2_7():
+    response = client.get("/path/param-le-int/2.7")
+    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": "2.7",
+                    "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_path_param_ge_int_42():
+    response = client.get("/path/param-ge-int/42")
+    assert response.status_code == 200
+    assert response.json() == 42
+
+
+def test_path_param_ge_int_3():
+    response = client.get("/path/param-ge-int/3")
+    assert response.status_code == 200
+    assert response.json() == 3
+
+
+def test_path_param_ge_int_2():
+    response = client.get("/path/param-ge-int/2")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "greater_than_equal",
+                    "loc": ["path", "item_id"],
+                    "msg": "Input should be greater than or equal to 3",
+                    "input": "2",
+                    "ctx": {"ge": 3},
+                    "url": match_pydantic_error_url("greater_than_equal"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "ensure this value is greater than or equal to 3",
+                    "type": "value_error.number.not_ge",
+                    "ctx": {"limit_value": 3},
+                }
+            ]
+        }
+    )
+
+
+def test_path_param_ge_int_2_7():
+    response = client.get("/path/param-ge-int/2.7")
+    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": "2.7",
+                    "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_path_param_lt_gt_int_2():
+    response = client.get("/path/param-lt-gt-int/2")
+    assert response.status_code == 200
+    assert response.json() == 2
+
+
+def test_path_param_lt_gt_int_4():
+    response = client.get("/path/param-lt-gt-int/4")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "less_than",
+                    "loc": ["path", "item_id"],
+                    "msg": "Input should be less than 3",
+                    "input": "4",
+                    "ctx": {"lt": 3},
+                    "url": match_pydantic_error_url("less_than"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "ensure this value is less than 3",
+                    "type": "value_error.number.not_lt",
+                    "ctx": {"limit_value": 3},
+                }
+            ]
+        }
+    )
+
+
+def test_path_param_lt_gt_int_0():
+    response = client.get("/path/param-lt-gt-int/0")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "greater_than",
+                    "loc": ["path", "item_id"],
+                    "msg": "Input should be greater than 1",
+                    "input": "0",
+                    "ctx": {"gt": 1},
+                    "url": match_pydantic_error_url("greater_than"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "ensure this value is greater than 1",
+                    "type": "value_error.number.not_gt",
+                    "ctx": {"limit_value": 1},
+                }
+            ]
+        }
+    )
+
+
+def test_path_param_lt_gt_int_2_7():
+    response = client.get("/path/param-lt-gt-int/2.7")
+    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": "2.7",
+                    "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_path_param_le_ge_int_2():
+    response = client.get("/path/param-le-ge-int/2")
+    assert response.status_code == 200
+    assert response.json() == 2
+
+
+def test_path_param_le_ge_int_1():
+    response = client.get("/path/param-le-ge-int/1")
+    assert response.status_code == 200
+    assert response.json() == 1
+
+
+def test_path_param_le_ge_int_3():
+    response = client.get("/path/param-le-ge-int/3")
+    assert response.status_code == 200
+    assert response.json() == 3
+
+
+def test_path_param_le_ge_int_4():
+    response = client.get("/path/param-le-ge-int/4")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "less_than_equal",
+                    "loc": ["path", "item_id"],
+                    "msg": "Input should be less than or equal to 3",
+                    "input": "4",
+                    "ctx": {"le": 3},
+                    "url": match_pydantic_error_url("less_than_equal"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "ensure this value is less than or equal to 3",
+                    "type": "value_error.number.not_le",
+                    "ctx": {"limit_value": 3},
+                }
+            ]
+        }
+    )
+
+
+def test_path_param_le_ge_int_2_7():
+    response = client.get("/path/param-le-ge-int/2.7")
+    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": "2.7",
+                    "url": match_pydantic_error_url("int_parsing"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
         {
-            "loc": ["path", "item_id"],
-            "msg": "ensure this value is greater than 1",
-            "type": "value_error.number.not_gt",
-            "ctx": {"limit_value": 1},
+            "detail": [
+                {
+                    "loc": ["path", "item_id"],
+                    "msg": "value is not a valid integer",
+                    "type": "type_error.integer",
+                }
+            ]
         }
-    ]
-}
-
-
-response_greater_than_equal_3 = {
-    "detail": [
-        {
-            "loc": ["path", "item_id"],
-            "msg": "ensure this value is greater than or equal to 3",
-            "type": "value_error.number.not_ge",
-            "ctx": {"limit_value": 3},
-        }
-    ]
-}
-
-
-response_less_than_3 = {
-    "detail": [
-        {
-            "loc": ["path", "item_id"],
-            "msg": "ensure this value is less than 3",
-            "type": "value_error.number.not_lt",
-            "ctx": {"limit_value": 3},
-        }
-    ]
-}
-
-
-response_less_than_0 = {
-    "detail": [
-        {
-            "loc": ["path", "item_id"],
-            "msg": "ensure this value is less than 0",
-            "type": "value_error.number.not_lt",
-            "ctx": {"limit_value": 0},
-        }
-    ]
-}
-
-
-response_less_than_equal_3 = {
-    "detail": [
-        {
-            "loc": ["path", "item_id"],
-            "msg": "ensure this value is less than or equal to 3",
-            "type": "value_error.number.not_le",
-            "ctx": {"limit_value": 3},
-        }
-    ]
-}
-
-
-@pytest.mark.parametrize(
-    "path,expected_status,expected_response",
-    [
-        ("/path/foobar", 200, "foobar"),
-        ("/path/str/foobar", 200, "foobar"),
-        ("/path/str/42", 200, "42"),
-        ("/path/str/True", 200, "True"),
-        ("/path/int/foobar", 422, response_not_valid_int),
-        ("/path/int/True", 422, response_not_valid_int),
-        ("/path/int/42", 200, 42),
-        ("/path/int/42.5", 422, response_not_valid_int),
-        ("/path/float/foobar", 422, response_not_valid_float),
-        ("/path/float/True", 422, response_not_valid_float),
-        ("/path/float/42", 200, 42),
-        ("/path/float/42.5", 200, 42.5),
-        ("/path/bool/foobar", 422, response_not_valid_bool),
-        ("/path/bool/True", 200, True),
-        ("/path/bool/42", 422, response_not_valid_bool),
-        ("/path/bool/42.5", 422, response_not_valid_bool),
-        ("/path/bool/1", 200, True),
-        ("/path/bool/0", 200, False),
-        ("/path/bool/true", 200, True),
-        ("/path/bool/False", 200, False),
-        ("/path/bool/false", 200, False),
-        ("/path/param/foo", 200, "foo"),
-        ("/path/param-minlength/foo", 200, "foo"),
-        ("/path/param-minlength/fo", 422, response_at_least_3),
-        ("/path/param-maxlength/foo", 200, "foo"),
-        ("/path/param-maxlength/foobar", 422, response_maximum_3),
-        ("/path/param-min_maxlength/foo", 200, "foo"),
-        ("/path/param-min_maxlength/foobar", 422, response_maximum_3),
-        ("/path/param-min_maxlength/f", 422, response_at_least_2),
-        ("/path/param-gt/42", 200, 42),
-        ("/path/param-gt/2", 422, response_greater_than_3),
-        ("/path/param-gt0/0.05", 200, 0.05),
-        ("/path/param-gt0/0", 422, response_greater_than_0),
-        ("/path/param-ge/42", 200, 42),
-        ("/path/param-ge/3", 200, 3),
-        ("/path/param-ge/2", 422, response_greater_than_equal_3),
-        ("/path/param-lt/42", 422, response_less_than_3),
-        ("/path/param-lt/2", 200, 2),
-        ("/path/param-lt0/-1", 200, -1),
-        ("/path/param-lt0/0", 422, response_less_than_0),
-        ("/path/param-le/42", 422, response_less_than_equal_3),
-        ("/path/param-le/3", 200, 3),
-        ("/path/param-le/2", 200, 2),
-        ("/path/param-lt-gt/2", 200, 2),
-        ("/path/param-lt-gt/4", 422, response_less_than_3),
-        ("/path/param-lt-gt/0", 422, response_greater_than_1),
-        ("/path/param-le-ge/2", 200, 2),
-        ("/path/param-le-ge/1", 200, 1),
-        ("/path/param-le-ge/3", 200, 3),
-        ("/path/param-le-ge/4", 422, response_less_than_equal_3),
-        ("/path/param-lt-int/2", 200, 2),
-        ("/path/param-lt-int/42", 422, response_less_than_3),
-        ("/path/param-lt-int/2.7", 422, response_not_valid_int),
-        ("/path/param-gt-int/42", 200, 42),
-        ("/path/param-gt-int/2", 422, response_greater_than_3),
-        ("/path/param-gt-int/2.7", 422, response_not_valid_int),
-        ("/path/param-le-int/42", 422, response_less_than_equal_3),
-        ("/path/param-le-int/3", 200, 3),
-        ("/path/param-le-int/2", 200, 2),
-        ("/path/param-le-int/2.7", 422, response_not_valid_int),
-        ("/path/param-ge-int/42", 200, 42),
-        ("/path/param-ge-int/3", 200, 3),
-        ("/path/param-ge-int/2", 422, response_greater_than_equal_3),
-        ("/path/param-ge-int/2.7", 422, response_not_valid_int),
-        ("/path/param-lt-gt-int/2", 200, 2),
-        ("/path/param-lt-gt-int/4", 422, response_less_than_3),
-        ("/path/param-lt-gt-int/0", 422, response_greater_than_1),
-        ("/path/param-lt-gt-int/2.7", 422, response_not_valid_int),
-        ("/path/param-le-ge-int/2", 200, 2),
-        ("/path/param-le-ge-int/1", 200, 1),
-        ("/path/param-le-ge-int/3", 200, 3),
-        ("/path/param-le-ge-int/4", 422, response_less_than_equal_3),
-        ("/path/param-le-ge-int/2.7", 422, response_not_valid_int),
-    ],
-)
-def test_get_path(path, expected_status, expected_response):
-    response = client.get(path)
-    assert response.status_code == expected_status
-    assert response.json() == expected_response
+    )
index 0c73eb665eda71f5986e2cbbac6c59199fddceb4..5bb9995d6c5ad443109264b0ac9e0ffdc5159060 100644 (file)
-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"
index 360ad250350ec52366b423d38b988643572e2f00..b35987443928113594d0958d64498ea22b1230cc 100644 (file)
@@ -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()
diff --git a/tests/test_regex_deprecated_body.py b/tests/test_regex_deprecated_body.py
new file mode 100644 (file)
index 0000000..ca1ab51
--- /dev/null
@@ -0,0 +1,182 @@
+import pytest
+from dirty_equals import IsDict
+from fastapi import FastAPI, Form
+from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
+from typing_extensions import Annotated
+
+from .utils import needs_py310
+
+
+def get_client():
+    app = FastAPI()
+    with pytest.warns(DeprecationWarning):
+
+        @app.post("/items/")
+        async def read_items(
+            q: Annotated[str | None, Form(regex="^fixedquery$")] = None
+        ):
+            if q:
+                return f"Hello {q}"
+            else:
+                return "Hello World"
+
+    client = TestClient(app)
+    return client
+
+
+@needs_py310
+def test_no_query():
+    client = get_client()
+    response = client.post("/items/")
+    assert response.status_code == 200
+    assert response.json() == "Hello World"
+
+
+@needs_py310
+def test_q_fixedquery():
+    client = get_client()
+    response = client.post("/items/", data={"q": "fixedquery"})
+    assert response.status_code == 200
+    assert response.json() == "Hello fixedquery"
+
+
+@needs_py310
+def test_query_nonregexquery():
+    client = get_client()
+    response = client.post("/items/", data={"q": "nonregexquery"})
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "string_pattern_mismatch",
+                    "loc": ["body", "q"],
+                    "msg": "String should match pattern '^fixedquery$'",
+                    "input": "nonregexquery",
+                    "ctx": {"pattern": "^fixedquery$"},
+                    "url": match_pydantic_error_url("string_pattern_mismatch"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "ctx": {"pattern": "^fixedquery$"},
+                    "loc": ["body", "q"],
+                    "msg": 'string does not match regex "^fixedquery$"',
+                    "type": "value_error.str.regex",
+                }
+            ]
+        }
+    )
+
+
+@needs_py310
+def test_openapi_schema():
+    client = get_client()
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    # insert_assert(response.json())
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/": {
+                "post": {
+                    "summary": "Read Items",
+                    "operationId": "read_items_items__post",
+                    "requestBody": {
+                        "content": {
+                            "application/x-www-form-urlencoded": {
+                                "schema": IsDict(
+                                    {
+                                        "allOf": [
+                                            {
+                                                "$ref": "#/components/schemas/Body_read_items_items__post"
+                                            }
+                                        ],
+                                        "title": "Body",
+                                    }
+                                )
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "$ref": "#/components/schemas/Body_read_items_items__post"
+                                    }
+                                )
+                            }
+                        }
+                    },
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "Body_read_items_items__post": {
+                    "properties": {
+                        "q": IsDict(
+                            {
+                                "anyOf": [
+                                    {"type": "string", "pattern": "^fixedquery$"},
+                                    {"type": "null"},
+                                ],
+                                "title": "Q",
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"type": "string", "pattern": "^fixedquery$", "title": "Q"}
+                        )
+                    },
+                    "type": "object",
+                    "title": "Body_read_items_items__post",
+                },
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                            "type": "array",
+                            "title": "Detail",
+                        }
+                    },
+                    "type": "object",
+                    "title": "HTTPValidationError",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                            "type": "array",
+                            "title": "Location",
+                        },
+                        "msg": {"type": "string", "title": "Message"},
+                        "type": {"type": "string", "title": "Error Type"},
+                    },
+                    "type": "object",
+                    "required": ["loc", "msg", "type"],
+                    "title": "ValidationError",
+                },
+            }
+        },
+    }
diff --git a/tests/test_regex_deprecated_params.py b/tests/test_regex_deprecated_params.py
new file mode 100644 (file)
index 0000000..79a6533
--- /dev/null
@@ -0,0 +1,165 @@
+import pytest
+from dirty_equals import IsDict
+from fastapi import FastAPI, Query
+from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
+from typing_extensions import Annotated
+
+from .utils import needs_py310
+
+
+def get_client():
+    app = FastAPI()
+    with pytest.warns(DeprecationWarning):
+
+        @app.get("/items/")
+        async def read_items(
+            q: Annotated[str | None, Query(regex="^fixedquery$")] = None
+        ):
+            if q:
+                return f"Hello {q}"
+            else:
+                return "Hello World"
+
+    client = TestClient(app)
+    return client
+
+
+@needs_py310
+def test_query_params_str_validations_no_query():
+    client = get_client()
+    response = client.get("/items/")
+    assert response.status_code == 200
+    assert response.json() == "Hello World"
+
+
+@needs_py310
+def test_query_params_str_validations_q_fixedquery():
+    client = get_client()
+    response = client.get("/items/", params={"q": "fixedquery"})
+    assert response.status_code == 200
+    assert response.json() == "Hello fixedquery"
+
+
+@needs_py310
+def test_query_params_str_validations_item_query_nonregexquery():
+    client = get_client()
+    response = client.get("/items/", params={"q": "nonregexquery"})
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "string_pattern_mismatch",
+                    "loc": ["query", "q"],
+                    "msg": "String should match pattern '^fixedquery$'",
+                    "input": "nonregexquery",
+                    "ctx": {"pattern": "^fixedquery$"},
+                    "url": match_pydantic_error_url("string_pattern_mismatch"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "ctx": {"pattern": "^fixedquery$"},
+                    "loc": ["query", "q"],
+                    "msg": 'string does not match regex "^fixedquery$"',
+                    "type": "value_error.str.regex",
+                }
+            ]
+        }
+    )
+
+
+@needs_py310
+def test_openapi_schema():
+    client = get_client()
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    # insert_assert(response.json())
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/": {
+                "get": {
+                    "summary": "Read Items",
+                    "operationId": "read_items_items__get",
+                    "parameters": [
+                        {
+                            "name": "q",
+                            "in": "query",
+                            "required": False,
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [
+                                        {"type": "string", "pattern": "^fixedquery$"},
+                                        {"type": "null"},
+                                    ],
+                                    "title": "Q",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {
+                                    "type": "string",
+                                    "pattern": "^fixedquery$",
+                                    "title": "Q",
+                                }
+                            ),
+                        }
+                    ],
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                            "type": "array",
+                            "title": "Detail",
+                        }
+                    },
+                    "type": "object",
+                    "title": "HTTPValidationError",
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                            "type": "array",
+                            "title": "Location",
+                        },
+                        "msg": {"type": "string", "title": "Message"},
+                        "type": {"type": "string", "title": "Error Type"},
+                    },
+                    "type": "object",
+                    "required": ["loc", "msg", "type"],
+                    "title": "ValidationError",
+                },
+            }
+        },
+    }
index 8424bf551ed7639fb40cb502d5c5b42e9a2347cd..8c72fee54a4dfa3740428fa6904932892fea6abb 100644 (file)
@@ -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.1.0",
         "info": {"title": "FastAPI", "version": "0.1.0"},
index c3ff5b1d25a8511482e5c9b7ba6e8170c0a68064..e162cd39b5e854ba3f462b8c7fa268afb17d211b 100644 (file)
@@ -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)
index 7a0cf47ec4004c14694d2202b3f97eb032fb355a..85dd450eb07f4572c19b85d9631bad05e22c20f9 100644 (file)
@@ -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")
 
 
diff --git a/tests/test_response_model_data_filter.py b/tests/test_response_model_data_filter.py
new file mode 100644 (file)
index 0000000..a3e0f95
--- /dev/null
@@ -0,0 +1,81 @@
+from typing import List
+
+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="johndoe@example.com",
+        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="johndoe@example.com",
+        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": "johndoe@example.com", "password": "secret"}
+    )
+    assert response.json() == {"email": "johndoe@example.com"}
+
+
+def test_filter_second_level_model():
+    response = client.get("/pets/1")
+    assert response.json() == {
+        "name": "Nibbler",
+        "owner": {"email": "johndoe@example.com"},
+    }
+
+
+def test_list_of_models():
+    response = client.get("/pets/")
+    assert response.json() == [
+        {"name": "Nibbler", "owner": {"email": "johndoe@example.com"}},
+        {"name": "Zoidberg", "owner": {"email": "johndoe@example.com"}},
+    ]
diff --git a/tests/test_response_model_data_filter_no_inheritance.py b/tests/test_response_model_data_filter_no_inheritance.py
new file mode 100644 (file)
index 0000000..64003a8
--- /dev/null
@@ -0,0 +1,83 @@
+from typing import List
+
+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="johndoe@example.com",
+        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="johndoe@example.com",
+        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": "johndoe@example.com", "password": "secret"}
+    )
+    assert response.json() == {"email": "johndoe@example.com"}
+
+
+def test_filter_second_level_model():
+    response = client.get("/pets/1")
+    assert response.json() == {
+        "name": "Nibbler",
+        "owner": {"email": "johndoe@example.com"},
+    }
+
+
+def test_list_of_models():
+    response = client.get("/pets/")
+    assert response.json() == [
+        {"name": "Nibbler", "owner": {"email": "johndoe@example.com"}},
+        {"name": "Zoidberg", "owner": {"email": "johndoe@example.com"}},
+    ]
index 45caa16150ed7da89ccd91801334469f5af122dd..a1505afe23083053ac3e242f782049225b360825 100644 (file)
@@ -1,9 +1,11 @@
 from typing import Union
 
 import pytest
+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
 
 
 def create_app():
@@ -12,8 +14,14 @@ def create_app():
     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/")
     def schema_extra(item: Item):
@@ -333,14 +341,28 @@ def test_openapi_schema():
                     "requestBody": {
                         "content": {
                             "application/json": {
-                                "schema": {
-                                    "allOf": [{"$ref": "#/components/schemas/Item"}],
-                                    "title": "Item",
-                                    "examples": [
-                                        {"data": "Data in Body examples, example1"},
-                                        {"data": "Data in Body examples, example2"},
-                                    ],
-                                }
+                                "schema": IsDict(
+                                    {
+                                        "$ref": "#/components/schemas/Item",
+                                        "examples": [
+                                            {"data": "Data in Body examples, example1"},
+                                            {"data": "Data in Body examples, example2"},
+                                        ],
+                                    }
+                                )
+                                | IsDict(
+                                    # TODO: remove this when deprecating Pydantic v1
+                                    {
+                                        "allOf": [
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ],
+                                        "title": "Item",
+                                        "examples": [
+                                            {"data": "Data in Body examples, example1"},
+                                            {"data": "Data in Body examples, example2"},
+                                        ],
+                                    }
+                                )
                             }
                         },
                         "required": True,
@@ -370,14 +392,28 @@ def test_openapi_schema():
                     "requestBody": {
                         "content": {
                             "application/json": {
-                                "schema": {
-                                    "allOf": [{"$ref": "#/components/schemas/Item"}],
-                                    "title": "Item",
-                                    "examples": [
-                                        {"data": "examples example_examples 1"},
-                                        {"data": "examples example_examples 2"},
-                                    ],
-                                },
+                                "schema": IsDict(
+                                    {
+                                        "$ref": "#/components/schemas/Item",
+                                        "examples": [
+                                            {"data": "examples example_examples 1"},
+                                            {"data": "examples example_examples 2"},
+                                        ],
+                                    }
+                                )
+                                | IsDict(
+                                    # TODO: remove this when deprecating Pydantic v1
+                                    {
+                                        "allOf": [
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ],
+                                        "title": "Item",
+                                        "examples": [
+                                            {"data": "examples example_examples 1"},
+                                            {"data": "examples example_examples 2"},
+                                        ],
+                                    },
+                                ),
                                 "example": {"data": "Overridden example"},
                             }
                         },
@@ -508,7 +544,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",
@@ -539,11 +584,21 @@ def test_openapi_schema():
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {
-                                "type": "string",
-                                "title": "Data",
-                                "examples": ["query1", "query2"],
-                            },
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                    "title": "Data",
+                                    "examples": ["query1", "query2"],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: Remove this when deprecating Pydantic v1
+                                {
+                                    "type": "string",
+                                    "title": "Data",
+                                    "examples": ["query1", "query2"],
+                                }
+                            ),
                             "name": "data",
                             "in": "query",
                         }
@@ -573,11 +628,21 @@ def test_openapi_schema():
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {
-                                "type": "string",
-                                "title": "Data",
-                                "examples": ["query1", "query2"],
-                            },
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                    "title": "Data",
+                                    "examples": ["query1", "query2"],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: Remove this when deprecating Pydantic v1
+                                {
+                                    "type": "string",
+                                    "title": "Data",
+                                    "examples": ["query1", "query2"],
+                                }
+                            ),
                             "example": "query_overridden",
                             "name": "data",
                             "in": "query",
@@ -608,7 +673,16 @@ def test_openapi_schema():
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {"type": "string", "title": "Data"},
+                            "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",
@@ -639,11 +713,21 @@ def test_openapi_schema():
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {
-                                "type": "string",
-                                "title": "Data",
-                                "examples": ["header1", "header2"],
-                            },
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                    "title": "Data",
+                                    "examples": ["header1", "header2"],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: Remove this when deprecating Pydantic v1
+                                {
+                                    "type": "string",
+                                    "title": "Data",
+                                    "examples": ["header1", "header2"],
+                                }
+                            ),
                             "name": "data",
                             "in": "header",
                         }
@@ -673,11 +757,21 @@ def test_openapi_schema():
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {
-                                "type": "string",
-                                "title": "Data",
-                                "examples": ["header1", "header2"],
-                            },
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                    "title": "Data",
+                                    "examples": ["header1", "header2"],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: Remove this when deprecating Pydantic v1
+                                {
+                                    "title": "Data",
+                                    "type": "string",
+                                    "examples": ["header1", "header2"],
+                                }
+                            ),
                             "example": "header_overridden",
                             "name": "data",
                             "in": "header",
@@ -708,7 +802,16 @@ def test_openapi_schema():
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {"type": "string", "title": "Data"},
+                            "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",
@@ -739,11 +842,21 @@ def test_openapi_schema():
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {
-                                "type": "string",
-                                "title": "Data",
-                                "examples": ["cookie1", "cookie2"],
-                            },
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                    "title": "Data",
+                                    "examples": ["cookie1", "cookie2"],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: Remove this when deprecating Pydantic v1
+                                {
+                                    "title": "Data",
+                                    "type": "string",
+                                    "examples": ["cookie1", "cookie2"],
+                                }
+                            ),
                             "name": "data",
                             "in": "cookie",
                         }
@@ -773,11 +886,21 @@ def test_openapi_schema():
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {
-                                "type": "string",
-                                "title": "Data",
-                                "examples": ["cookie1", "cookie2"],
-                            },
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                    "title": "Data",
+                                    "examples": ["cookie1", "cookie2"],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: Remove this when deprecating Pydantic v1
+                                {
+                                    "title": "Data",
+                                    "type": "string",
+                                    "examples": ["cookie1", "cookie2"],
+                                }
+                            ),
                             "example": "cookie_overridden",
                             "name": "data",
                             "in": "cookie",
index 73d1b7d94cebf0ec68950578f2d44a1b9ef071b7..e98f80ebfbe03849d89ae4e0c9fb35636e78e86b 100644 (file)
@@ -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": {
index b24c1b58ff7a504e78b8918f9ff09a33690fec6b..d06c01bba0c7479b11615e3d197b7a3d6ed1fe8f 100644 (file)
@@ -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": {
index cda635151e451b943084c03382f819b21b7592aa..9287e4366e64364b8cb3d4ca089d7f3b637fa364 100644 (file)
@@ -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": {
index 181fff612ee7ff3e11498f780638f75b196f54f4..02765291cb7a7d6c0649ae6814b197b2a4c1d4fb 100644 (file)
@@ -12,7 +12,7 @@ class SubModel(BaseModel):
 
 
 class Model(BaseModel):
-    x: Optional[int]
+    x: Optional[int] = None
     sub: SubModel
 
 
index dce3ea5e2956d76b80c6890d6cf93535dbcb0a84..ed7f4efe8a36078a8553cec2c286e2ecfedb14b4 100644 (file)
@@ -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"},
                         },
index c37a25ca69bd706d7c2d98d6acb227d2c04da76c..ca33d2580c2f9fe1db051b7740b6c49b16e1bdcf 100644 (file)
@@ -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"}],
+                                }
+                            ),
                         }
                     },
                 },
index 8e084e152d69c14f5c2b8e08629085fe1f9684e6..588a3160a93e9adda7608e9dfe6981d6ebc88351 100644 (file)
@@ -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",
                         },
index 5fc8b81ca00298b618d45ce1da08081bfb8d01d0..55b556d8e19b87cfe6bdb777657813b11cef5abe 100644 (file)
@@ -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",
                         },
index 8126cdcc6cc015c55826061584ab0ba625db02f4..25d6df3e9af6885d39f8d09351f38ea84c11db4f 100644 (file)
@@ -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}
index 0ae9f4f9332f631504b499ab0139d29bf4a2d82c..ec17b4179001fb0475929933e4c37dddb0e247b9 100644 (file)
@@ -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",
             },
         ],
index 576a411a4a1d4aec71cc924ef12bbba02eaa9de6..2f8eb46995b29a557bafb317e223753aa0303f67 100644 (file)
@@ -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.1.0",
         "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",
             },
         ],
index 7da663435a274dbbc8efac88d44565f0ed5fc830..526e265a612a49aa7a12c918d0939e249dd5d1e6 100644 (file)
 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() == {
index 8f42d9dd1d9035052eecd327d465a10a7f6045e0..c0b77d4a722424b186a756fbd6c15ed95b4074c1 100644 (file)
 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() == {
index 44694e371ed57396efb827a7cfe2ced472024bca..948331b5ddfd364822cf58dcdd0fdbce331ce979 100644 (file)
@@ -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
index 469198e0ff10014a970b8b6ceef927562d380f50..2476b773f40704f8c89f41fe9f721d4d0ffcc6b3 100644 (file)
 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 a 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": "model_attributes_type",
+                    "loc": ["body"],
+                    "msg": "Input should be a valid dictionary or object to extract fields from",
+                    "input": "name=Foo&price=50.5",
+                    "url": match_pydantic_error_url("model_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": "model_attributes_type",
+                    "loc": ["body"],
+                    "msg": "Input should be a valid dictionary or object to extract fields from",
+                    "input": '{"name": "Foo", "price": 50.5}',
+                    "url": match_pydantic_error_url(
+                        "model_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": "model_attributes_type",
+                    "loc": ["body"],
+                    "msg": "Input should be a valid dictionary or object to extract fields from",
+                    "input": '{"name": "Foo", "price": 50.5}',
+                    "url": match_pydantic_error_url("model_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": "model_attributes_type",
+                    "loc": ["body"],
+                    "msg": "Input should be a valid dictionary or object to extract fields from",
+                    "input": '{"name": "Foo", "price": 50.5}',
+                    "url": match_pydantic_error_url("model_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": {
index a68b4e0446a9a5456634383ad136bc06c0ee047b..b64d860053515c982c1ae8898f2480b9dd7c6f10 100644 (file)
@@ -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 a 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": "model_attributes_type",
+                    "loc": ["body"],
+                    "msg": "Input should be a valid dictionary or object to extract fields from",
+                    "input": "name=Foo&price=50.5",
+                    "url": match_pydantic_error_url("model_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": "model_attributes_type",
+                    "loc": ["body"],
+                    "msg": "Input should be a valid dictionary or object to extract fields from",
+                    "input": '{"name": "Foo", "price": 50.5}',
+                    "url": match_pydantic_error_url("model_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": "model_attributes_type",
+                    "loc": ["body"],
+                    "msg": "Input should be a valid dictionary or object to extract fields from",
+                    "input": '{"name": "Foo", "price": 50.5}',
+                    "url": match_pydantic_error_url("model_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": "model_attributes_type",
+                    "loc": ["body"],
+                    "msg": "Input should be a valid dictionary or object to extract fields from",
+                    "input": '{"name": "Foo", "price": 50.5}',
+                    "url": match_pydantic_error_url("model_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": {
index 4999cbf6b5f3650990960afa4b91ad59b7b67f29..1ff2d95760b9b33a7fd94488e6d13dee856782f7 100644 (file)
@@ -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": {
index 011946d075876e3fe003f9c05499f1475dd78969..907d6842a42fea0e4718c9dcf40ac26d2d3742be 100644 (file)
@@ -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": {
index e7dcb54e9c86ea3b27aa33d565a93d6e03a47fd9..431d2d1819468429fcca2611561a3b8fa3927929 100644 (file)
@@ -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": {
index f1015a03b4695b22c56d24ac0bab4e69892e04c6..8cef6c154c9cc4ac935e04629424ff60b7d49086 100644 (file)
@@ -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": {
index 29c8ef4e9cfaade9e0a4159411f6842c0255ac67..b48cd9ec26d921f6675c4b6aa53f27adcd29cc3a 100644 (file)
@@ -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": {
index ce41a4283425f90d1c19551cb5e5817bb0e7dbd0..e5dc13b268e7d04c11685a5c0366f12f13d40436 100644 (file)
@@ -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": {
index acc4cfadc765298fc7f81c16fcec7365f949456f..51e8e3a4e9977ed1e8b7dc3c8c35fa2f6524c5ed 100644 (file)
@@ -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": {
index a8dc02a6c2e91d6ab4783c46f601f1a71c0d2b3d..8ac1f726185512aaa6291f94fa8c85b56911d138 100644 (file)
@@ -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": {
index f31fee78e4839a51d8143902e740e02526ac9e23..7ada42c528b54ef9942c7d1cf53a73335222e01b 100644 (file)
@@ -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": {
index 0e46df253f716aaede32e593c65e5eca7cec3d3b..0a832eaf6f0ffb4a33365a0837f9880cc1e08f5d 100644 (file)
@@ -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": {
index 8555cf88c5ad778afc93c02db51518e31b4e7d78..2046579a94c415a5a3298219a3fe1fe01eb00f23 100644 (file)
 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": {
index f4d300cc56c9c7f42277540f9bf85a483a4e195c..1282483e0732b447535b7e304c2d125be9e5c2a1 100644 (file)
 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": {
index afe2b2c20b608333699ec49213353c3403ce205a..577c079d00fbdf223e85313b8679e691db82a6e0 100644 (file)
@@ -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": {
index 033d5892e593c7c3da20962b8cea74e0f0a1ea5b..0ec04151ccf4c6169c3bf090d352d44f0496385b 100644 (file)
@@ -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": {
index 8fcc000138983251e9fedf5153bc6fe84fab2d9e..9caf5fe6cbe1307b98c74f79869a633445a6ba7f 100644 (file)
@@ -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": {
index ac39cd93f424845cfc85e2f85c94b44c892a441f..f4a76be4496baf1cd9ae13f19adb339ef8dbc75f 100644 (file)
@@ -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() == {
index 0800abe29b1b50c27f23009ca9997a16849e4902..8ab9bcac83b1a01d31041c14f8c6a6315d9a0269 100644 (file)
@@ -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
index 151b4b9176699d2d1aa91e8e0f529afc0bf3fc6c..b02f7c81c3f86b83edc162a50625864a0656914b 100644 (file)
@@ -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",
index c4b4b9df338b71c812a7f4132aec3fbbb7d71c53..4af2652a78e360cdcf21ddb19e85bf1514e9a2ba 100644 (file)
@@ -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",
index 940b4b3b8699f72531c7a20ee9b1412f86dfe004..832f45388443901c98fcde0a361f025345f1954b 100644 (file)
@@ -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",
index a43394ab16defd5ccf2db29217eae983fde7e75c..b098f259c2f1ae1fbbf0965237f4182f41d86d10 100644 (file)
@@ -2,13 +2,23 @@ import importlib
 
 from fastapi.testclient import TestClient
 
-from docs_src.conditional_openapi import tutorial001
+from ...utils import needs_pydanticv2
 
 
-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
+
+
+@needs_pydanticv2
+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 +27,17 @@ def test_disable_openapi(monkeypatch):
     assert response.status_code == 404, response.text
 
 
+@needs_pydanticv2
 def test_root():
-    client = TestClient(tutorial001.app)
+    client = get_client()
     response = client.get("/")
     assert response.status_code == 200
     assert response.json() == {"message": "Hello World"}
 
 
+@needs_pydanticv2
 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")
index 902bed843bb0372dc212faf4140be5ac239d9c6a..7d0e669abafd125daf8c0881abcfb07554ef5b5f 100644 (file)
@@ -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",
                         }
index aa5807844f281a31d416e1e2845f777c06795e9c..2505876c87ec96948ce62cafaa94f452ed7e94dd 100644 (file)
@@ -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",
                         }
index ffb55d4e15c4026f7330fcfdf640f6b3c3930d46..108f78b9c839a78849f3b834fe96140fa3b808c8 100644 (file)
@@ -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",
                         }
index 9bc38effdee7fa9e4409763d7b60f588ca256427..8126a105236ccbea6151cdd6c0c1b3ebcc2272ce 100644 (file)
@@ -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",
                         }
index bb2953ef61a4126a16d7d508eff9189dc8ccb311..6711fa5818d1f10df37dd9bbc1fdea1042303753 100644 (file)
@@ -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",
                         }
index d2d27f8a274bd48aee63451ecbbed1d5eb12a11f..ad142ec887fae29b513b0f1782453db18d946afb 100644 (file)
@@ -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",
+                    }
+                ],
+            }
         }
-    }
+    )
index e20c0efe95c608844ca0d5950de1df65f2e70b8c..9f1200f373c912196f651d824569065c920be8c4 100644 (file)
@@ -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 a 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": {
index e122239d85044090b4883d047d15342950cc6816..7d88e286168c45c846fc71945a9a3e3024c60593 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.dataclasses.tutorial002 import app
@@ -51,13 +52,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"}
+                        ),
                     },
                 }
             }
index 204426e8bca80f24eb01bae87c700913c47b70dd..597757e0931c9d86c6f8181bb9aa6316ab1f8739 100644 (file)
@@ -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": {
index a8e564ebe6136de316f1fb6c602f54283ea4180c..d1324a64113ed1e2f4ae574d0cb4f3a1b3deb5be 100644 (file)
@@ -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",
                         },
index 4e6a329f44bc3e8f568c3bf7cc44e69db79d622d..79c2a1e10e495a6ebdc600b34a67db121ba88dda 100644 (file)
@@ -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",
                         },
index 205aee908a3a1bfecb3ec640b306926e387ad57a..7db55a1c55a5eff9ff6682c0e33a9bc6a77868b1 100644 (file)
@@ -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",
                         },
index 73593ea55ecab874909c5fea50b7b79f2fdba348..68c2dedb1ba87440bc85fd459d8e00ed97712299 100644 (file)
@@ -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",
                         },
index 10bf84fb53e17fde718d5e0cc824e05a0ebbcdcc..381eecb63911cc75d2bc3cae1c9a36837cf036d3 100644 (file)
@@ -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",
                         },
index d16fd9ef707d8b641010ca41c272e0ea9dcef335..5c5d34cfcc6df28945922ab9755bb0035314142a 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.dependencies.tutorial004 import app
@@ -90,7 +91,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",
                         },
index 46fe97fb2c731933d2c79eaa17dc38d42be62c11..c5c1a1fb883c159ad8db11145c8391d26cec8f62 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.dependencies.tutorial004_an import app
@@ -90,7 +91,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",
                         },
index c6a0fc6656090b07e61a797419c21c9a179c8142..6fd093ddb14cb3ab4c95ac63c129b41e3c0a57bf 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -98,7 +99,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",
                         },
index 30431cd293c37588e02a7afda861db681bb910f4..fbbe84cc9483da1388da6452d34d6c9a6f12fe1f 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py39
@@ -98,7 +99,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",
                         },
index 9793c8c331abbf461f2d3332c1a1d613eb297fed..845b098e79a33cd115f93bc7d920b0c14dfb2dfc 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -98,7 +99,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",
                         },
index 6fac9f8eb3938eb7e91fa0be31fa279d75ca3519..704e389a5bbfcdeadeb025a56c12438aa7f3a344 100644 (file)
@@ -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.dependencies.tutorial006 import app
 
@@ -8,20 +10,42 @@ client = TestClient(app)
 def test_get_no_headers():
     response = client.get("/items/")
     assert response.status_code == 422, response.text
-    assert response.json() == {
-        "detail": [
-            {
-                "loc": ["header", "x-token"],
-                "msg": "field required",
-                "type": "value_error.missing",
-            },
-            {
-                "loc": ["header", "x-key"],
-                "msg": "field required",
-                "type": "value_error.missing",
-            },
-        ]
-    }
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["header", "x-token"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["header", "x-key"],
+                    "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",
+                },
+                {
+                    "loc": ["header", "x-key"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
 def test_get_invalid_one_header():
index 810537e48625323a5991625305bb484662c7cb4a..5034fceba5e259e132020962e6c01402cd619d04 100644 (file)
@@ -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.dependencies.tutorial006_an import app
 
@@ -8,20 +10,42 @@ client = TestClient(app)
 def test_get_no_headers():
     response = client.get("/items/")
     assert response.status_code == 422, response.text
-    assert response.json() == {
-        "detail": [
-            {
-                "loc": ["header", "x-token"],
-                "msg": "field required",
-                "type": "value_error.missing",
-            },
-            {
-                "loc": ["header", "x-key"],
-                "msg": "field required",
-                "type": "value_error.missing",
-            },
-        ]
-    }
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["header", "x-token"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["header", "x-key"],
+                    "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",
+                },
+                {
+                    "loc": ["header", "x-key"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
 def test_get_invalid_one_header():
index f17cbcfc720d0b04a6736308d172c50b2db732d9..3fc22dd3c2e6f0dc7f38d9ac246525eb8a68c1fa 100644 (file)
@@ -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
 
@@ -16,20 +18,42 @@ def get_client():
 def test_get_no_headers(client: TestClient):
     response = client.get("/items/")
     assert response.status_code == 422, response.text
-    assert response.json() == {
-        "detail": [
-            {
-                "loc": ["header", "x-token"],
-                "msg": "field required",
-                "type": "value_error.missing",
-            },
-            {
-                "loc": ["header", "x-key"],
-                "msg": "field required",
-                "type": "value_error.missing",
-            },
-        ]
-    }
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["header", "x-token"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["header", "x-key"],
+                    "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",
+                },
+                {
+                    "loc": ["header", "x-key"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
 @needs_py39
index af1fcde55e06e6b9099f05b4b5f5ff2ac28a964d..753e62e43e408ed667bc3658f91be392efc5095a 100644 (file)
@@ -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.dependencies.tutorial012 import app
 
@@ -8,39 +10,83 @@ client = TestClient(app)
 def test_get_no_headers_items():
     response = client.get("/items/")
     assert response.status_code == 422, response.text
-    assert response.json() == {
-        "detail": [
-            {
-                "loc": ["header", "x-token"],
-                "msg": "field required",
-                "type": "value_error.missing",
-            },
-            {
-                "loc": ["header", "x-key"],
-                "msg": "field required",
-                "type": "value_error.missing",
-            },
-        ]
-    }
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["header", "x-token"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["header", "x-key"],
+                    "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",
+                },
+                {
+                    "loc": ["header", "x-key"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
 def test_get_no_headers_users():
     response = client.get("/users/")
     assert response.status_code == 422, response.text
-    assert response.json() == {
-        "detail": [
-            {
-                "loc": ["header", "x-token"],
-                "msg": "field required",
-                "type": "value_error.missing",
-            },
-            {
-                "loc": ["header", "x-key"],
-                "msg": "field required",
-                "type": "value_error.missing",
-            },
-        ]
-    }
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["header", "x-token"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["header", "x-key"],
+                    "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",
+                },
+                {
+                    "loc": ["header", "x-key"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
 def test_get_invalid_one_header_items():
index c33d51d873a551b30a7b504ffc0a67975827bb3c..4157d46128bf689e761e10379be52dce38856380 100644 (file)
@@ -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.dependencies.tutorial012_an import app
 
@@ -8,39 +10,83 @@ client = TestClient(app)
 def test_get_no_headers_items():
     response = client.get("/items/")
     assert response.status_code == 422, response.text
-    assert response.json() == {
-        "detail": [
-            {
-                "loc": ["header", "x-token"],
-                "msg": "field required",
-                "type": "value_error.missing",
-            },
-            {
-                "loc": ["header", "x-key"],
-                "msg": "field required",
-                "type": "value_error.missing",
-            },
-        ]
-    }
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["header", "x-token"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["header", "x-key"],
+                    "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",
+                },
+                {
+                    "loc": ["header", "x-key"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
 def test_get_no_headers_users():
     response = client.get("/users/")
     assert response.status_code == 422, response.text
-    assert response.json() == {
-        "detail": [
-            {
-                "loc": ["header", "x-token"],
-                "msg": "field required",
-                "type": "value_error.missing",
-            },
-            {
-                "loc": ["header", "x-key"],
-                "msg": "field required",
-                "type": "value_error.missing",
-            },
-        ]
-    }
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["header", "x-token"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["header", "x-key"],
+                    "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",
+                },
+                {
+                    "loc": ["header", "x-key"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
 def test_get_invalid_one_header_items():
index d7bd756b5ff2515ee97d3d835e93c23b9422a706..9e46758cbdd76783bd3858854ee384802360bc83 100644 (file)
@@ -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
 
@@ -16,40 +18,84 @@ def get_client():
 def test_get_no_headers_items(client: TestClient):
     response = client.get("/items/")
     assert response.status_code == 422, response.text
-    assert response.json() == {
-        "detail": [
-            {
-                "loc": ["header", "x-token"],
-                "msg": "field required",
-                "type": "value_error.missing",
-            },
-            {
-                "loc": ["header", "x-key"],
-                "msg": "field required",
-                "type": "value_error.missing",
-            },
-        ]
-    }
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["header", "x-token"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["header", "x-key"],
+                    "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",
+                },
+                {
+                    "loc": ["header", "x-key"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
 @needs_py39
 def test_get_no_headers_users(client: TestClient):
     response = client.get("/users/")
     assert response.status_code == 422, response.text
-    assert response.json() == {
-        "detail": [
-            {
-                "loc": ["header", "x-token"],
-                "msg": "field required",
-                "type": "value_error.missing",
-            },
-            {
-                "loc": ["header", "x-key"],
-                "msg": "field required",
-                "type": "value_error.missing",
-            },
-        ]
-    }
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["header", "x-token"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["header", "x-key"],
+                    "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",
+                },
+                {
+                    "loc": ["header", "x-key"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
 @needs_py39
index 39d2005ab781f0e871dd5b8af90a82988b49994c..7710446ce34d9158b060b924aaa19b71b7c92b06 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.extra_data_types.tutorial001 import app
@@ -68,9 +69,22 @@ def test_openapi_schema():
                     "requestBody": {
                         "content": {
                             "application/json": {
-                                "schema": {
-                                    "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
-                                }
+                                "schema": IsDict(
+                                    {
+                                        "allOf": [
+                                            {
+                                                "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
+                                            }
+                                        ],
+                                        "title": "Body",
+                                    }
+                                )
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
+                                    }
+                                )
                             }
                         }
                     },
@@ -83,26 +97,74 @@ def test_openapi_schema():
                     "title": "Body_read_items_items__item_id__put",
                     "type": "object",
                     "properties": {
-                        "start_datetime": {
-                            "title": "Start Datetime",
-                            "type": "string",
-                            "format": "date-time",
-                        },
-                        "end_datetime": {
-                            "title": "End Datetime",
-                            "type": "string",
-                            "format": "date-time",
-                        },
-                        "repeat_at": {
-                            "title": "Repeat At",
-                            "type": "string",
-                            "format": "time",
-                        },
-                        "process_after": {
-                            "title": "Process After",
-                            "type": "number",
-                            "format": "time-delta",
-                        },
+                        "start_datetime": IsDict(
+                            {
+                                "title": "Start Datetime",
+                                "anyOf": [
+                                    {"type": "string", "format": "date-time"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Start Datetime",
+                                "type": "string",
+                                "format": "date-time",
+                            }
+                        ),
+                        "end_datetime": IsDict(
+                            {
+                                "title": "End Datetime",
+                                "anyOf": [
+                                    {"type": "string", "format": "date-time"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "End Datetime",
+                                "type": "string",
+                                "format": "date-time",
+                            }
+                        ),
+                        "repeat_at": IsDict(
+                            {
+                                "title": "Repeat At",
+                                "anyOf": [
+                                    {"type": "string", "format": "time"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Repeat At",
+                                "type": "string",
+                                "format": "time",
+                            }
+                        ),
+                        "process_after": IsDict(
+                            {
+                                "title": "Process After",
+                                "anyOf": [
+                                    {"type": "string", "format": "duration"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Process After",
+                                "type": "number",
+                                "format": "time-delta",
+                            }
+                        ),
                     },
                 },
                 "ValidationError": {
index 3e497a291b53ead0e2438b2241e35ca1d290a951..9951b3b51173a3d8db76c46e79b1ef019a10e3e0 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.extra_data_types.tutorial001_an import app
@@ -68,9 +69,22 @@ def test_openapi_schema():
                     "requestBody": {
                         "content": {
                             "application/json": {
-                                "schema": {
-                                    "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
-                                }
+                                "schema": IsDict(
+                                    {
+                                        "allOf": [
+                                            {
+                                                "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
+                                            }
+                                        ],
+                                        "title": "Body",
+                                    }
+                                )
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
+                                    }
+                                )
                             }
                         }
                     },
@@ -83,26 +97,74 @@ def test_openapi_schema():
                     "title": "Body_read_items_items__item_id__put",
                     "type": "object",
                     "properties": {
-                        "start_datetime": {
-                            "title": "Start Datetime",
-                            "type": "string",
-                            "format": "date-time",
-                        },
-                        "end_datetime": {
-                            "title": "End Datetime",
-                            "type": "string",
-                            "format": "date-time",
-                        },
-                        "repeat_at": {
-                            "title": "Repeat At",
-                            "type": "string",
-                            "format": "time",
-                        },
-                        "process_after": {
-                            "title": "Process After",
-                            "type": "number",
-                            "format": "time-delta",
-                        },
+                        "start_datetime": IsDict(
+                            {
+                                "title": "Start Datetime",
+                                "anyOf": [
+                                    {"type": "string", "format": "date-time"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Start Datetime",
+                                "type": "string",
+                                "format": "date-time",
+                            }
+                        ),
+                        "end_datetime": IsDict(
+                            {
+                                "title": "End Datetime",
+                                "anyOf": [
+                                    {"type": "string", "format": "date-time"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "End Datetime",
+                                "type": "string",
+                                "format": "date-time",
+                            }
+                        ),
+                        "repeat_at": IsDict(
+                            {
+                                "title": "Repeat At",
+                                "anyOf": [
+                                    {"type": "string", "format": "time"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Repeat At",
+                                "type": "string",
+                                "format": "time",
+                            }
+                        ),
+                        "process_after": IsDict(
+                            {
+                                "title": "Process After",
+                                "anyOf": [
+                                    {"type": "string", "format": "duration"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Process After",
+                                "type": "number",
+                                "format": "time-delta",
+                            }
+                        ),
                     },
                 },
                 "ValidationError": {
index b539cf3d65f79de2636674bd9d27cd501b8bdaab..7c482b8cb2384c8d0241b066e9a33224d17746f7 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -77,9 +78,22 @@ def test_openapi_schema(client: TestClient):
                     "requestBody": {
                         "content": {
                             "application/json": {
-                                "schema": {
-                                    "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
-                                }
+                                "schema": IsDict(
+                                    {
+                                        "allOf": [
+                                            {
+                                                "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
+                                            }
+                                        ],
+                                        "title": "Body",
+                                    }
+                                )
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
+                                    }
+                                )
                             }
                         }
                     },
@@ -92,26 +106,74 @@ def test_openapi_schema(client: TestClient):
                     "title": "Body_read_items_items__item_id__put",
                     "type": "object",
                     "properties": {
-                        "start_datetime": {
-                            "title": "Start Datetime",
-                            "type": "string",
-                            "format": "date-time",
-                        },
-                        "end_datetime": {
-                            "title": "End Datetime",
-                            "type": "string",
-                            "format": "date-time",
-                        },
-                        "repeat_at": {
-                            "title": "Repeat At",
-                            "type": "string",
-                            "format": "time",
-                        },
-                        "process_after": {
-                            "title": "Process After",
-                            "type": "number",
-                            "format": "time-delta",
-                        },
+                        "start_datetime": IsDict(
+                            {
+                                "title": "Start Datetime",
+                                "anyOf": [
+                                    {"type": "string", "format": "date-time"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Start Datetime",
+                                "type": "string",
+                                "format": "date-time",
+                            }
+                        ),
+                        "end_datetime": IsDict(
+                            {
+                                "title": "End Datetime",
+                                "anyOf": [
+                                    {"type": "string", "format": "date-time"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "End Datetime",
+                                "type": "string",
+                                "format": "date-time",
+                            }
+                        ),
+                        "repeat_at": IsDict(
+                            {
+                                "title": "Repeat At",
+                                "anyOf": [
+                                    {"type": "string", "format": "time"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Repeat At",
+                                "type": "string",
+                                "format": "time",
+                            }
+                        ),
+                        "process_after": IsDict(
+                            {
+                                "title": "Process After",
+                                "anyOf": [
+                                    {"type": "string", "format": "duration"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Process After",
+                                "type": "number",
+                                "format": "time-delta",
+                            }
+                        ),
                     },
                 },
                 "ValidationError": {
index efd31e63d7ced23ec4e9329b9600ad1b763b739d..87473867b02fcf2efdb61150265037de2d811cf3 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py39
@@ -77,9 +78,22 @@ def test_openapi_schema(client: TestClient):
                     "requestBody": {
                         "content": {
                             "application/json": {
-                                "schema": {
-                                    "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
-                                }
+                                "schema": IsDict(
+                                    {
+                                        "allOf": [
+                                            {
+                                                "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
+                                            }
+                                        ],
+                                        "title": "Body",
+                                    }
+                                )
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
+                                    }
+                                )
                             }
                         }
                     },
@@ -92,26 +106,74 @@ def test_openapi_schema(client: TestClient):
                     "title": "Body_read_items_items__item_id__put",
                     "type": "object",
                     "properties": {
-                        "start_datetime": {
-                            "title": "Start Datetime",
-                            "type": "string",
-                            "format": "date-time",
-                        },
-                        "end_datetime": {
-                            "title": "End Datetime",
-                            "type": "string",
-                            "format": "date-time",
-                        },
-                        "repeat_at": {
-                            "title": "Repeat At",
-                            "type": "string",
-                            "format": "time",
-                        },
-                        "process_after": {
-                            "title": "Process After",
-                            "type": "number",
-                            "format": "time-delta",
-                        },
+                        "start_datetime": IsDict(
+                            {
+                                "title": "Start Datetime",
+                                "anyOf": [
+                                    {"type": "string", "format": "date-time"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Start Datetime",
+                                "type": "string",
+                                "format": "date-time",
+                            }
+                        ),
+                        "end_datetime": IsDict(
+                            {
+                                "title": "End Datetime",
+                                "anyOf": [
+                                    {"type": "string", "format": "date-time"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "End Datetime",
+                                "type": "string",
+                                "format": "date-time",
+                            }
+                        ),
+                        "repeat_at": IsDict(
+                            {
+                                "title": "Repeat At",
+                                "anyOf": [
+                                    {"type": "string", "format": "time"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Repeat At",
+                                "type": "string",
+                                "format": "time",
+                            }
+                        ),
+                        "process_after": IsDict(
+                            {
+                                "title": "Process After",
+                                "anyOf": [
+                                    {"type": "string", "format": "duration"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Process After",
+                                "type": "number",
+                                "format": "time-delta",
+                            }
+                        ),
                     },
                 },
                 "ValidationError": {
index 733d9f4060b3d4e59b76848a5ce66b3913a34ccb..0b71d9177332979a661b3a344e8e023d92e4cb4e 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -77,9 +78,22 @@ def test_openapi_schema(client: TestClient):
                     "requestBody": {
                         "content": {
                             "application/json": {
-                                "schema": {
-                                    "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
-                                }
+                                "schema": IsDict(
+                                    {
+                                        "allOf": [
+                                            {
+                                                "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
+                                            }
+                                        ],
+                                        "title": "Body",
+                                    }
+                                )
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "$ref": "#/components/schemas/Body_read_items_items__item_id__put"
+                                    }
+                                )
                             }
                         }
                     },
@@ -92,26 +106,74 @@ def test_openapi_schema(client: TestClient):
                     "title": "Body_read_items_items__item_id__put",
                     "type": "object",
                     "properties": {
-                        "start_datetime": {
-                            "title": "Start Datetime",
-                            "type": "string",
-                            "format": "date-time",
-                        },
-                        "end_datetime": {
-                            "title": "End Datetime",
-                            "type": "string",
-                            "format": "date-time",
-                        },
-                        "repeat_at": {
-                            "title": "Repeat At",
-                            "type": "string",
-                            "format": "time",
-                        },
-                        "process_after": {
-                            "title": "Process After",
-                            "type": "number",
-                            "format": "time-delta",
-                        },
+                        "start_datetime": IsDict(
+                            {
+                                "title": "Start Datetime",
+                                "anyOf": [
+                                    {"type": "string", "format": "date-time"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Start Datetime",
+                                "type": "string",
+                                "format": "date-time",
+                            }
+                        ),
+                        "end_datetime": IsDict(
+                            {
+                                "title": "End Datetime",
+                                "anyOf": [
+                                    {"type": "string", "format": "date-time"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "End Datetime",
+                                "type": "string",
+                                "format": "date-time",
+                            }
+                        ),
+                        "repeat_at": IsDict(
+                            {
+                                "title": "Repeat At",
+                                "anyOf": [
+                                    {"type": "string", "format": "time"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Repeat At",
+                                "type": "string",
+                                "format": "time",
+                            }
+                        ),
+                        "process_after": IsDict(
+                            {
+                                "title": "Process After",
+                                "anyOf": [
+                                    {"type": "string", "format": "duration"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Process After",
+                                "type": "number",
+                                "format": "time-delta",
+                            }
+                        ),
                     },
                 },
                 "ValidationError": {
index 0c0988c6431af6c87ef7e9e3b0a161e4a0cafb12..217159a59685e9311f5b822598297668a3d6546a 100644 (file)
@@ -8,12 +8,18 @@ client = TestClient(app)
 def test_get_validation_error():
     response = client.get("/items/foo")
     assert response.status_code == 400, response.text
-    validation_error_str_lines = [
-        b"1 validation error for Request",
-        b"path -> item_id",
-        b"  value is not a valid integer (type=type_error.integer)",
-    ]
-    assert response.content == b"\n".join(validation_error_str_lines)
+    # TODO: remove when deprecating Pydantic v1
+    assert (
+        # TODO: remove when deprecating Pydantic v1
+        "path -> item_id" in response.text
+        or "'loc': ('path', 'item_id')" in response.text
+    )
+    assert (
+        # TODO: remove when deprecating Pydantic v1
+        "value is not a valid integer" in response.text
+        or "Input should be a valid integer, unable to parse string as an integer"
+        in response.text
+    )
 
 
 def test_get_http_error():
index f356178ac7c0dc50f554ddbe39d31c06346da357..494c317cabe62ffe3b17a68d227db106adc4e67e 100644 (file)
@@ -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.handling_errors.tutorial005 import app
 
@@ -8,16 +10,32 @@ client = TestClient(app)
 def test_post_validation_error():
     response = client.post("/items/", json={"title": "towel", "size": "XL"})
     assert response.status_code == 422, response.text
-    assert response.json() == {
-        "detail": [
-            {
-                "loc": ["body", "size"],
-                "msg": "value is not a valid integer",
-                "type": "type_error.integer",
-            }
-        ],
-        "body": {"title": "towel", "size": "XL"},
-    }
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "int_parsing",
+                    "loc": ["body", "size"],
+                    "msg": "Input should be a valid integer, unable to parse string as an integer",
+                    "input": "XL",
+                    "url": match_pydantic_error_url("int_parsing"),
+                }
+            ],
+            "body": {"title": "towel", "size": "XL"},
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "size"],
+                    "msg": "value is not a valid integer",
+                    "type": "type_error.integer",
+                }
+            ],
+            "body": {"title": "towel", "size": "XL"},
+        }
+    )
 
 
 def test_post():
index 4dd1adf43ee9e51256d95defd6b3b630eeb17c6a..cc2b496a839afdde16dfd3db9b3aa3a06f54948d 100644 (file)
@@ -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.handling_errors.tutorial006 import app
 
@@ -8,15 +10,30 @@ client = TestClient(app)
 def test_get_validation_error():
     response = client.get("/items/foo")
     assert response.status_code == 422, response.text
-    assert response.json() == {
-        "detail": [
-            {
-                "loc": ["path", "item_id"],
-                "msg": "value is not a valid integer",
-                "type": "type_error.integer",
-            }
-        ]
-    }
+    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_get_http_error():
index 030159dcf02ea7a06922dbd7b655bf69aa44f7ba..746fc05024f17770bc3029c582d91c1bc47a68dd 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.header_params.tutorial001 import app
@@ -20,7 +21,7 @@ def test(path, headers, expected_status, expected_response):
     assert response.json() == expected_response
 
 
-def test_openapi():
+def test_openapi_schema():
     response = client.get("/openapi.json")
     assert response.status_code == 200
     assert response.json() == {
@@ -50,7 +51,16 @@ def test_openapi():
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {"title": "User-Agent", "type": "string"},
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                    "title": "User-Agent",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "User-Agent", "type": "string"}
+                            ),
                             "name": "user-agent",
                             "in": "header",
                         }
index 3755ab758ca1863c4b5390710ac9de99c8a5a09f..a715228aa298bef322fc1128c54a5e421b6fbabe 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.header_params.tutorial001_an import app
@@ -50,7 +51,16 @@ def test_openapi_schema():
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {"title": "User-Agent", "type": "string"},
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                    "title": "User-Agent",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "User-Agent", "type": "string"}
+                            ),
                             "name": "user-agent",
                             "in": "header",
                         }
index 207b3b02b0954f7608d9e468d2b188f862507ac9..caf85bc6ca157462f07edeb003d9c4f62ec6418a 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -58,7 +59,16 @@ def test_openapi_schema(client: TestClient):
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {"title": "User-Agent", "type": "string"},
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                    "title": "User-Agent",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "User-Agent", "type": "string"}
+                            ),
                             "name": "user-agent",
                             "in": "header",
                         }
index bf51982b7eeb4d1351084bf98338edb8939a123e..57e0a296af00e752e38585a936eeea9e30b74956 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -58,7 +59,16 @@ def test_openapi_schema(client: TestClient):
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {"title": "User-Agent", "type": "string"},
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                    "title": "User-Agent",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "User-Agent", "type": "string"}
+                            ),
                             "name": "user-agent",
                             "in": "header",
                         }
index 545fc836bccc19813bb1e277d5c119bc13881cfa..78bac838cfe6a454cfc2f8e9ccf70d6f5b6ce378 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.header_params.tutorial002 import app
@@ -61,7 +62,16 @@ def test_openapi_schema():
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {"title": "Strange Header", "type": "string"},
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                    "title": "Strange Header",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Strange Header", "type": "string"}
+                            ),
                             "name": "strange_header",
                             "in": "header",
                         }
index cfd581e33f98f29fe52d70db636997758205e588..ffda8158fc32602e9346dfe0ab21ae9001e732a1 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.header_params.tutorial002_an import app
@@ -61,7 +62,16 @@ def test_openapi_schema():
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {"title": "Strange Header", "type": "string"},
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                    "title": "Strange Header",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Strange Header", "type": "string"}
+                            ),
                             "name": "strange_header",
                             "in": "header",
                         }
index c8d61e42ec34f20030aeab5c43fa5f2dbb79ab17..6f332f3bac7a8c5c1e98334903ffc59775fee455 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -69,7 +70,16 @@ def test_openapi_schema(client: TestClient):
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {"title": "Strange Header", "type": "string"},
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                    "title": "Strange Header",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Strange Header", "type": "string"}
+                            ),
                             "name": "strange_header",
                             "in": "header",
                         }
index 85150d4a9e8e217e3682e237174e326cca454e07..8202bc671eba5fa271931e586429a147ce3a897e 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py39
@@ -72,7 +73,16 @@ def test_openapi_schema():
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {"title": "Strange Header", "type": "string"},
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                    "title": "Strange Header",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Strange Header", "type": "string"}
+                            ),
                             "name": "strange_header",
                             "in": "header",
                         }
index f189d85b5602b39d3295ec5493824cc1f85e5eff..c113ed23e12bb10e3f897ffd67b2e56ba5ac90c0 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -72,7 +73,16 @@ def test_openapi_schema():
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {"title": "Strange Header", "type": "string"},
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [{"type": "string"}, {"type": "null"}],
+                                    "title": "Strange Header",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Strange Header", "type": "string"}
+                            ),
                             "name": "strange_header",
                             "in": "header",
                         }
index b2fc17b8faee27be6517e3ce2d4ab657e5942b40..268df7a3e9a808045b6f9a5065d61a1a5791ec97 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.header_params.tutorial003 import app
@@ -24,7 +25,6 @@ def test(path, headers, expected_status, expected_response):
 def test_openapi_schema():
     response = client.get("/openapi.json")
     assert response.status_code == 200
-    # insert_assert(response.json())
     assert response.json() == {
         "openapi": "3.1.0",
         "info": {"title": "FastAPI", "version": "0.1.0"},
@@ -36,11 +36,23 @@ def test_openapi_schema():
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {
-                                "title": "X-Token",
-                                "type": "array",
-                                "items": {"type": "string"},
-                            },
+                            "schema": IsDict(
+                                {
+                                    "title": "X-Token",
+                                    "anyOf": [
+                                        {"type": "array", "items": {"type": "string"}},
+                                        {"type": "null"},
+                                    ],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {
+                                    "title": "X-Token",
+                                    "type": "array",
+                                    "items": {"type": "string"},
+                                }
+                            ),
                             "name": "x-token",
                             "in": "header",
                         }
index 87fa839e2def32223c5416c402926f98a22995be..742ed41f489f00ffe420a102272a375ba1c52e05 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.header_params.tutorial003_an import app
@@ -24,7 +25,6 @@ def test(path, headers, expected_status, expected_response):
 def test_openapi_schema():
     response = client.get("/openapi.json")
     assert response.status_code == 200
-    # insert_assert(response.json())
     assert response.json() == {
         "openapi": "3.1.0",
         "info": {"title": "FastAPI", "version": "0.1.0"},
@@ -36,11 +36,23 @@ def test_openapi_schema():
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {
-                                "title": "X-Token",
-                                "type": "array",
-                                "items": {"type": "string"},
-                            },
+                            "schema": IsDict(
+                                {
+                                    "title": "X-Token",
+                                    "anyOf": [
+                                        {"type": "array", "items": {"type": "string"}},
+                                        {"type": "null"},
+                                    ],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {
+                                    "title": "X-Token",
+                                    "type": "array",
+                                    "items": {"type": "string"},
+                                }
+                            ),
                             "name": "x-token",
                             "in": "header",
                         }
index ef6c268c5ab38f335eff3fbcd72115f04ee96610..fdac4a416cec581ab310cc1c0f25c06bee283053 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -32,7 +33,6 @@ def test(path, headers, expected_status, expected_response, client: TestClient):
 def test_openapi_schema(client: TestClient):
     response = client.get("/openapi.json")
     assert response.status_code == 200
-    # insert_assert(response.json())
     assert response.json() == {
         "openapi": "3.1.0",
         "info": {"title": "FastAPI", "version": "0.1.0"},
@@ -44,11 +44,23 @@ def test_openapi_schema(client: TestClient):
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {
-                                "title": "X-Token",
-                                "type": "array",
-                                "items": {"type": "string"},
-                            },
+                            "schema": IsDict(
+                                {
+                                    "title": "X-Token",
+                                    "anyOf": [
+                                        {"type": "array", "items": {"type": "string"}},
+                                        {"type": "null"},
+                                    ],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {
+                                    "title": "X-Token",
+                                    "type": "array",
+                                    "items": {"type": "string"},
+                                }
+                            ),
                             "name": "x-token",
                             "in": "header",
                         }
index 6525fd50c3c16e184c9f67b1139555d19af0acc2..c50543cc88d010eaeba40e70b7dbc9b9a61099ca 100644 (file)
@@ -1,7 +1,8 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
-from ...utils import needs_py310
+from ...utils import needs_py39
 
 
 @pytest.fixture(name="client")
@@ -12,7 +13,7 @@ def get_client():
     return client
 
 
-@needs_py310
+@needs_py39
 @pytest.mark.parametrize(
     "path,headers,expected_status,expected_response",
     [
@@ -28,11 +29,10 @@ def test(path, headers, expected_status, expected_response, client: TestClient):
     assert response.json() == expected_response
 
 
-@needs_py310
+@needs_py39
 def test_openapi_schema(client: TestClient):
     response = client.get("/openapi.json")
     assert response.status_code == 200
-    # insert_assert(response.json())
     assert response.json() == {
         "openapi": "3.1.0",
         "info": {"title": "FastAPI", "version": "0.1.0"},
@@ -44,11 +44,23 @@ def test_openapi_schema(client: TestClient):
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {
-                                "title": "X-Token",
-                                "type": "array",
-                                "items": {"type": "string"},
-                            },
+                            "schema": IsDict(
+                                {
+                                    "title": "X-Token",
+                                    "anyOf": [
+                                        {"type": "array", "items": {"type": "string"}},
+                                        {"type": "null"},
+                                    ],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {
+                                    "title": "X-Token",
+                                    "type": "array",
+                                    "items": {"type": "string"},
+                                }
+                            ),
                             "name": "x-token",
                             "in": "header",
                         }
index b404ce5d8f99cb2319cb0b429a4f88905bcfd7e7..3afb355e948ae11bc68132aed84e8ae96474ca09 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -32,7 +33,6 @@ def test(path, headers, expected_status, expected_response, client: TestClient):
 def test_openapi_schema(client: TestClient):
     response = client.get("/openapi.json")
     assert response.status_code == 200
-    # insert_assert(response.json())
     assert response.json() == {
         "openapi": "3.1.0",
         "info": {"title": "FastAPI", "version": "0.1.0"},
@@ -44,11 +44,23 @@ def test_openapi_schema(client: TestClient):
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {
-                                "title": "X-Token",
-                                "type": "array",
-                                "items": {"type": "string"},
-                            },
+                            "schema": IsDict(
+                                {
+                                    "title": "X-Token",
+                                    "anyOf": [
+                                        {"type": "array", "items": {"type": "string"}},
+                                        {"type": "null"},
+                                    ],
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {
+                                    "title": "X-Token",
+                                    "type": "array",
+                                    "items": {"type": "string"},
+                                }
+                            ),
                             "name": "x-token",
                             "in": "header",
                         }
index a6e898c498c3eea530aa4ff5d82cdea4b6edc622..73af420ae1eff7c2e16a58ba3a776864f901d5cf 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.openapi_callbacks.tutorial001 import app, invoice_notification
@@ -33,13 +34,30 @@ def test_openapi_schema():
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {
-                                "title": "Callback Url",
-                                "maxLength": 2083,
-                                "minLength": 1,
-                                "type": "string",
-                                "format": "uri",
-                            },
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [
+                                        {
+                                            "type": "string",
+                                            "format": "uri",
+                                            "minLength": 1,
+                                            "maxLength": 2083,
+                                        },
+                                        {"type": "null"},
+                                    ],
+                                    "title": "Callback Url",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {
+                                    "title": "Callback Url",
+                                    "maxLength": 2083,
+                                    "minLength": 1,
+                                    "type": "string",
+                                    "format": "uri",
+                                }
+                            ),
                             "name": "callback_url",
                             "in": "query",
                         }
@@ -132,7 +150,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"},
                     },
index cd9fc520e4165d21d5a15c0baf86909a54933c8b..dd123f48d6e8cff9f0bab1c9beee36f55a25ba4b 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.path_operation_advanced_configuration.tutorial004 import app
@@ -68,9 +69,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"}
+                        ),
                         "tags": {
                             "title": "Tags",
                             "uniqueItems": True,
index 3b88a38c28a7a3ea4ef5273119789e4d2ce8d7c0..2d280226914293541a640e2fc84c1e35ecc0ea4a 100644 (file)
@@ -1,11 +1,20 @@
+import pytest
 from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
 
-from docs_src.path_operation_advanced_configuration.tutorial007 import app
+from ...utils import needs_pydanticv2
 
-client = TestClient(app)
 
+@pytest.fixture(name="client")
+def get_client():
+    from docs_src.path_operation_advanced_configuration.tutorial007 import app
 
-def test_post():
+    client = TestClient(app)
+    return client
+
+
+@needs_pydanticv2
+def test_post(client: TestClient):
     yaml_data = """
         name: Deadpoolio
         tags:
@@ -21,7 +30,8 @@ def test_post():
     }
 
 
-def test_post_broken_yaml():
+@needs_pydanticv2
+def test_post_broken_yaml(client: TestClient):
     yaml_data = """
         name: Deadpoolio
         tags:
@@ -34,7 +44,8 @@ def test_post_broken_yaml():
     assert response.json() == {"detail": "Invalid YAML"}
 
 
-def test_post_invalid():
+@needs_pydanticv2
+def test_post_invalid(client: TestClient):
     yaml_data = """
         name: Deadpoolio
         tags:
@@ -45,14 +56,22 @@ def test_post_invalid():
         """
     response = client.post("/items/", content=yaml_data)
     assert response.status_code == 422, response.text
+    # insert_assert(response.json())
     assert response.json() == {
         "detail": [
-            {"loc": ["tags", 3], "msg": "str type expected", "type": "type_error.str"}
+            {
+                "type": "string_type",
+                "loc": ["tags", 3],
+                "msg": "Input should be a valid string",
+                "input": {"sneaky": "object"},
+                "url": match_pydantic_error_url("string_type"),
+            }
         ]
     }
 
 
-def test_openapi_schema():
+@needs_pydanticv2
+def test_openapi_schema(client: TestClient):
     response = client.get("/openapi.json")
     assert response.status_code == 200, response.text
     assert response.json() == {
diff --git a/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py b/tests/test_tutorial/test_path_operation_advanced_configurations/test_tutorial007_pv1.py
new file mode 100644 (file)
index 0000000..ef012f8
--- /dev/null
@@ -0,0 +1,106 @@
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_pydanticv1
+
+
+@pytest.fixture(name="client")
+def get_client():
+    from docs_src.path_operation_advanced_configuration.tutorial007_pv1 import app
+
+    client = TestClient(app)
+    return client
+
+
+@needs_pydanticv1
+def test_post(client: TestClient):
+    yaml_data = """
+        name: Deadpoolio
+        tags:
+        - x-force
+        - x-men
+        - x-avengers
+        """
+    response = client.post("/items/", content=yaml_data)
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "name": "Deadpoolio",
+        "tags": ["x-force", "x-men", "x-avengers"],
+    }
+
+
+@needs_pydanticv1
+def test_post_broken_yaml(client: TestClient):
+    yaml_data = """
+        name: Deadpoolio
+        tags:
+        x - x-force
+        x - x-men
+        x - x-avengers
+        """
+    response = client.post("/items/", content=yaml_data)
+    assert response.status_code == 422, response.text
+    assert response.json() == {"detail": "Invalid YAML"}
+
+
+@needs_pydanticv1
+def test_post_invalid(client: TestClient):
+    yaml_data = """
+        name: Deadpoolio
+        tags:
+        - x-force
+        - x-men
+        - x-avengers
+        - sneaky: object
+        """
+    response = client.post("/items/", content=yaml_data)
+    assert response.status_code == 422, response.text
+    assert response.json() == {
+        "detail": [
+            {"loc": ["tags", 3], "msg": "str type expected", "type": "type_error.str"}
+        ]
+    }
+
+
+@needs_pydanticv1
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/": {
+                "post": {
+                    "summary": "Create Item",
+                    "operationId": "create_item_items__post",
+                    "requestBody": {
+                        "content": {
+                            "application/x-yaml": {
+                                "schema": {
+                                    "title": "Item",
+                                    "required": ["name", "tags"],
+                                    "type": "object",
+                                    "properties": {
+                                        "name": {"title": "Name", "type": "string"},
+                                        "tags": {
+                                            "title": "Tags",
+                                            "type": "array",
+                                            "items": {"type": "string"},
+                                        },
+                                    },
+                                }
+                            }
+                        },
+                        "required": True,
+                    },
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        }
+                    },
+                }
+            }
+        },
+    }
index 30278caf86437351c78bfca5c7c1e17cef74b58e..e7e9a982e1f33d8ada66b245103f27367fc362b2 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.path_operation_configuration.tutorial005 import app
@@ -68,9 +69,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"}
+                        ),
                         "tags": {
                             "title": "Tags",
                             "uniqueItems": True,
index cf59d354cd753269e9417cef48643eef24ef9d2d..ebfeb809cc3a13cb86c42d0d36056e6f6021bec7 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -77,9 +78,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"}
+                        ),
                         "tags": {
                             "title": "Tags",
                             "uniqueItems": True,
index a93ea8807eaa760a99d7061682de51d339d88265..8e79afe968843e9aef4eb31ae2da671d327fc87c 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py39
@@ -77,9 +78,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"}
+                        ),
                         "tags": {
                             "title": "Tags",
                             "uniqueItems": True,
index b9b58c96154c5ea82d3da742b4e20f456767418e..90fa6adaf7898900b846598e3bf1334fd253df2d 100644 (file)
@@ -1,4 +1,4 @@
-import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.path_params.tutorial005 import app
@@ -6,47 +6,55 @@ from docs_src.path_params.tutorial005 import app
 client = TestClient(app)
 
 
-@pytest.mark.parametrize(
-    "url,status_code,expected",
-    [
-        (
-            "/models/alexnet",
-            200,
-            {"model_name": "alexnet", "message": "Deep Learning FTW!"},
-        ),
-        (
-            "/models/lenet",
-            200,
-            {"model_name": "lenet", "message": "LeCNN all the images"},
-        ),
-        (
-            "/models/resnet",
-            200,
-            {"model_name": "resnet", "message": "Have some residuals"},
-        ),
-        (
-            "/models/foo",
-            422,
-            {
-                "detail": [
-                    {
-                        "ctx": {"enum_values": ["alexnet", "resnet", "lenet"]},
-                        "loc": ["path", "model_name"],
-                        "msg": "value is not a valid enumeration member; permitted: 'alexnet', 'resnet', 'lenet'",
-                        "type": "type_error.enum",
-                    }
-                ]
-            },
-        ),
-    ],
-)
-def test_get_enums(url, status_code, expected):
-    response = client.get(url)
-    assert response.status_code == status_code
-    assert response.json() == expected
+def test_get_enums_alexnet():
+    response = client.get("/models/alexnet")
+    assert response.status_code == 200
+    assert response.json() == {"model_name": "alexnet", "message": "Deep Learning FTW!"}
+
+
+def test_get_enums_lenet():
+    response = client.get("/models/lenet")
+    assert response.status_code == 200
+    assert response.json() == {"model_name": "lenet", "message": "LeCNN all the images"}
 
 
-def test_openapi():
+def test_get_enums_resnet():
+    response = client.get("/models/resnet")
+    assert response.status_code == 200
+    assert response.json() == {"model_name": "resnet", "message": "Have some residuals"}
+
+
+def test_get_enums_invalid():
+    response = client.get("/models/foo")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "enum",
+                    "loc": ["path", "model_name"],
+                    "msg": "Input should be 'alexnet','resnet' or 'lenet'",
+                    "input": "foo",
+                    "ctx": {"expected": "'alexnet','resnet' or 'lenet'"},
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "ctx": {"enum_values": ["alexnet", "resnet", "lenet"]},
+                    "loc": ["path", "model_name"],
+                    "msg": "value is not a valid enumeration member; permitted: 'alexnet', 'resnet', 'lenet'",
+                    "type": "type_error.enum",
+                }
+            ]
+        }
+    )
+
+
+def test_openapi_schema():
     response = client.get("/openapi.json")
     assert response.status_code == 200, response.text
     data = response.json()
@@ -98,12 +106,22 @@ def test_openapi():
                         }
                     },
                 },
-                "ModelName": {
-                    "title": "ModelName",
-                    "enum": ["alexnet", "resnet", "lenet"],
-                    "type": "string",
-                    "description": "An enumeration.",
-                },
+                "ModelName": IsDict(
+                    {
+                        "title": "ModelName",
+                        "enum": ["alexnet", "resnet", "lenet"],
+                        "type": "string",
+                    }
+                )
+                | IsDict(
+                    {
+                        # TODO: remove when deprecating Pydantic v1
+                        "title": "ModelName",
+                        "enum": ["alexnet", "resnet", "lenet"],
+                        "type": "string",
+                        "description": "An enumeration.",
+                    }
+                ),
                 "ValidationError": {
                     "title": "ValidationError",
                     "required": ["loc", "msg", "type"],
index 6c2cba7e1299a41d77b3a29134a91a29428aca97..9215863576349223bf0b5279534213d0c56a5146 100644 (file)
@@ -1,34 +1,45 @@
-import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
 
 from docs_src.query_params.tutorial005 import app
 
 client = TestClient(app)
 
 
-query_required = {
-    "detail": [
-        {
-            "loc": ["query", "needy"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        }
-    ]
-}
+def test_foo_needy_very():
+    response = client.get("/items/foo?needy=very")
+    assert response.status_code == 200
+    assert response.json() == {"item_id": "foo", "needy": "very"}
 
 
-@pytest.mark.parametrize(
-    "path,expected_status,expected_response",
-    [
-        ("/items/foo?needy=very", 200, {"item_id": "foo", "needy": "very"}),
-        ("/items/foo", 422, query_required),
-        ("/items/foo", 422, query_required),
-    ],
-)
-def test(path, expected_status, expected_response):
-    response = client.get(path)
-    assert response.status_code == expected_status
-    assert response.json() == expected_response
+def test_foo_no_needy():
+    response = client.get("/items/foo")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["query", "needy"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["query", "needy"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
+        }
+    )
 
 
 def test_openapi_schema():
index 626637903f5a6f88e265aab3f521249d97a65c36..e07803d6c9b5f9efaee4990a6560405f444aa411 100644 (file)
@@ -1,62 +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.query_params.tutorial006 import app
 
-client = TestClient(app)
+@pytest.fixture(name="client")
+def get_client():
+    from docs_src.query_params.tutorial006 import app
 
+    c = TestClient(app)
+    return c
 
-query_required = {
-    "detail": [
-        {
-            "loc": ["query", "needy"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        }
-    ]
-}
+
+def test_foo_needy_very(client: TestClient):
+    response = client.get("/items/foo?needy=very")
+    assert response.status_code == 200
+    assert response.json() == {
+        "item_id": "foo",
+        "needy": "very",
+        "skip": 0,
+        "limit": None,
+    }
 
 
-@pytest.mark.parametrize(
-    "path,expected_status,expected_response",
-    [
-        (
-            "/items/foo?needy=very",
-            200,
-            {"item_id": "foo", "needy": "very", "skip": 0, "limit": None},
-        ),
-        (
-            "/items/foo?skip=a&limit=b",
-            422,
-            {
-                "detail": [
-                    {
-                        "loc": ["query", "needy"],
-                        "msg": "field required",
-                        "type": "value_error.missing",
-                    },
-                    {
-                        "loc": ["query", "skip"],
-                        "msg": "value is not a valid integer",
-                        "type": "type_error.integer",
-                    },
-                    {
-                        "loc": ["query", "limit"],
-                        "msg": "value is not a valid integer",
-                        "type": "type_error.integer",
-                    },
-                ]
-            },
-        ),
-    ],
-)
-def test(path, expected_status, expected_response):
-    response = client.get(path)
-    assert response.status_code == expected_status
-    assert response.json() == expected_response
+def test_foo_no_needy(client: TestClient):
+    response = client.get("/items/foo?skip=a&limit=b")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["query", "needy"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "int_parsing",
+                    "loc": ["query", "skip"],
+                    "msg": "Input should be a valid integer, unable to parse string as an integer",
+                    "input": "a",
+                    "url": match_pydantic_error_url("int_parsing"),
+                },
+                {
+                    "type": "int_parsing",
+                    "loc": ["query", "limit"],
+                    "msg": "Input should be a valid integer, unable to parse string as an integer",
+                    "input": "b",
+                    "url": match_pydantic_error_url("int_parsing"),
+                },
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["query", "needy"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["query", "skip"],
+                    "msg": "value is not a valid integer",
+                    "type": "type_error.integer",
+                },
+                {
+                    "loc": ["query", "limit"],
+                    "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
     assert response.json() == {
@@ -108,7 +128,16 @@ def test_openapi_schema():
                         },
                         {
                             "required": False,
-                            "schema": {"title": "Limit", "type": "integer"},
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                    "title": "Limit",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Limit", "type": "integer"}
+                            ),
                             "name": "limit",
                             "in": "query",
                         },
index b6fb2f39e112b6cac9e5c02382542ccdf8094bb9..6c4c0b4dc8927468cbc98c18e3afe58e73b3ebd8 100644 (file)
@@ -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_py310
 
-query_required = {
-    "detail": [
-        {
-            "loc": ["query", "needy"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        }
-    ]
-}
-
 
 @pytest.fixture(name="client")
 def get_client():
@@ -23,43 +15,69 @@ def get_client():
 
 
 @needs_py310
-@pytest.mark.parametrize(
-    "path,expected_status,expected_response",
-    [
-        (
-            "/items/foo?needy=very",
-            200,
-            {"item_id": "foo", "needy": "very", "skip": 0, "limit": None},
-        ),
-        (
-            "/items/foo?skip=a&limit=b",
-            422,
-            {
-                "detail": [
-                    {
-                        "loc": ["query", "needy"],
-                        "msg": "field required",
-                        "type": "value_error.missing",
-                    },
-                    {
-                        "loc": ["query", "skip"],
-                        "msg": "value is not a valid integer",
-                        "type": "type_error.integer",
-                    },
-                    {
-                        "loc": ["query", "limit"],
-                        "msg": "value is not a valid integer",
-                        "type": "type_error.integer",
-                    },
-                ]
-            },
-        ),
-    ],
-)
-def test(path, expected_status, expected_response, client: TestClient):
-    response = client.get(path)
-    assert response.status_code == expected_status
-    assert response.json() == expected_response
+def test_foo_needy_very(client: TestClient):
+    response = client.get("/items/foo?needy=very")
+    assert response.status_code == 200
+    assert response.json() == {
+        "item_id": "foo",
+        "needy": "very",
+        "skip": 0,
+        "limit": None,
+    }
+
+
+@needs_py310
+def test_foo_no_needy(client: TestClient):
+    response = client.get("/items/foo?skip=a&limit=b")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["query", "needy"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "int_parsing",
+                    "loc": ["query", "skip"],
+                    "msg": "Input should be a valid integer, unable to parse string as an integer",
+                    "input": "a",
+                    "url": match_pydantic_error_url("int_parsing"),
+                },
+                {
+                    "type": "int_parsing",
+                    "loc": ["query", "limit"],
+                    "msg": "Input should be a valid integer, unable to parse string as an integer",
+                    "input": "b",
+                    "url": match_pydantic_error_url("int_parsing"),
+                },
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["query", "needy"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["query", "skip"],
+                    "msg": "value is not a valid integer",
+                    "type": "type_error.integer",
+                },
+                {
+                    "loc": ["query", "limit"],
+                    "msg": "value is not a valid integer",
+                    "type": "type_error.integer",
+                },
+            ]
+        }
+    )
 
 
 @needs_py310
@@ -115,7 +133,16 @@ def test_openapi_schema(client: TestClient):
                         },
                         {
                             "required": False,
-                            "schema": {"title": "Limit", "type": "integer"},
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [{"type": "integer"}, {"type": "null"}],
+                                    "title": "Limit",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {"title": "Limit", "type": "integer"}
+                            ),
                             "name": "limit",
                             "in": "query",
                         },
index 370ae0ff05790ef5046b6b13bfbc406cac2976b3..287c2e8f8e9e0ae299387dba97997503dcedc89b 100644 (file)
@@ -1,47 +1,70 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
 
-from docs_src.query_params_str_validations.tutorial010 import app
 
-client = TestClient(app)
+@pytest.fixture(name="client")
+def get_client():
+    from docs_src.query_params_str_validations.tutorial010 import app
 
+    client = TestClient(app)
+    return client
 
-regex_error = {
-    "detail": [
-        {
-            "ctx": {"pattern": "^fixedquery$"},
-            "loc": ["query", "item-query"],
-            "msg": 'string does not match regex "^fixedquery$"',
-            "type": "value_error.str.regex",
-        }
-    ]
-}
+
+def test_query_params_str_validations_no_query(client: TestClient):
+    response = client.get("/items/")
+    assert response.status_code == 200
+    assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
+
+
+def test_query_params_str_validations_item_query_fixedquery(client: TestClient):
+    response = client.get("/items/", params={"item-query": "fixedquery"})
+    assert response.status_code == 200
+    assert response.json() == {
+        "items": [{"item_id": "Foo"}, {"item_id": "Bar"}],
+        "q": "fixedquery",
+    }
+
+
+def test_query_params_str_validations_q_fixedquery(client: TestClient):
+    response = client.get("/items/", params={"q": "fixedquery"})
+    assert response.status_code == 200
+    assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
 
 
-@pytest.mark.parametrize(
-    "q_name,q,expected_status,expected_response",
-    [
-        (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}),
-        (
-            "item-query",
-            "fixedquery",
-            200,
-            {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"},
-        ),
-        ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}),
-        ("item-query", "nonregexquery", 422, regex_error),
-    ],
-)
-def test_query_params_str_validations(q_name, q, expected_status, expected_response):
-    url = "/items/"
-    if q_name and q:
-        url = f"{url}?{q_name}={q}"
-    response = client.get(url)
-    assert response.status_code == expected_status
-    assert response.json() == expected_response
+def test_query_params_str_validations_item_query_nonregexquery(client: TestClient):
+    response = client.get("/items/", params={"item-query": "nonregexquery"})
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "string_pattern_mismatch",
+                    "loc": ["query", "item-query"],
+                    "msg": "String should match pattern '^fixedquery$'",
+                    "input": "nonregexquery",
+                    "ctx": {"pattern": "^fixedquery$"},
+                    "url": match_pydantic_error_url("string_pattern_mismatch"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "ctx": {"pattern": "^fixedquery$"},
+                    "loc": ["query", "item-query"],
+                    "msg": 'string does not match regex "^fixedquery$"',
+                    "type": "value_error.str.regex",
+                }
+            ]
+        }
+    )
 
 
-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() == {
@@ -73,14 +96,32 @@ def test_openapi_schema():
                             "description": "Query string for the items to search in the database that have a good match",
                             "required": False,
                             "deprecated": True,
-                            "schema": {
-                                "title": "Query string",
-                                "maxLength": 50,
-                                "minLength": 3,
-                                "pattern": "^fixedquery$",
-                                "type": "string",
-                                "description": "Query string for the items to search in the database that have a good match",
-                            },
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [
+                                        {
+                                            "type": "string",
+                                            "minLength": 3,
+                                            "maxLength": 50,
+                                            "pattern": "^fixedquery$",
+                                        },
+                                        {"type": "null"},
+                                    ],
+                                    "title": "Query string",
+                                    "description": "Query string for the items to search in the database that have a good match",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {
+                                    "title": "Query string",
+                                    "maxLength": 50,
+                                    "minLength": 3,
+                                    "pattern": "^fixedquery$",
+                                    "type": "string",
+                                    "description": "Query string for the items to search in the database that have a good match",
+                                }
+                            ),
                             "name": "item-query",
                             "in": "query",
                         }
index 1f76ef31467eaa1153ac4d4ecb7bceb2781cbcd3..5b0515070ab6079eb785c96f7c6810ffe63b5906 100644 (file)
@@ -1,47 +1,70 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
 
-from docs_src.query_params_str_validations.tutorial010_an import app
 
-client = TestClient(app)
+@pytest.fixture(name="client")
+def get_client():
+    from docs_src.query_params_str_validations.tutorial010_an import app
 
+    client = TestClient(app)
+    return client
 
-regex_error = {
-    "detail": [
-        {
-            "ctx": {"pattern": "^fixedquery$"},
-            "loc": ["query", "item-query"],
-            "msg": 'string does not match regex "^fixedquery$"',
-            "type": "value_error.str.regex",
-        }
-    ]
-}
+
+def test_query_params_str_validations_no_query(client: TestClient):
+    response = client.get("/items/")
+    assert response.status_code == 200
+    assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
+
+
+def test_query_params_str_validations_item_query_fixedquery(client: TestClient):
+    response = client.get("/items/", params={"item-query": "fixedquery"})
+    assert response.status_code == 200
+    assert response.json() == {
+        "items": [{"item_id": "Foo"}, {"item_id": "Bar"}],
+        "q": "fixedquery",
+    }
+
+
+def test_query_params_str_validations_q_fixedquery(client: TestClient):
+    response = client.get("/items/", params={"q": "fixedquery"})
+    assert response.status_code == 200
+    assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
 
 
-@pytest.mark.parametrize(
-    "q_name,q,expected_status,expected_response",
-    [
-        (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}),
-        (
-            "item-query",
-            "fixedquery",
-            200,
-            {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"},
-        ),
-        ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}),
-        ("item-query", "nonregexquery", 422, regex_error),
-    ],
-)
-def test_query_params_str_validations(q_name, q, expected_status, expected_response):
-    url = "/items/"
-    if q_name and q:
-        url = f"{url}?{q_name}={q}"
-    response = client.get(url)
-    assert response.status_code == expected_status
-    assert response.json() == expected_response
+def test_query_params_str_validations_item_query_nonregexquery(client: TestClient):
+    response = client.get("/items/", params={"item-query": "nonregexquery"})
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "string_pattern_mismatch",
+                    "loc": ["query", "item-query"],
+                    "msg": "String should match pattern '^fixedquery$'",
+                    "input": "nonregexquery",
+                    "ctx": {"pattern": "^fixedquery$"},
+                    "url": match_pydantic_error_url("string_pattern_mismatch"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "ctx": {"pattern": "^fixedquery$"},
+                    "loc": ["query", "item-query"],
+                    "msg": 'string does not match regex "^fixedquery$"',
+                    "type": "value_error.str.regex",
+                }
+            ]
+        }
+    )
 
 
-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() == {
@@ -73,14 +96,32 @@ def test_openapi_schema():
                             "description": "Query string for the items to search in the database that have a good match",
                             "required": False,
                             "deprecated": True,
-                            "schema": {
-                                "title": "Query string",
-                                "maxLength": 50,
-                                "minLength": 3,
-                                "pattern": "^fixedquery$",
-                                "type": "string",
-                                "description": "Query string for the items to search in the database that have a good match",
-                            },
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [
+                                        {
+                                            "type": "string",
+                                            "minLength": 3,
+                                            "maxLength": 50,
+                                            "pattern": "^fixedquery$",
+                                        },
+                                        {"type": "null"},
+                                    ],
+                                    "title": "Query string",
+                                    "description": "Query string for the items to search in the database that have a good match",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {
+                                    "title": "Query string",
+                                    "maxLength": 50,
+                                    "minLength": 3,
+                                    "pattern": "^fixedquery$",
+                                    "type": "string",
+                                    "description": "Query string for the items to search in the database that have a good match",
+                                }
+                            ),
                             "name": "item-query",
                             "in": "query",
                         }
index 3a06b4bc7a68e92b04a203b9d2284d2b6b518592..d22b1ce204a11d759b1d896ee45fccae72ead789 100644 (file)
@@ -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,42 +14,60 @@ def get_client():
     return client
 
 
-regex_error = {
-    "detail": [
-        {
-            "ctx": {"pattern": "^fixedquery$"},
-            "loc": ["query", "item-query"],
-            "msg": 'string does not match regex "^fixedquery$"',
-            "type": "value_error.str.regex",
-        }
-    ]
-}
+@needs_py310
+def test_query_params_str_validations_no_query(client: TestClient):
+    response = client.get("/items/")
+    assert response.status_code == 200
+    assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
+
+
+@needs_py310
+def test_query_params_str_validations_item_query_fixedquery(client: TestClient):
+    response = client.get("/items/", params={"item-query": "fixedquery"})
+    assert response.status_code == 200
+    assert response.json() == {
+        "items": [{"item_id": "Foo"}, {"item_id": "Bar"}],
+        "q": "fixedquery",
+    }
+
+
+@needs_py310
+def test_query_params_str_validations_q_fixedquery(client: TestClient):
+    response = client.get("/items/", params={"q": "fixedquery"})
+    assert response.status_code == 200
+    assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
 
 
 @needs_py310
-@pytest.mark.parametrize(
-    "q_name,q,expected_status,expected_response",
-    [
-        (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}),
-        (
-            "item-query",
-            "fixedquery",
-            200,
-            {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"},
-        ),
-        ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}),
-        ("item-query", "nonregexquery", 422, regex_error),
-    ],
-)
-def test_query_params_str_validations(
-    q_name, q, expected_status, expected_response, client: TestClient
-):
-    url = "/items/"
-    if q_name and q:
-        url = f"{url}?{q_name}={q}"
-    response = client.get(url)
-    assert response.status_code == expected_status
-    assert response.json() == expected_response
+def test_query_params_str_validations_item_query_nonregexquery(client: TestClient):
+    response = client.get("/items/", params={"item-query": "nonregexquery"})
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "string_pattern_mismatch",
+                    "loc": ["query", "item-query"],
+                    "msg": "String should match pattern '^fixedquery$'",
+                    "input": "nonregexquery",
+                    "ctx": {"pattern": "^fixedquery$"},
+                    "url": match_pydantic_error_url("string_pattern_mismatch"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "ctx": {"pattern": "^fixedquery$"},
+                    "loc": ["query", "item-query"],
+                    "msg": 'string does not match regex "^fixedquery$"',
+                    "type": "value_error.str.regex",
+                }
+            ]
+        }
+    )
 
 
 @needs_py310
@@ -83,14 +103,32 @@ def test_openapi_schema(client: TestClient):
                             "description": "Query string for the items to search in the database that have a good match",
                             "required": False,
                             "deprecated": True,
-                            "schema": {
-                                "title": "Query string",
-                                "maxLength": 50,
-                                "minLength": 3,
-                                "pattern": "^fixedquery$",
-                                "type": "string",
-                                "description": "Query string for the items to search in the database that have a good match",
-                            },
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [
+                                        {
+                                            "type": "string",
+                                            "minLength": 3,
+                                            "maxLength": 50,
+                                            "pattern": "^fixedquery$",
+                                        },
+                                        {"type": "null"},
+                                    ],
+                                    "title": "Query string",
+                                    "description": "Query string for the items to search in the database that have a good match",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {
+                                    "title": "Query string",
+                                    "maxLength": 50,
+                                    "minLength": 3,
+                                    "pattern": "^fixedquery$",
+                                    "type": "string",
+                                    "description": "Query string for the items to search in the database that have a good match",
+                                }
+                            ),
                             "name": "item-query",
                             "in": "query",
                         }
index 1e6f9309370fea96b3e6a14af7521cdc77868fd8..3e7d5d3adbe63bd02e63edbb3349bdc3ff2f1f78 100644 (file)
@@ -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,42 +14,60 @@ def get_client():
     return client
 
 
-regex_error = {
-    "detail": [
-        {
-            "ctx": {"pattern": "^fixedquery$"},
-            "loc": ["query", "item-query"],
-            "msg": 'string does not match regex "^fixedquery$"',
-            "type": "value_error.str.regex",
-        }
-    ]
-}
+@needs_py39
+def test_query_params_str_validations_no_query(client: TestClient):
+    response = client.get("/items/")
+    assert response.status_code == 200
+    assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
+
+
+@needs_py39
+def test_query_params_str_validations_item_query_fixedquery(client: TestClient):
+    response = client.get("/items/", params={"item-query": "fixedquery"})
+    assert response.status_code == 200
+    assert response.json() == {
+        "items": [{"item_id": "Foo"}, {"item_id": "Bar"}],
+        "q": "fixedquery",
+    }
+
+
+@needs_py39
+def test_query_params_str_validations_q_fixedquery(client: TestClient):
+    response = client.get("/items/", params={"q": "fixedquery"})
+    assert response.status_code == 200
+    assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
 
 
 @needs_py39
-@pytest.mark.parametrize(
-    "q_name,q,expected_status,expected_response",
-    [
-        (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}),
-        (
-            "item-query",
-            "fixedquery",
-            200,
-            {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"},
-        ),
-        ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}),
-        ("item-query", "nonregexquery", 422, regex_error),
-    ],
-)
-def test_query_params_str_validations(
-    q_name, q, expected_status, expected_response, client: TestClient
-):
-    url = "/items/"
-    if q_name and q:
-        url = f"{url}?{q_name}={q}"
-    response = client.get(url)
-    assert response.status_code == expected_status
-    assert response.json() == expected_response
+def test_query_params_str_validations_item_query_nonregexquery(client: TestClient):
+    response = client.get("/items/", params={"item-query": "nonregexquery"})
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "string_pattern_mismatch",
+                    "loc": ["query", "item-query"],
+                    "msg": "String should match pattern '^fixedquery$'",
+                    "input": "nonregexquery",
+                    "ctx": {"pattern": "^fixedquery$"},
+                    "url": match_pydantic_error_url("string_pattern_mismatch"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "ctx": {"pattern": "^fixedquery$"},
+                    "loc": ["query", "item-query"],
+                    "msg": 'string does not match regex "^fixedquery$"',
+                    "type": "value_error.str.regex",
+                }
+            ]
+        }
+    )
 
 
 @needs_py39
@@ -83,14 +103,32 @@ def test_openapi_schema(client: TestClient):
                             "description": "Query string for the items to search in the database that have a good match",
                             "required": False,
                             "deprecated": True,
-                            "schema": {
-                                "title": "Query string",
-                                "maxLength": 50,
-                                "minLength": 3,
-                                "pattern": "^fixedquery$",
-                                "type": "string",
-                                "description": "Query string for the items to search in the database that have a good match",
-                            },
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [
+                                        {
+                                            "type": "string",
+                                            "minLength": 3,
+                                            "maxLength": 50,
+                                            "pattern": "^fixedquery$",
+                                        },
+                                        {"type": "null"},
+                                    ],
+                                    "title": "Query string",
+                                    "description": "Query string for the items to search in the database that have a good match",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {
+                                    "title": "Query string",
+                                    "maxLength": 50,
+                                    "minLength": 3,
+                                    "pattern": "^fixedquery$",
+                                    "type": "string",
+                                    "description": "Query string for the items to search in the database that have a good match",
+                                }
+                            ),
                             "name": "item-query",
                             "in": "query",
                         }
index 63524d291d81f6b33fd80328e2db1e906619d00e..1c3a09d399f46c64a0c725841d0dc7edb45974f4 100644 (file)
@@ -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,42 +14,60 @@ def get_client():
     return client
 
 
-regex_error = {
-    "detail": [
-        {
-            "ctx": {"pattern": "^fixedquery$"},
-            "loc": ["query", "item-query"],
-            "msg": 'string does not match regex "^fixedquery$"',
-            "type": "value_error.str.regex",
-        }
-    ]
-}
+@needs_py310
+def test_query_params_str_validations_no_query(client: TestClient):
+    response = client.get("/items/")
+    assert response.status_code == 200
+    assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
+
+
+@needs_py310
+def test_query_params_str_validations_item_query_fixedquery(client: TestClient):
+    response = client.get("/items/", params={"item-query": "fixedquery"})
+    assert response.status_code == 200
+    assert response.json() == {
+        "items": [{"item_id": "Foo"}, {"item_id": "Bar"}],
+        "q": "fixedquery",
+    }
+
+
+@needs_py310
+def test_query_params_str_validations_q_fixedquery(client: TestClient):
+    response = client.get("/items/", params={"q": "fixedquery"})
+    assert response.status_code == 200
+    assert response.json() == {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
 
 
 @needs_py310
-@pytest.mark.parametrize(
-    "q_name,q,expected_status,expected_response",
-    [
-        (None, None, 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}),
-        (
-            "item-query",
-            "fixedquery",
-            200,
-            {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}], "q": "fixedquery"},
-        ),
-        ("q", "fixedquery", 200, {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}),
-        ("item-query", "nonregexquery", 422, regex_error),
-    ],
-)
-def test_query_params_str_validations(
-    q_name, q, expected_status, expected_response, client: TestClient
-):
-    url = "/items/"
-    if q_name and q:
-        url = f"{url}?{q_name}={q}"
-    response = client.get(url)
-    assert response.status_code == expected_status
-    assert response.json() == expected_response
+def test_query_params_str_validations_item_query_nonregexquery(client: TestClient):
+    response = client.get("/items/", params={"item-query": "nonregexquery"})
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "string_pattern_mismatch",
+                    "loc": ["query", "item-query"],
+                    "msg": "String should match pattern '^fixedquery$'",
+                    "input": "nonregexquery",
+                    "ctx": {"pattern": "^fixedquery$"},
+                    "url": match_pydantic_error_url("string_pattern_mismatch"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "ctx": {"pattern": "^fixedquery$"},
+                    "loc": ["query", "item-query"],
+                    "msg": 'string does not match regex "^fixedquery$"',
+                    "type": "value_error.str.regex",
+                }
+            ]
+        }
+    )
 
 
 @needs_py310
@@ -83,14 +103,32 @@ def test_openapi_schema(client: TestClient):
                             "description": "Query string for the items to search in the database that have a good match",
                             "required": False,
                             "deprecated": True,
-                            "schema": {
-                                "title": "Query string",
-                                "maxLength": 50,
-                                "minLength": 3,
-                                "pattern": "^fixedquery$",
-                                "type": "string",
-                                "description": "Query string for the items to search in the database that have a good match",
-                            },
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [
+                                        {
+                                            "type": "string",
+                                            "minLength": 3,
+                                            "maxLength": 50,
+                                            "pattern": "^fixedquery$",
+                                        },
+                                        {"type": "null"},
+                                    ],
+                                    "title": "Query string",
+                                    "description": "Query string for the items to search in the database that have a good match",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {
+                                    "title": "Query string",
+                                    "maxLength": 50,
+                                    "minLength": 3,
+                                    "pattern": "^fixedquery$",
+                                    "type": "string",
+                                    "description": "Query string for the items to search in the database that have a good match",
+                                }
+                            ),
                             "name": "item-query",
                             "in": "query",
                         }
index 164ec11930275a393fa91a3599b7654d1ad5a98b..5ba39b05d612b47502a2fe98c58667bff392b111 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.query_params_str_validations.tutorial011 import app
@@ -49,11 +50,23 @@ def test_openapi_schema():
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {
-                                "title": "Q",
-                                "type": "array",
-                                "items": {"type": "string"},
-                            },
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [
+                                        {"type": "array", "items": {"type": "string"}},
+                                        {"type": "null"},
+                                    ],
+                                    "title": "Q",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {
+                                    "title": "Q",
+                                    "type": "array",
+                                    "items": {"type": "string"},
+                                }
+                            ),
                             "name": "q",
                             "in": "query",
                         }
index 2afaafd92ec150813388496f54399dc561f26ae5..3942ea77a93854f294bae233163ec0c917249884 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.query_params_str_validations.tutorial011_an import app
@@ -49,11 +50,23 @@ def test_openapi_schema():
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {
-                                "title": "Q",
-                                "type": "array",
-                                "items": {"type": "string"},
-                            },
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [
+                                        {"type": "array", "items": {"type": "string"}},
+                                        {"type": "null"},
+                                    ],
+                                    "title": "Q",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {
+                                    "title": "Q",
+                                    "type": "array",
+                                    "items": {"type": "string"},
+                                }
+                            ),
                             "name": "q",
                             "in": "query",
                         }
index fafd38337c9efc3094f532da3e198f6f3604b09a..f2ec38c9504151a15a6da6e60813ad6332710c67 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -59,11 +60,23 @@ def test_openapi_schema(client: TestClient):
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {
-                                "title": "Q",
-                                "type": "array",
-                                "items": {"type": "string"},
-                            },
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [
+                                        {"type": "array", "items": {"type": "string"}},
+                                        {"type": "null"},
+                                    ],
+                                    "title": "Q",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {
+                                    "title": "Q",
+                                    "type": "array",
+                                    "items": {"type": "string"},
+                                }
+                            ),
                             "name": "q",
                             "in": "query",
                         }
index f3fb4752830cb74333e0902f5fce028374c7ff9c..cd7b156798981fcdaccdef6c6cddffc374bf4fdb 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py39
@@ -59,11 +60,23 @@ def test_openapi_schema(client: TestClient):
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {
-                                "title": "Q",
-                                "type": "array",
-                                "items": {"type": "string"},
-                            },
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [
+                                        {"type": "array", "items": {"type": "string"}},
+                                        {"type": "null"},
+                                    ],
+                                    "title": "Q",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {
+                                    "title": "Q",
+                                    "type": "array",
+                                    "items": {"type": "string"},
+                                }
+                            ),
                             "name": "q",
                             "in": "query",
                         }
index 21f348f2b959ddcf565917adb5a0f5efcd9122bf..bdc7295162515d6ded45c39d77be9b095c7ae4c3 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -59,11 +60,23 @@ def test_openapi_schema(client: TestClient):
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {
-                                "title": "Q",
-                                "type": "array",
-                                "items": {"type": "string"},
-                            },
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [
+                                        {"type": "array", "items": {"type": "string"}},
+                                        {"type": "null"},
+                                    ],
+                                    "title": "Q",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {
+                                    "title": "Q",
+                                    "type": "array",
+                                    "items": {"type": "string"},
+                                }
+                            ),
                             "name": "q",
                             "in": "query",
                         }
index f2c2a5a33e8b9392a54354e21d57a1aa5697d819..26ac56b2f1bd8a38a236cd000cda49fed7a58bb6 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py39
@@ -59,11 +60,23 @@ def test_openapi_schema(client: TestClient):
                     "parameters": [
                         {
                             "required": False,
-                            "schema": {
-                                "title": "Q",
-                                "type": "array",
-                                "items": {"type": "string"},
-                            },
+                            "schema": IsDict(
+                                {
+                                    "anyOf": [
+                                        {"type": "array", "items": {"type": "string"}},
+                                        {"type": "null"},
+                                    ],
+                                    "title": "Q",
+                                }
+                            )
+                            | IsDict(
+                                # TODO: remove when deprecating Pydantic v1
+                                {
+                                    "title": "Q",
+                                    "type": "array",
+                                    "items": {"type": "string"},
+                                }
+                            ),
                             "name": "q",
                             "in": "query",
                         }
index 84c736180f9ce830c3b15fddbb8894e7678da731..91cc2b6365a16a869d8226fee2fe3fc39a741543 100644 (file)
@@ -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.request_files.tutorial001 import app
 
@@ -19,13 +21,59 @@ file_required = {
 def test_post_form_no_body():
     response = client.post("/files/")
     assert response.status_code == 422, response.text
-    assert response.json() == file_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "file"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "file"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
+        }
+    )
 
 
 def test_post_body_json():
     response = client.post("/files/", json={"file": "Foo"})
     assert response.status_code == 422, response.text
-    assert response.json() == file_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "file"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "file"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
+        }
+    )
 
 
 def test_post_file(tmp_path):
index 8ebe4eafdd6a30772ac2425c94cb78f773490ec7..42f75442a54899ede263fde35862ade21ede36e5 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.request_files.tutorial001_02 import app
@@ -53,9 +54,22 @@ def test_openapi_schema():
                     "requestBody": {
                         "content": {
                             "multipart/form-data": {
-                                "schema": {
-                                    "$ref": "#/components/schemas/Body_create_file_files__post"
-                                }
+                                "schema": IsDict(
+                                    {
+                                        "allOf": [
+                                            {
+                                                "$ref": "#/components/schemas/Body_create_file_files__post"
+                                            }
+                                        ],
+                                        "title": "Body",
+                                    }
+                                )
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "$ref": "#/components/schemas/Body_create_file_files__post"
+                                    }
+                                )
                             }
                         }
                     },
@@ -84,9 +98,22 @@ def test_openapi_schema():
                     "requestBody": {
                         "content": {
                             "multipart/form-data": {
-                                "schema": {
-                                    "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
-                                }
+                                "schema": IsDict(
+                                    {
+                                        "allOf": [
+                                            {
+                                                "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
+                                            }
+                                        ],
+                                        "title": "Body",
+                                    }
+                                )
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
+                                    }
+                                )
                             }
                         }
                     },
@@ -115,14 +142,38 @@ def test_openapi_schema():
                     "title": "Body_create_file_files__post",
                     "type": "object",
                     "properties": {
-                        "file": {"title": "File", "type": "string", "format": "binary"}
+                        "file": IsDict(
+                            {
+                                "title": "File",
+                                "anyOf": [
+                                    {"type": "string", "format": "binary"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "File", "type": "string", "format": "binary"}
+                        )
                     },
                 },
                 "Body_create_upload_file_uploadfile__post": {
                     "title": "Body_create_upload_file_uploadfile__post",
                     "type": "object",
                     "properties": {
-                        "file": {"title": "File", "type": "string", "format": "binary"}
+                        "file": IsDict(
+                            {
+                                "title": "File",
+                                "anyOf": [
+                                    {"type": "string", "format": "binary"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "File", "type": "string", "format": "binary"}
+                        )
                     },
                 },
                 "HTTPValidationError": {
index 5da8b320be055f1ad5a4a8e1e9341fbf7195ab91..f63eb339c4263474e00093a3f9de6d0706d9a05c 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.request_files.tutorial001_02_an import app
@@ -53,9 +54,22 @@ def test_openapi_schema():
                     "requestBody": {
                         "content": {
                             "multipart/form-data": {
-                                "schema": {
-                                    "$ref": "#/components/schemas/Body_create_file_files__post"
-                                }
+                                "schema": IsDict(
+                                    {
+                                        "allOf": [
+                                            {
+                                                "$ref": "#/components/schemas/Body_create_file_files__post"
+                                            }
+                                        ],
+                                        "title": "Body",
+                                    }
+                                )
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "$ref": "#/components/schemas/Body_create_file_files__post"
+                                    }
+                                )
                             }
                         }
                     },
@@ -84,9 +98,22 @@ def test_openapi_schema():
                     "requestBody": {
                         "content": {
                             "multipart/form-data": {
-                                "schema": {
-                                    "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
-                                }
+                                "schema": IsDict(
+                                    {
+                                        "allOf": [
+                                            {
+                                                "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
+                                            }
+                                        ],
+                                        "title": "Body",
+                                    }
+                                )
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
+                                    }
+                                )
                             }
                         }
                     },
@@ -115,14 +142,38 @@ def test_openapi_schema():
                     "title": "Body_create_file_files__post",
                     "type": "object",
                     "properties": {
-                        "file": {"title": "File", "type": "string", "format": "binary"}
+                        "file": IsDict(
+                            {
+                                "title": "File",
+                                "anyOf": [
+                                    {"type": "string", "format": "binary"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "File", "type": "string", "format": "binary"}
+                        )
                     },
                 },
                 "Body_create_upload_file_uploadfile__post": {
                     "title": "Body_create_upload_file_uploadfile__post",
                     "type": "object",
                     "properties": {
-                        "file": {"title": "File", "type": "string", "format": "binary"}
+                        "file": IsDict(
+                            {
+                                "title": "File",
+                                "anyOf": [
+                                    {"type": "string", "format": "binary"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "File", "type": "string", "format": "binary"}
+                        )
                     },
                 },
                 "HTTPValidationError": {
index 166f59b1a1a8b05b310a94296db55e2e8d9d836a..94b6ac67ee02cd294f2ae09201979990546bc201 100644 (file)
@@ -1,6 +1,7 @@
 from pathlib import Path
 
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -65,9 +66,22 @@ def test_openapi_schema(client: TestClient):
                     "requestBody": {
                         "content": {
                             "multipart/form-data": {
-                                "schema": {
-                                    "$ref": "#/components/schemas/Body_create_file_files__post"
-                                }
+                                "schema": IsDict(
+                                    {
+                                        "allOf": [
+                                            {
+                                                "$ref": "#/components/schemas/Body_create_file_files__post"
+                                            }
+                                        ],
+                                        "title": "Body",
+                                    }
+                                )
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "$ref": "#/components/schemas/Body_create_file_files__post"
+                                    }
+                                )
                             }
                         }
                     },
@@ -96,9 +110,22 @@ def test_openapi_schema(client: TestClient):
                     "requestBody": {
                         "content": {
                             "multipart/form-data": {
-                                "schema": {
-                                    "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
-                                }
+                                "schema": IsDict(
+                                    {
+                                        "allOf": [
+                                            {
+                                                "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
+                                            }
+                                        ],
+                                        "title": "Body",
+                                    }
+                                )
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
+                                    }
+                                )
                             }
                         }
                     },
@@ -127,14 +154,38 @@ def test_openapi_schema(client: TestClient):
                     "title": "Body_create_file_files__post",
                     "type": "object",
                     "properties": {
-                        "file": {"title": "File", "type": "string", "format": "binary"}
+                        "file": IsDict(
+                            {
+                                "title": "File",
+                                "anyOf": [
+                                    {"type": "string", "format": "binary"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "File", "type": "string", "format": "binary"}
+                        )
                     },
                 },
                 "Body_create_upload_file_uploadfile__post": {
                     "title": "Body_create_upload_file_uploadfile__post",
                     "type": "object",
                     "properties": {
-                        "file": {"title": "File", "type": "string", "format": "binary"}
+                        "file": IsDict(
+                            {
+                                "title": "File",
+                                "anyOf": [
+                                    {"type": "string", "format": "binary"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "File", "type": "string", "format": "binary"}
+                        )
                     },
                 },
                 "HTTPValidationError": {
index 02ea604b2bbbff1ea79e385bbc84f5d9f2572f0c..fcb39f8f185758ccbb9c682acca8a70829d42022 100644 (file)
@@ -1,6 +1,7 @@
 from pathlib import Path
 
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py39
@@ -65,9 +66,22 @@ def test_openapi_schema(client: TestClient):
                     "requestBody": {
                         "content": {
                             "multipart/form-data": {
-                                "schema": {
-                                    "$ref": "#/components/schemas/Body_create_file_files__post"
-                                }
+                                "schema": IsDict(
+                                    {
+                                        "allOf": [
+                                            {
+                                                "$ref": "#/components/schemas/Body_create_file_files__post"
+                                            }
+                                        ],
+                                        "title": "Body",
+                                    }
+                                )
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "$ref": "#/components/schemas/Body_create_file_files__post"
+                                    }
+                                )
                             }
                         }
                     },
@@ -96,9 +110,22 @@ def test_openapi_schema(client: TestClient):
                     "requestBody": {
                         "content": {
                             "multipart/form-data": {
-                                "schema": {
-                                    "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
-                                }
+                                "schema": IsDict(
+                                    {
+                                        "allOf": [
+                                            {
+                                                "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
+                                            }
+                                        ],
+                                        "title": "Body",
+                                    }
+                                )
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
+                                    }
+                                )
                             }
                         }
                     },
@@ -127,14 +154,38 @@ def test_openapi_schema(client: TestClient):
                     "title": "Body_create_file_files__post",
                     "type": "object",
                     "properties": {
-                        "file": {"title": "File", "type": "string", "format": "binary"}
+                        "file": IsDict(
+                            {
+                                "title": "File",
+                                "anyOf": [
+                                    {"type": "string", "format": "binary"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "File", "type": "string", "format": "binary"}
+                        )
                     },
                 },
                 "Body_create_upload_file_uploadfile__post": {
                     "title": "Body_create_upload_file_uploadfile__post",
                     "type": "object",
                     "properties": {
-                        "file": {"title": "File", "type": "string", "format": "binary"}
+                        "file": IsDict(
+                            {
+                                "title": "File",
+                                "anyOf": [
+                                    {"type": "string", "format": "binary"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "File", "type": "string", "format": "binary"}
+                        )
                     },
                 },
                 "HTTPValidationError": {
index c753e14d164bc0659147f86ba30ccb0e1ec97ce9..a700752a302b90bb019c0ab3b63fde745215997d 100644 (file)
@@ -1,6 +1,7 @@
 from pathlib import Path
 
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -65,9 +66,22 @@ def test_openapi_schema(client: TestClient):
                     "requestBody": {
                         "content": {
                             "multipart/form-data": {
-                                "schema": {
-                                    "$ref": "#/components/schemas/Body_create_file_files__post"
-                                }
+                                "schema": IsDict(
+                                    {
+                                        "allOf": [
+                                            {
+                                                "$ref": "#/components/schemas/Body_create_file_files__post"
+                                            }
+                                        ],
+                                        "title": "Body",
+                                    }
+                                )
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "$ref": "#/components/schemas/Body_create_file_files__post"
+                                    }
+                                )
                             }
                         }
                     },
@@ -96,9 +110,22 @@ def test_openapi_schema(client: TestClient):
                     "requestBody": {
                         "content": {
                             "multipart/form-data": {
-                                "schema": {
-                                    "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
-                                }
+                                "schema": IsDict(
+                                    {
+                                        "allOf": [
+                                            {
+                                                "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
+                                            }
+                                        ],
+                                        "title": "Body",
+                                    }
+                                )
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "$ref": "#/components/schemas/Body_create_upload_file_uploadfile__post"
+                                    }
+                                )
                             }
                         }
                     },
@@ -127,14 +154,38 @@ def test_openapi_schema(client: TestClient):
                     "title": "Body_create_file_files__post",
                     "type": "object",
                     "properties": {
-                        "file": {"title": "File", "type": "string", "format": "binary"}
+                        "file": IsDict(
+                            {
+                                "title": "File",
+                                "anyOf": [
+                                    {"type": "string", "format": "binary"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "File", "type": "string", "format": "binary"}
+                        )
                     },
                 },
                 "Body_create_upload_file_uploadfile__post": {
                     "title": "Body_create_upload_file_uploadfile__post",
                     "type": "object",
                     "properties": {
-                        "file": {"title": "File", "type": "string", "format": "binary"}
+                        "file": IsDict(
+                            {
+                                "title": "File",
+                                "anyOf": [
+                                    {"type": "string", "format": "binary"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "File", "type": "string", "format": "binary"}
+                        )
                     },
                 },
                 "HTTPValidationError": {
index 6eb2d55dc81e3bc92cf25f92a6bf15eba4c69d30..3021eb3c3c98e33e5d6afcbb74fbaa8fbf6d4b22 100644 (file)
@@ -1,31 +1,68 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
 
 from docs_src.request_files.tutorial001_an import app
 
 client = TestClient(app)
 
 
-file_required = {
-    "detail": [
-        {
-            "loc": ["body", "file"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        }
-    ]
-}
-
-
 def test_post_form_no_body():
     response = client.post("/files/")
     assert response.status_code == 422, response.text
-    assert response.json() == file_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "file"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "file"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
+        }
+    )
 
 
 def test_post_body_json():
     response = client.post("/files/", json={"file": "Foo"})
     assert response.status_code == 422, response.text
-    assert response.json() == file_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "file"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "file"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
+        }
+    )
 
 
 def test_post_file(tmp_path):
index 4e3ef686946c2aeeb5e3f33bd4b639950ce7390b..04f3a4693bfcf6ad76590d4345181ed853041fc8 100644 (file)
@@ -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,29 +14,64 @@ def get_client():
     return client
 
 
-file_required = {
-    "detail": [
-        {
-            "loc": ["body", "file"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        }
-    ]
-}
-
-
 @needs_py39
 def test_post_form_no_body(client: TestClient):
     response = client.post("/files/")
     assert response.status_code == 422, response.text
-    assert response.json() == file_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "file"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "file"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
+        }
+    )
 
 
 @needs_py39
 def test_post_body_json(client: TestClient):
     response = client.post("/files/", json={"file": "Foo"})
     assert response.status_code == 422, response.text
-    assert response.json() == file_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "file"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "file"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
+        }
+    )
 
 
 @needs_py39
index 65a8a9e61aa1af4e2124343c8fecb42d4b3f80a4..ed9680b62b78e284a819fa0245033fe33df015ff 100644 (file)
@@ -1,31 +1,68 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
 
 from docs_src.request_files.tutorial002 import app
 
 client = TestClient(app)
 
 
-file_required = {
-    "detail": [
-        {
-            "loc": ["body", "files"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        }
-    ]
-}
-
-
 def test_post_form_no_body():
     response = client.post("/files/")
     assert response.status_code == 422, response.text
-    assert response.json() == file_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "files"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "files"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
+        }
+    )
 
 
 def test_post_body_json():
     response = client.post("/files/", json={"file": "Foo"})
     assert response.status_code == 422, response.text
-    assert response.json() == file_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "files"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "files"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
+        }
+    )
 
 
 def test_post_files(tmp_path):
index 52a8e1964807185aed89ffeea98dcebf86627adf..ea8c1216c0e42b5cccb128692e1c01fc0c928e39 100644 (file)
@@ -1,31 +1,68 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
 
 from docs_src.request_files.tutorial002_an import app
 
 client = TestClient(app)
 
 
-file_required = {
-    "detail": [
-        {
-            "loc": ["body", "files"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        }
-    ]
-}
-
-
 def test_post_form_no_body():
     response = client.post("/files/")
     assert response.status_code == 422, response.text
-    assert response.json() == file_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "files"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "files"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
+        }
+    )
 
 
 def test_post_body_json():
     response = client.post("/files/", json={"file": "Foo"})
     assert response.status_code == 422, response.text
-    assert response.json() == file_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "files"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "files"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
+        }
+    )
 
 
 def test_post_files(tmp_path):
index 6594e0116ce24f24e715785b71c7340a8cebb160..6d587783677ecbc14e00f1e66ea333aabd4dfcc3 100644 (file)
@@ -1,6 +1,8 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi import FastAPI
 from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
 
 from ...utils import needs_py39
 
@@ -18,29 +20,64 @@ def get_client(app: FastAPI):
     return client
 
 
-file_required = {
-    "detail": [
-        {
-            "loc": ["body", "files"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        }
-    ]
-}
-
-
 @needs_py39
 def test_post_form_no_body(client: TestClient):
     response = client.post("/files/")
     assert response.status_code == 422, response.text
-    assert response.json() == file_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "files"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "files"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
+        }
+    )
 
 
 @needs_py39
 def test_post_body_json(client: TestClient):
     response = client.post("/files/", json={"file": "Foo"})
     assert response.status_code == 422, response.text
-    assert response.json() == file_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "files"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "files"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
+        }
+    )
 
 
 @needs_py39
index bfe964604ec1b06a4ed82d04627047cf71187ac0..2d0445421b54b81993976da05a55580138f42585 100644 (file)
@@ -1,6 +1,8 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi import FastAPI
 from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
 
 from ...utils import needs_py39
 
@@ -33,14 +35,60 @@ file_required = {
 def test_post_form_no_body(client: TestClient):
     response = client.post("/files/")
     assert response.status_code == 422, response.text
-    assert response.json() == file_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "files"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "files"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
+        }
+    )
 
 
 @needs_py39
 def test_post_body_json(client: TestClient):
     response = client.post("/files/", json={"file": "Foo"})
     assert response.status_code == 422, response.text
-    assert response.json() == file_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "files"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                }
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "files"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
+        }
+    )
 
 
 @needs_py39
index 4a2a7abe95016f64458a0161a7dc6c7f2c6923a4..805daeb10ff2e5fb9f3a90852c8341d44881aa2a 100644 (file)
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
 
-from docs_src.request_forms.tutorial001 import app
 
-client = TestClient(app)
+@pytest.fixture(name="client")
+def get_client():
+    from docs_src.request_forms.tutorial001 import app
 
+    client = TestClient(app)
+    return client
 
-password_required = {
-    "detail": [
+
+def test_post_body_form(client: TestClient):
+    response = client.post("/login/", data={"username": "Foo", "password": "secret"})
+    assert response.status_code == 200
+    assert response.json() == {"username": "Foo"}
+
+
+def test_post_body_form_no_password(client: TestClient):
+    response = client.post("/login/", data={"username": "Foo"})
+    assert response.status_code == 422
+    assert response.json() == IsDict(
         {
-            "loc": ["body", "password"],
-            "msg": "field required",
-            "type": "value_error.missing",
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "password"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                }
+            ]
         }
-    ]
-}
-username_required = {
-    "detail": [
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
         {
-            "loc": ["body", "username"],
-            "msg": "field required",
-            "type": "value_error.missing",
+            "detail": [
+                {
+                    "loc": ["body", "password"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
         }
-    ]
-}
-username_and_password_required = {
-    "detail": [
+    )
+
+
+def test_post_body_form_no_username(client: TestClient):
+    response = client.post("/login/", data={"password": "secret"})
+    assert response.status_code == 422
+    assert response.json() == IsDict(
         {
-            "loc": ["body", "username"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        },
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "username"],
+                    "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", "username"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
+        }
+    )
 
 
-@pytest.mark.parametrize(
-    "path,body,expected_status,expected_response",
-    [
-        (
-            "/login/",
-            {"username": "Foo", "password": "secret"},
-            200,
-            {"username": "Foo"},
-        ),
-        ("/login/", {"username": "Foo"}, 422, password_required),
-        ("/login/", {"password": "secret"}, 422, username_required),
-        ("/login/", None, 422, username_and_password_required),
-    ],
-)
-def test_post_body_form(path, body, expected_status, expected_response):
-    response = client.post(path, data=body)
-    assert response.status_code == expected_status
-    assert response.json() == expected_response
+def test_post_body_form_no_data(client: TestClient):
+    response = client.post("/login/")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "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
+        {
+            "detail": [
+                {
+                    "loc": ["body", "username"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "password"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
-def test_post_body_json():
+def test_post_body_json(client: TestClient):
     response = client.post("/login/", json={"username": "Foo", "password": "secret"})
     assert response.status_code == 422, response.text
-    assert response.json() == username_and_password_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "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
+        {
+            "detail": [
+                {
+                    "loc": ["body", "username"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "password"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
-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() == {
index 347361344bad6190ee09c24210b687e7e1c4ae20..c43a0b69558027d6dff7d292a49c1bbfe3434887 100644 (file)
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
 
-from docs_src.request_forms.tutorial001_an import app
 
-client = TestClient(app)
+@pytest.fixture(name="client")
+def get_client():
+    from docs_src.request_forms.tutorial001_an import app
 
+    client = TestClient(app)
+    return client
 
-password_required = {
-    "detail": [
+
+def test_post_body_form(client: TestClient):
+    response = client.post("/login/", data={"username": "Foo", "password": "secret"})
+    assert response.status_code == 200
+    assert response.json() == {"username": "Foo"}
+
+
+def test_post_body_form_no_password(client: TestClient):
+    response = client.post("/login/", data={"username": "Foo"})
+    assert response.status_code == 422
+    assert response.json() == IsDict(
         {
-            "loc": ["body", "password"],
-            "msg": "field required",
-            "type": "value_error.missing",
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "password"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                }
+            ]
         }
-    ]
-}
-username_required = {
-    "detail": [
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
         {
-            "loc": ["body", "username"],
-            "msg": "field required",
-            "type": "value_error.missing",
+            "detail": [
+                {
+                    "loc": ["body", "password"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
         }
-    ]
-}
-username_and_password_required = {
-    "detail": [
+    )
+
+
+def test_post_body_form_no_username(client: TestClient):
+    response = client.post("/login/", data={"password": "secret"})
+    assert response.status_code == 422
+    assert response.json() == IsDict(
         {
-            "loc": ["body", "username"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        },
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "username"],
+                    "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", "username"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
+        }
+    )
 
 
-@pytest.mark.parametrize(
-    "path,body,expected_status,expected_response",
-    [
-        (
-            "/login/",
-            {"username": "Foo", "password": "secret"},
-            200,
-            {"username": "Foo"},
-        ),
-        ("/login/", {"username": "Foo"}, 422, password_required),
-        ("/login/", {"password": "secret"}, 422, username_required),
-        ("/login/", None, 422, username_and_password_required),
-    ],
-)
-def test_post_body_form(path, body, expected_status, expected_response):
-    response = client.post(path, data=body)
-    assert response.status_code == expected_status
-    assert response.json() == expected_response
+def test_post_body_form_no_data(client: TestClient):
+    response = client.post("/login/")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "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
+        {
+            "detail": [
+                {
+                    "loc": ["body", "username"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "password"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
-def test_post_body_json():
+def test_post_body_json(client: TestClient):
     response = client.post("/login/", json={"username": "Foo", "password": "secret"})
     assert response.status_code == 422, response.text
-    assert response.json() == username_and_password_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "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
+        {
+            "detail": [
+                {
+                    "loc": ["body", "username"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "password"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
-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() == {
index e65a8823e72897a1df12e8a9571da4be9a89f987..078b812aa5b28e4a661d4c8c0580c5eb2381fd85 100644 (file)
@@ -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,68 +14,155 @@ def get_client():
     return client
 
 
-password_required = {
-    "detail": [
+@needs_py39
+def test_post_body_form(client: TestClient):
+    response = client.post("/login/", data={"username": "Foo", "password": "secret"})
+    assert response.status_code == 200
+    assert response.json() == {"username": "Foo"}
+
+
+@needs_py39
+def test_post_body_form_no_password(client: TestClient):
+    response = client.post("/login/", data={"username": "Foo"})
+    assert response.status_code == 422
+    assert response.json() == IsDict(
         {
-            "loc": ["body", "password"],
-            "msg": "field required",
-            "type": "value_error.missing",
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "password"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                }
+            ]
         }
-    ]
-}
-username_required = {
-    "detail": [
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
         {
-            "loc": ["body", "username"],
-            "msg": "field required",
-            "type": "value_error.missing",
+            "detail": [
+                {
+                    "loc": ["body", "password"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
         }
-    ]
-}
-username_and_password_required = {
-    "detail": [
+    )
+
+
+@needs_py39
+def test_post_body_form_no_username(client: TestClient):
+    response = client.post("/login/", data={"password": "secret"})
+    assert response.status_code == 422
+    assert response.json() == IsDict(
         {
-            "loc": ["body", "username"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        },
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "username"],
+                    "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", "username"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                }
+            ]
+        }
+    )
 
 
 @needs_py39
-@pytest.mark.parametrize(
-    "path,body,expected_status,expected_response",
-    [
-        (
-            "/login/",
-            {"username": "Foo", "password": "secret"},
-            200,
-            {"username": "Foo"},
-        ),
-        ("/login/", {"username": "Foo"}, 422, password_required),
-        ("/login/", {"password": "secret"}, 422, username_required),
-        ("/login/", None, 422, username_and_password_required),
-    ],
-)
-def test_post_body_form(
-    path, body, expected_status, expected_response, client: TestClient
-):
-    response = client.post(path, data=body)
-    assert response.status_code == expected_status
-    assert response.json() == expected_response
+def test_post_body_form_no_data(client: TestClient):
+    response = client.post("/login/")
+    assert response.status_code == 422
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "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
+        {
+            "detail": [
+                {
+                    "loc": ["body", "username"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "password"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
 @needs_py39
 def test_post_body_json(client: TestClient):
     response = client.post("/login/", json={"username": "Foo", "password": "secret"})
     assert response.status_code == 422, response.text
-    assert response.json() == username_and_password_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "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
+        {
+            "detail": [
+                {
+                    "loc": ["body", "username"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "password"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
 @needs_py39
index be12656d2c460c407ed0ef1f6f30dcc00656e7f1..cac58639f1fcd34face76c3718319b35d1a4b610 100644 (file)
+import pytest
+from dirty_equals import IsDict
+from fastapi import FastAPI
 from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
 
-from docs_src.request_forms_and_files.tutorial001 import app
 
-client = TestClient(app)
+@pytest.fixture(name="app")
+def get_app():
+    from docs_src.request_forms_and_files.tutorial001 import app
 
+    return app
 
-file_required = {
-    "detail": [
-        {
-            "loc": ["body", "file"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        },
-        {
-            "loc": ["body", "fileb"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        },
-    ]
-}
-
-token_required = {
-    "detail": [
-        {
-            "loc": ["body", "fileb"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        },
-        {
-            "loc": ["body", "token"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        },
-    ]
-}
 
-# {'detail': [, {'loc': ['body', 'token'], 'msg': 'field required', 'type': 'value_error.missing'}]}
-
-file_and_token_required = {
-    "detail": [
-        {
-            "loc": ["body", "file"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        },
-        {
-            "loc": ["body", "fileb"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        },
-        {
-            "loc": ["body", "token"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        },
-    ]
-}
+@pytest.fixture(name="client")
+def get_client(app: FastAPI):
+    client = TestClient(app)
+    return client
 
 
-def test_post_form_no_body():
+def test_post_form_no_body(client: TestClient):
     response = client.post("/files/")
     assert response.status_code == 422, response.text
-    assert response.json() == file_and_token_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "file"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["body", "fileb"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["body", "token"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "file"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "fileb"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "token"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
-def test_post_form_no_file():
+def test_post_form_no_file(client: TestClient):
     response = client.post("/files/", data={"token": "foo"})
     assert response.status_code == 422, response.text
-    assert response.json() == file_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "file"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["body", "fileb"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "file"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "fileb"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
-def test_post_body_json():
+def test_post_body_json(client: TestClient):
     response = client.post("/files/", json={"file": "Foo", "token": "Bar"})
     assert response.status_code == 422, response.text
-    assert response.json() == file_and_token_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "file"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["body", "fileb"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["body", "token"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "file"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "fileb"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "token"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
-def test_post_file_no_token(tmp_path):
+def test_post_file_no_token(tmp_path, app: FastAPI):
     path = tmp_path / "test.txt"
     path.write_bytes(b"<file content>")
 
@@ -84,10 +173,45 @@ def test_post_file_no_token(tmp_path):
     with path.open("rb") as file:
         response = client.post("/files/", files={"file": file})
     assert response.status_code == 422, response.text
-    assert response.json() == token_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "fileb"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["body", "token"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "fileb"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "token"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
-def test_post_files_and_token(tmp_path):
+def test_post_files_and_token(tmp_path, app: FastAPI):
     patha = tmp_path / "test.txt"
     pathb = tmp_path / "testb.txt"
     patha.write_text("<file content>")
@@ -108,7 +232,7 @@ def test_post_files_and_token(tmp_path):
     }
 
 
-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() == {
index a5fcb3a94dbdc6405ad2f2a7ebdc0bd5243eb062..009568048ee4050fc4eeed55260b28c1ff3cd9d8 100644 (file)
+import pytest
+from dirty_equals import IsDict
+from fastapi import FastAPI
 from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
 
-from docs_src.request_forms_and_files.tutorial001_an import app
 
-client = TestClient(app)
+@pytest.fixture(name="app")
+def get_app():
+    from docs_src.request_forms_and_files.tutorial001_an import app
 
+    return app
 
-file_required = {
-    "detail": [
-        {
-            "loc": ["body", "file"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        },
-        {
-            "loc": ["body", "fileb"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        },
-    ]
-}
-
-token_required = {
-    "detail": [
-        {
-            "loc": ["body", "fileb"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        },
-        {
-            "loc": ["body", "token"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        },
-    ]
-}
 
-# {'detail': [, {'loc': ['body', 'token'], 'msg': 'field required', 'type': 'value_error.missing'}]}
-
-file_and_token_required = {
-    "detail": [
-        {
-            "loc": ["body", "file"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        },
-        {
-            "loc": ["body", "fileb"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        },
-        {
-            "loc": ["body", "token"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        },
-    ]
-}
+@pytest.fixture(name="client")
+def get_client(app: FastAPI):
+    client = TestClient(app)
+    return client
 
 
-def test_post_form_no_body():
+def test_post_form_no_body(client: TestClient):
     response = client.post("/files/")
     assert response.status_code == 422, response.text
-    assert response.json() == file_and_token_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "file"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["body", "fileb"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["body", "token"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "file"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "fileb"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "token"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
-def test_post_form_no_file():
+def test_post_form_no_file(client: TestClient):
     response = client.post("/files/", data={"token": "foo"})
     assert response.status_code == 422, response.text
-    assert response.json() == file_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "file"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["body", "fileb"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "file"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "fileb"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
-def test_post_body_json():
+def test_post_body_json(client: TestClient):
     response = client.post("/files/", json={"file": "Foo", "token": "Bar"})
     assert response.status_code == 422, response.text
-    assert response.json() == file_and_token_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "file"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["body", "fileb"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["body", "token"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "file"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "fileb"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "token"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
-def test_post_file_no_token(tmp_path):
+def test_post_file_no_token(tmp_path, app: FastAPI):
     path = tmp_path / "test.txt"
     path.write_bytes(b"<file content>")
 
@@ -84,10 +173,45 @@ def test_post_file_no_token(tmp_path):
     with path.open("rb") as file:
         response = client.post("/files/", files={"file": file})
     assert response.status_code == 422, response.text
-    assert response.json() == token_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "fileb"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["body", "token"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "fileb"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "token"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
-def test_post_files_and_token(tmp_path):
+def test_post_files_and_token(tmp_path, app: FastAPI):
     patha = tmp_path / "test.txt"
     pathb = tmp_path / "testb.txt"
     patha.write_text("<file content>")
@@ -108,7 +232,7 @@ def test_post_files_and_token(tmp_path):
     }
 
 
-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() == {
index 6eacb2fcf20e8f0d3d187f8f2ca9619241b565ca..3d007e90ba4f9f23ac6e01c30a7b60c9bbbe030d 100644 (file)
@@ -1,6 +1,8 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi import FastAPI
 from fastapi.testclient import TestClient
+from fastapi.utils import match_pydantic_error_url
 
 from ...utils import needs_py39
 
@@ -18,78 +20,154 @@ def get_client(app: FastAPI):
     return client
 
 
-file_required = {
-    "detail": [
-        {
-            "loc": ["body", "file"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        },
-        {
-            "loc": ["body", "fileb"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        },
-    ]
-}
-
-token_required = {
-    "detail": [
-        {
-            "loc": ["body", "fileb"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        },
-        {
-            "loc": ["body", "token"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        },
-    ]
-}
-
-# {'detail': [, {'loc': ['body', 'token'], 'msg': 'field required', 'type': 'value_error.missing'}]}
-
-file_and_token_required = {
-    "detail": [
-        {
-            "loc": ["body", "file"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        },
-        {
-            "loc": ["body", "fileb"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        },
-        {
-            "loc": ["body", "token"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        },
-    ]
-}
-
-
 @needs_py39
 def test_post_form_no_body(client: TestClient):
     response = client.post("/files/")
     assert response.status_code == 422, response.text
-    assert response.json() == file_and_token_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "file"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["body", "fileb"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["body", "token"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "file"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "fileb"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "token"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
 @needs_py39
 def test_post_form_no_file(client: TestClient):
     response = client.post("/files/", data={"token": "foo"})
     assert response.status_code == 422, response.text
-    assert response.json() == file_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "file"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["body", "fileb"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "file"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "fileb"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
 @needs_py39
 def test_post_body_json(client: TestClient):
     response = client.post("/files/", json={"file": "Foo", "token": "Bar"})
     assert response.status_code == 422, response.text
-    assert response.json() == file_and_token_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "file"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["body", "fileb"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["body", "token"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "file"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "fileb"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "token"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
 @needs_py39
@@ -101,7 +179,42 @@ def test_post_file_no_token(tmp_path, app: FastAPI):
     with path.open("rb") as file:
         response = client.post("/files/", files={"file": file})
     assert response.status_code == 422, response.text
-    assert response.json() == token_required
+    assert response.json() == IsDict(
+        {
+            "detail": [
+                {
+                    "type": "missing",
+                    "loc": ["body", "fileb"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+                {
+                    "type": "missing",
+                    "loc": ["body", "token"],
+                    "msg": "Field required",
+                    "input": None,
+                    "url": match_pydantic_error_url("missing"),
+                },
+            ]
+        }
+    ) | IsDict(
+        # TODO: remove when deprecating Pydantic v1
+        {
+            "detail": [
+                {
+                    "loc": ["body", "fileb"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+                {
+                    "loc": ["body", "token"],
+                    "msg": "field required",
+                    "type": "value_error.missing",
+                },
+            ]
+        }
+    )
 
 
 @needs_py39
index 9cb0419a33ca821752f8a92277981a4ee875968a..20221399b14fd825b289a3d82d670a43627ba14b 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.response_model.tutorial003 import app
@@ -78,7 +79,16 @@ def test_openapi_schema():
                             "type": "string",
                             "format": "email",
                         },
-                        "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"}
+                        ),
                     },
                 },
                 "UserIn": {
@@ -93,7 +103,16 @@ def test_openapi_schema():
                             "type": "string",
                             "format": "email",
                         },
-                        "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"}
+                        ),
                     },
                 },
                 "ValidationError": {
index 8b8fe514ae48137ec30fbec2d0dca3668c01c3b6..e8f0658f4c38b68df40caa2f5999e84998976df1 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.response_model.tutorial003_01 import app
@@ -78,7 +79,16 @@ def test_openapi_schema():
                             "type": "string",
                             "format": "email",
                         },
-                        "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"}
+                        ),
                     },
                 },
                 "HTTPValidationError": {
@@ -103,7 +113,16 @@ def test_openapi_schema():
                             "type": "string",
                             "format": "email",
                         },
-                        "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"}
+                        ),
                         "password": {"title": "Password", "type": "string"},
                     },
                 },
index 01dc8e71ca3a94a95adac6407417e16e83477c4b..a69f8cc8debed3763f4ba8092775e2a5dd727642 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -87,7 +88,16 @@ def test_openapi_schema(client: TestClient):
                             "type": "string",
                             "format": "email",
                         },
-                        "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"}
+                        ),
                     },
                 },
                 "HTTPValidationError": {
@@ -112,7 +122,16 @@ def test_openapi_schema(client: TestClient):
                             "type": "string",
                             "format": "email",
                         },
-                        "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"}
+                        ),
                         "password": {"title": "Password", "type": "string"},
                     },
                 },
index 602147b1390d1098f54ac2ebb89240d1db95a72a..64dcd6cbdf39cd06a2b69f2ae47c0ed7d5be9774 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -87,7 +88,16 @@ def test_openapi_schema(client: TestClient):
                             "type": "string",
                             "format": "email",
                         },
-                        "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"}
+                        ),
                     },
                 },
                 "UserIn": {
@@ -102,7 +112,16 @@ def test_openapi_schema(client: TestClient):
                             "type": "string",
                             "format": "email",
                         },
-                        "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"}
+                        ),
                     },
                 },
                 "ValidationError": {
index 07af2920727610d1d74eb33d5663631e5ee76b20..8beb847d1df7d36f172a2610e2334e6a5b1a31d1 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.response_model.tutorial004 import app
@@ -83,7 +84,16 @@ def test_openapi_schema():
                     "properties": {
                         "name": {"title": "Name", "type": "string"},
                         "price": {"title": "Price", "type": "number"},
-                        "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"}
+                        ),
                         "tax": {"title": "Tax", "type": "number", "default": 10.5},
                         "tags": {
                             "title": "Tags",
index 90147fbdd2526244bc94c84c7e976386e01d29eb..28eb88c3478fd49bd3b8ca0d505cca5ed52bd3e1 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -91,7 +92,16 @@ def test_openapi_schema(client: TestClient):
                     "properties": {
                         "name": {"title": "Name", "type": "string"},
                         "price": {"title": "Price", "type": "number"},
-                        "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"}
+                        ),
                         "tax": {"title": "Tax", "type": "number", "default": 10.5},
                         "tags": {
                             "title": "Tags",
index 740a49590e5469627b0b4feeb898c87c17370cc2..9e1a21f8db6ae0a981cd50c168f256b33ff9b038 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py39
@@ -91,7 +92,16 @@ def test_openapi_schema(client: TestClient):
                     "properties": {
                         "name": {"title": "Name", "type": "string"},
                         "price": {"title": "Price", "type": "number"},
-                        "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"}
+                        ),
                         "tax": {"title": "Tax", "type": "number", "default": 10.5},
                         "tags": {
                             "title": "Tags",
index e8c8946c543ef9e46d43337d34d1a976c4317c82..06e5d0fd153a3bf83c32c8584caa2f21d024d54d 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.response_model.tutorial005 import app
@@ -106,7 +107,16 @@ def test_openapi_schema():
                     "properties": {
                         "name": {"title": "Name", "type": "string"},
                         "price": {"title": "Price", "type": "number"},
-                        "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"}
+                        ),
                         "tax": {"title": "Tax", "type": "number", "default": 10.5},
                     },
                 },
index 388e030bd580077438a5f3fc9d02d6d94292ea92..0f15662439b018719d55eb0fcb70685aadb23cb7 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -116,7 +117,16 @@ def test_openapi_schema(client: TestClient):
                     "properties": {
                         "name": {"title": "Name", "type": "string"},
                         "price": {"title": "Price", "type": "number"},
-                        "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"}
+                        ),
                         "tax": {"title": "Tax", "type": "number", "default": 10.5},
                     },
                 },
index 548a3dbd8710a90fd96037a298a176d3aa891da0..6e6152b9f16cbdd3e0f01939d5672a4dea55578c 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.response_model.tutorial006 import app
@@ -106,7 +107,16 @@ def test_openapi_schema():
                     "properties": {
                         "name": {"title": "Name", "type": "string"},
                         "price": {"title": "Price", "type": "number"},
-                        "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"}
+                        ),
                         "tax": {"title": "Tax", "type": "number", "default": 10.5},
                     },
                 },
index 075bb8079044bc759b0b82c52ba5f90b120e4afa..9a980ab5b0ae5b154f11b6962e2d02930760274a 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -116,7 +117,16 @@ def test_openapi_schema(client: TestClient):
                     "properties": {
                         "name": {"title": "Name", "type": "string"},
                         "price": {"title": "Price", "type": "number"},
-                        "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"}
+                        ),
                         "tax": {"title": "Tax", "type": "number", "default": 10.5},
                     },
                 },
diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001.py
new file mode 100644 (file)
index 0000000..98b1873
--- /dev/null
@@ -0,0 +1,133 @@
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_pydanticv2
+
+
+@pytest.fixture(name="client")
+def get_client():
+    from docs_src.schema_extra_example.tutorial001 import app
+
+    client = TestClient(app)
+    return client
+
+
+@needs_pydanticv2
+def test_post_body_example(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={
+            "name": "Foo",
+            "description": "A very nice Item",
+            "price": 35.4,
+            "tax": 3.2,
+        },
+    )
+    assert response.status_code == 200
+
+
+@needs_pydanticv2
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    # insert_assert(response.json())
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "put": {
+                    "summary": "Update Item",
+                    "operationId": "update_item_items__item_id__put",
+                    "parameters": [
+                        {
+                            "name": "item_id",
+                            "in": "path",
+                            "required": True,
+                            "schema": {"type": "integer", "title": "Item Id"},
+                        }
+                    ],
+                    "requestBody": {
+                        "required": True,
+                        "content": {
+                            "application/json": {
+                                "schema": {"$ref": "#/components/schemas/Item"}
+                            }
+                        },
+                    },
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                            "type": "array",
+                            "title": "Detail",
+                        }
+                    },
+                    "type": "object",
+                    "title": "HTTPValidationError",
+                },
+                "Item": {
+                    "properties": {
+                        "name": {"type": "string", "title": "Name"},
+                        "description": {
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                            "title": "Description",
+                        },
+                        "price": {"type": "number", "title": "Price"},
+                        "tax": {
+                            "anyOf": [{"type": "number"}, {"type": "null"}],
+                            "title": "Tax",
+                        },
+                    },
+                    "type": "object",
+                    "required": ["name", "price"],
+                    "title": "Item",
+                    "examples": [
+                        {
+                            "description": "A very nice Item",
+                            "name": "Foo",
+                            "price": 35.4,
+                            "tax": 3.2,
+                        }
+                    ],
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                            "type": "array",
+                            "title": "Location",
+                        },
+                        "msg": {"type": "string", "title": "Message"},
+                        "type": {"type": "string", "title": "Error Type"},
+                    },
+                    "type": "object",
+                    "required": ["loc", "msg", "type"],
+                    "title": "ValidationError",
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_pv1.py
new file mode 100644 (file)
index 0000000..3520ef6
--- /dev/null
@@ -0,0 +1,127 @@
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_pydanticv1
+
+
+@pytest.fixture(name="client")
+def get_client():
+    from docs_src.schema_extra_example.tutorial001_pv1 import app
+
+    client = TestClient(app)
+    return client
+
+
+@needs_pydanticv1
+def test_post_body_example(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={
+            "name": "Foo",
+            "description": "A very nice Item",
+            "price": 35.4,
+            "tax": 3.2,
+        },
+    )
+    assert response.status_code == 200
+
+
+@needs_pydanticv1
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    # insert_assert(response.json())
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "put": {
+                    "summary": "Update Item",
+                    "operationId": "update_item_items__item_id__put",
+                    "parameters": [
+                        {
+                            "required": True,
+                            "schema": {"type": "integer", "title": "Item Id"},
+                            "name": "item_id",
+                            "in": "path",
+                        }
+                    ],
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {"$ref": "#/components/schemas/Item"}
+                            }
+                        },
+                        "required": True,
+                    },
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                            "type": "array",
+                            "title": "Detail",
+                        }
+                    },
+                    "type": "object",
+                    "title": "HTTPValidationError",
+                },
+                "Item": {
+                    "properties": {
+                        "name": {"type": "string", "title": "Name"},
+                        "description": {"type": "string", "title": "Description"},
+                        "price": {"type": "number", "title": "Price"},
+                        "tax": {"type": "number", "title": "Tax"},
+                    },
+                    "type": "object",
+                    "required": ["name", "price"],
+                    "title": "Item",
+                    "examples": [
+                        {
+                            "name": "Foo",
+                            "description": "A very nice Item",
+                            "price": 35.4,
+                            "tax": 3.2,
+                        }
+                    ],
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                            "type": "array",
+                            "title": "Location",
+                        },
+                        "msg": {"type": "string", "title": "Message"},
+                        "type": {"type": "string", "title": "Error Type"},
+                    },
+                    "type": "object",
+                    "required": ["loc", "msg", "type"],
+                    "title": "ValidationError",
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310.py
new file mode 100644 (file)
index 0000000..e63e33c
--- /dev/null
@@ -0,0 +1,135 @@
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310, needs_pydanticv2
+
+
+@pytest.fixture(name="client")
+def get_client():
+    from docs_src.schema_extra_example.tutorial001_py310 import app
+
+    client = TestClient(app)
+    return client
+
+
+@needs_py310
+@needs_pydanticv2
+def test_post_body_example(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={
+            "name": "Foo",
+            "description": "A very nice Item",
+            "price": 35.4,
+            "tax": 3.2,
+        },
+    )
+    assert response.status_code == 200
+
+
+@needs_py310
+@needs_pydanticv2
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    # insert_assert(response.json())
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "put": {
+                    "summary": "Update Item",
+                    "operationId": "update_item_items__item_id__put",
+                    "parameters": [
+                        {
+                            "name": "item_id",
+                            "in": "path",
+                            "required": True,
+                            "schema": {"type": "integer", "title": "Item Id"},
+                        }
+                    ],
+                    "requestBody": {
+                        "required": True,
+                        "content": {
+                            "application/json": {
+                                "schema": {"$ref": "#/components/schemas/Item"}
+                            }
+                        },
+                    },
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                            "type": "array",
+                            "title": "Detail",
+                        }
+                    },
+                    "type": "object",
+                    "title": "HTTPValidationError",
+                },
+                "Item": {
+                    "properties": {
+                        "name": {"type": "string", "title": "Name"},
+                        "description": {
+                            "anyOf": [{"type": "string"}, {"type": "null"}],
+                            "title": "Description",
+                        },
+                        "price": {"type": "number", "title": "Price"},
+                        "tax": {
+                            "anyOf": [{"type": "number"}, {"type": "null"}],
+                            "title": "Tax",
+                        },
+                    },
+                    "type": "object",
+                    "required": ["name", "price"],
+                    "title": "Item",
+                    "examples": [
+                        {
+                            "description": "A very nice Item",
+                            "name": "Foo",
+                            "price": 35.4,
+                            "tax": 3.2,
+                        }
+                    ],
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                            "type": "array",
+                            "title": "Location",
+                        },
+                        "msg": {"type": "string", "title": "Message"},
+                        "type": {"type": "string", "title": "Error Type"},
+                    },
+                    "type": "object",
+                    "required": ["loc", "msg", "type"],
+                    "title": "ValidationError",
+                },
+            }
+        },
+    }
diff --git a/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310_pv1.py b/tests/test_tutorial/test_schema_extra_example/test_tutorial001_py310_pv1.py
new file mode 100644 (file)
index 0000000..e036d6b
--- /dev/null
@@ -0,0 +1,129 @@
+import pytest
+from fastapi.testclient import TestClient
+
+from ...utils import needs_py310, needs_pydanticv1
+
+
+@pytest.fixture(name="client")
+def get_client():
+    from docs_src.schema_extra_example.tutorial001_py310_pv1 import app
+
+    client = TestClient(app)
+    return client
+
+
+@needs_py310
+@needs_pydanticv1
+def test_post_body_example(client: TestClient):
+    response = client.put(
+        "/items/5",
+        json={
+            "name": "Foo",
+            "description": "A very nice Item",
+            "price": 35.4,
+            "tax": 3.2,
+        },
+    )
+    assert response.status_code == 200
+
+
+@needs_py310
+@needs_pydanticv1
+def test_openapi_schema(client: TestClient):
+    response = client.get("/openapi.json")
+    assert response.status_code == 200, response.text
+    # insert_assert(response.json())
+    assert response.json() == {
+        "openapi": "3.1.0",
+        "info": {"title": "FastAPI", "version": "0.1.0"},
+        "paths": {
+            "/items/{item_id}": {
+                "put": {
+                    "summary": "Update Item",
+                    "operationId": "update_item_items__item_id__put",
+                    "parameters": [
+                        {
+                            "required": True,
+                            "schema": {"type": "integer", "title": "Item Id"},
+                            "name": "item_id",
+                            "in": "path",
+                        }
+                    ],
+                    "requestBody": {
+                        "content": {
+                            "application/json": {
+                                "schema": {"$ref": "#/components/schemas/Item"}
+                            }
+                        },
+                        "required": True,
+                    },
+                    "responses": {
+                        "200": {
+                            "description": "Successful Response",
+                            "content": {"application/json": {"schema": {}}},
+                        },
+                        "422": {
+                            "description": "Validation Error",
+                            "content": {
+                                "application/json": {
+                                    "schema": {
+                                        "$ref": "#/components/schemas/HTTPValidationError"
+                                    }
+                                }
+                            },
+                        },
+                    },
+                }
+            }
+        },
+        "components": {
+            "schemas": {
+                "HTTPValidationError": {
+                    "properties": {
+                        "detail": {
+                            "items": {"$ref": "#/components/schemas/ValidationError"},
+                            "type": "array",
+                            "title": "Detail",
+                        }
+                    },
+                    "type": "object",
+                    "title": "HTTPValidationError",
+                },
+                "Item": {
+                    "properties": {
+                        "name": {"type": "string", "title": "Name"},
+                        "description": {"type": "string", "title": "Description"},
+                        "price": {"type": "number", "title": "Price"},
+                        "tax": {"type": "number", "title": "Tax"},
+                    },
+                    "type": "object",
+                    "required": ["name", "price"],
+                    "title": "Item",
+                    "examples": [
+                        {
+                            "name": "Foo",
+                            "description": "A very nice Item",
+                            "price": 35.4,
+                            "tax": 3.2,
+                        }
+                    ],
+                },
+                "ValidationError": {
+                    "properties": {
+                        "loc": {
+                            "items": {
+                                "anyOf": [{"type": "string"}, {"type": "integer"}]
+                            },
+                            "type": "array",
+                            "title": "Location",
+                        },
+                        "msg": {"type": "string", "title": "Message"},
+                        "type": {"type": "string", "title": "Error Type"},
+                    },
+                    "type": "object",
+                    "required": ["loc", "msg", "type"],
+                    "title": "ValidationError",
+                },
+            }
+        },
+    }
index 313cd51d68d9a7e464d6a358200b2c3fdbaaadd1..eac0d1e29bbfd5b6309e1a0e763e701ce3458895 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.schema_extra_example.tutorial004 import app
@@ -41,23 +42,46 @@ def test_openapi_schema():
                     "requestBody": {
                         "content": {
                             "application/json": {
-                                "schema": {
-                                    "allOf": [{"$ref": "#/components/schemas/Item"}],
-                                    "title": "Item",
-                                    "examples": [
-                                        {
-                                            "name": "Foo",
-                                            "description": "A very nice Item",
-                                            "price": 35.4,
-                                            "tax": 3.2,
-                                        },
-                                        {"name": "Bar", "price": "35.4"},
-                                        {
-                                            "name": "Baz",
-                                            "price": "thirty five point four",
-                                        },
-                                    ],
-                                }
+                                "schema": IsDict(
+                                    {
+                                        "$ref": "#/components/schemas/Item",
+                                        "examples": [
+                                            {
+                                                "name": "Foo",
+                                                "description": "A very nice Item",
+                                                "price": 35.4,
+                                                "tax": 3.2,
+                                            },
+                                            {"name": "Bar", "price": "35.4"},
+                                            {
+                                                "name": "Baz",
+                                                "price": "thirty five point four",
+                                            },
+                                        ],
+                                    }
+                                )
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "allOf": [
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ],
+                                        "title": "Item",
+                                        "examples": [
+                                            {
+                                                "name": "Foo",
+                                                "description": "A very nice Item",
+                                                "price": 35.4,
+                                                "tax": 3.2,
+                                            },
+                                            {"name": "Bar", "price": "35.4"},
+                                            {
+                                                "name": "Baz",
+                                                "price": "thirty five point four",
+                                            },
+                                        ],
+                                    }
+                                )
                             }
                         },
                         "required": True,
@@ -100,9 +124,27 @@ 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"}
+                        ),
                         "price": {"title": "Price", "type": "number"},
-                        "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": {
index 353401b78eb2bc102ba789c2f689bcb3e8139509..a9cecd098308d12f74c8591162e5b4cc89c502af 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.schema_extra_example.tutorial004_an import app
@@ -41,23 +42,46 @@ def test_openapi_schema():
                     "requestBody": {
                         "content": {
                             "application/json": {
-                                "schema": {
-                                    "allOf": [{"$ref": "#/components/schemas/Item"}],
-                                    "title": "Item",
-                                    "examples": [
-                                        {
-                                            "name": "Foo",
-                                            "description": "A very nice Item",
-                                            "price": 35.4,
-                                            "tax": 3.2,
-                                        },
-                                        {"name": "Bar", "price": "35.4"},
-                                        {
-                                            "name": "Baz",
-                                            "price": "thirty five point four",
-                                        },
-                                    ],
-                                }
+                                "schema": IsDict(
+                                    {
+                                        "$ref": "#/components/schemas/Item",
+                                        "examples": [
+                                            {
+                                                "name": "Foo",
+                                                "description": "A very nice Item",
+                                                "price": 35.4,
+                                                "tax": 3.2,
+                                            },
+                                            {"name": "Bar", "price": "35.4"},
+                                            {
+                                                "name": "Baz",
+                                                "price": "thirty five point four",
+                                            },
+                                        ],
+                                    }
+                                )
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "allOf": [
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ],
+                                        "title": "Item",
+                                        "examples": [
+                                            {
+                                                "name": "Foo",
+                                                "description": "A very nice Item",
+                                                "price": 35.4,
+                                                "tax": 3.2,
+                                            },
+                                            {"name": "Bar", "price": "35.4"},
+                                            {
+                                                "name": "Baz",
+                                                "price": "thirty five point four",
+                                            },
+                                        ],
+                                    }
+                                )
                             }
                         },
                         "required": True,
@@ -100,9 +124,27 @@ 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"}
+                        ),
                         "price": {"title": "Price", "type": "number"},
-                        "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": {
index 79f4e1e1eb7801372c13dae57d4cb06f051aca61..b6a7355996289fbe071c756b2755c107317026be 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -50,23 +51,46 @@ def test_openapi_schema(client: TestClient):
                     "requestBody": {
                         "content": {
                             "application/json": {
-                                "schema": {
-                                    "allOf": [{"$ref": "#/components/schemas/Item"}],
-                                    "title": "Item",
-                                    "examples": [
-                                        {
-                                            "name": "Foo",
-                                            "description": "A very nice Item",
-                                            "price": 35.4,
-                                            "tax": 3.2,
-                                        },
-                                        {"name": "Bar", "price": "35.4"},
-                                        {
-                                            "name": "Baz",
-                                            "price": "thirty five point four",
-                                        },
-                                    ],
-                                }
+                                "schema": IsDict(
+                                    {
+                                        "$ref": "#/components/schemas/Item",
+                                        "examples": [
+                                            {
+                                                "name": "Foo",
+                                                "description": "A very nice Item",
+                                                "price": 35.4,
+                                                "tax": 3.2,
+                                            },
+                                            {"name": "Bar", "price": "35.4"},
+                                            {
+                                                "name": "Baz",
+                                                "price": "thirty five point four",
+                                            },
+                                        ],
+                                    }
+                                )
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "allOf": [
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ],
+                                        "title": "Item",
+                                        "examples": [
+                                            {
+                                                "name": "Foo",
+                                                "description": "A very nice Item",
+                                                "price": 35.4,
+                                                "tax": 3.2,
+                                            },
+                                            {"name": "Bar", "price": "35.4"},
+                                            {
+                                                "name": "Baz",
+                                                "price": "thirty five point four",
+                                            },
+                                        ],
+                                    }
+                                )
                             }
                         },
                         "required": True,
@@ -109,9 +133,27 @@ def test_openapi_schema(client: TestClient):
                     "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"}
+                        ),
                         "price": {"title": "Price", "type": "number"},
-                        "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": {
index 1ee120705116e05dbf5de28a55de9d49cfc0c052..2493194a00116d937a80481c476e7487332feb57 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py39
@@ -50,23 +51,46 @@ def test_openapi_schema(client: TestClient):
                     "requestBody": {
                         "content": {
                             "application/json": {
-                                "schema": {
-                                    "allOf": [{"$ref": "#/components/schemas/Item"}],
-                                    "title": "Item",
-                                    "examples": [
-                                        {
-                                            "name": "Foo",
-                                            "description": "A very nice Item",
-                                            "price": 35.4,
-                                            "tax": 3.2,
-                                        },
-                                        {"name": "Bar", "price": "35.4"},
-                                        {
-                                            "name": "Baz",
-                                            "price": "thirty five point four",
-                                        },
-                                    ],
-                                }
+                                "schema": IsDict(
+                                    {
+                                        "$ref": "#/components/schemas/Item",
+                                        "examples": [
+                                            {
+                                                "name": "Foo",
+                                                "description": "A very nice Item",
+                                                "price": 35.4,
+                                                "tax": 3.2,
+                                            },
+                                            {"name": "Bar", "price": "35.4"},
+                                            {
+                                                "name": "Baz",
+                                                "price": "thirty five point four",
+                                            },
+                                        ],
+                                    }
+                                )
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "allOf": [
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ],
+                                        "title": "Item",
+                                        "examples": [
+                                            {
+                                                "name": "Foo",
+                                                "description": "A very nice Item",
+                                                "price": 35.4,
+                                                "tax": 3.2,
+                                            },
+                                            {"name": "Bar", "price": "35.4"},
+                                            {
+                                                "name": "Baz",
+                                                "price": "thirty five point four",
+                                            },
+                                        ],
+                                    }
+                                )
                             }
                         },
                         "required": True,
@@ -109,9 +133,27 @@ def test_openapi_schema(client: TestClient):
                     "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"}
+                        ),
                         "price": {"title": "Price", "type": "number"},
-                        "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": {
index b7736840084313f7d90bee27933e633785bd9a92..15f54bd5a61d37c156c7d4bf8d368274fbcac3b9 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -50,23 +51,46 @@ def test_openapi_schema(client: TestClient):
                     "requestBody": {
                         "content": {
                             "application/json": {
-                                "schema": {
-                                    "allOf": [{"$ref": "#/components/schemas/Item"}],
-                                    "title": "Item",
-                                    "examples": [
-                                        {
-                                            "name": "Foo",
-                                            "description": "A very nice Item",
-                                            "price": 35.4,
-                                            "tax": 3.2,
-                                        },
-                                        {"name": "Bar", "price": "35.4"},
-                                        {
-                                            "name": "Baz",
-                                            "price": "thirty five point four",
-                                        },
-                                    ],
-                                }
+                                "schema": IsDict(
+                                    {
+                                        "$ref": "#/components/schemas/Item",
+                                        "examples": [
+                                            {
+                                                "name": "Foo",
+                                                "description": "A very nice Item",
+                                                "price": 35.4,
+                                                "tax": 3.2,
+                                            },
+                                            {"name": "Bar", "price": "35.4"},
+                                            {
+                                                "name": "Baz",
+                                                "price": "thirty five point four",
+                                            },
+                                        ],
+                                    }
+                                )
+                                | IsDict(
+                                    # TODO: remove when deprecating Pydantic v1
+                                    {
+                                        "allOf": [
+                                            {"$ref": "#/components/schemas/Item"}
+                                        ],
+                                        "title": "Item",
+                                        "examples": [
+                                            {
+                                                "name": "Foo",
+                                                "description": "A very nice Item",
+                                                "price": 35.4,
+                                                "tax": 3.2,
+                                            },
+                                            {"name": "Bar", "price": "35.4"},
+                                            {
+                                                "name": "Baz",
+                                                "price": "thirty five point four",
+                                            },
+                                        ],
+                                    }
+                                )
                             }
                         },
                         "required": True,
@@ -109,9 +133,27 @@ def test_openapi_schema(client: TestClient):
                     "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"}
+                        ),
                         "price": {"title": "Price", "type": "number"},
-                        "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": {
index cb5cdaa04d7947a1ea55f3aded38a4112dec0767..18d4680f627f60593bad2c8fbbe42ef93642fa5e 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.security.tutorial003 import app
@@ -126,16 +127,46 @@ def test_openapi_schema():
                     "required": ["username", "password"],
                     "type": "object",
                     "properties": {
-                        "grant_type": {
-                            "title": "Grant Type",
-                            "pattern": "password",
-                            "type": "string",
-                        },
+                        "grant_type": IsDict(
+                            {
+                                "title": "Grant Type",
+                                "anyOf": [
+                                    {"pattern": "password", "type": "string"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Grant Type",
+                                "pattern": "password",
+                                "type": "string",
+                            }
+                        ),
                         "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": {
index 26e68a0299bb26af427efbbfb0346f3b12d406e3..a8f64d0c60dbcb798dbdd9abfbc0d7f8f85a74fa 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.security.tutorial003_an import app
@@ -126,16 +127,46 @@ def test_openapi_schema():
                     "required": ["username", "password"],
                     "type": "object",
                     "properties": {
-                        "grant_type": {
-                            "title": "Grant Type",
-                            "pattern": "password",
-                            "type": "string",
-                        },
+                        "grant_type": IsDict(
+                            {
+                                "title": "Grant Type",
+                                "anyOf": [
+                                    {"pattern": "password", "type": "string"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Grant Type",
+                                "pattern": "password",
+                                "type": "string",
+                            }
+                        ),
                         "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": {
index 1250d4afb314f654de85a268ab50df5fefddc787..7cbbcee2f5c01676a4fcd59ac9fd0ace1810145c 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -142,16 +143,46 @@ def test_openapi_schema(client: TestClient):
                     "required": ["username", "password"],
                     "type": "object",
                     "properties": {
-                        "grant_type": {
-                            "title": "Grant Type",
-                            "pattern": "password",
-                            "type": "string",
-                        },
+                        "grant_type": IsDict(
+                            {
+                                "title": "Grant Type",
+                                "anyOf": [
+                                    {"pattern": "password", "type": "string"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Grant Type",
+                                "pattern": "password",
+                                "type": "string",
+                            }
+                        ),
                         "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": {
index b74cfdc54cdb5bcaca4438b97b2dea8c88ff1b67..7b21fbcc9290ef8bfa13349e22c76a70ada87320 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py39
@@ -142,16 +143,46 @@ def test_openapi_schema(client: TestClient):
                     "required": ["username", "password"],
                     "type": "object",
                     "properties": {
-                        "grant_type": {
-                            "title": "Grant Type",
-                            "pattern": "password",
-                            "type": "string",
-                        },
+                        "grant_type": IsDict(
+                            {
+                                "title": "Grant Type",
+                                "anyOf": [
+                                    {"pattern": "password", "type": "string"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Grant Type",
+                                "pattern": "password",
+                                "type": "string",
+                            }
+                        ),
                         "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": {
index 8a75d2321c26410f5a27a92e7ee6fa7c70b51a1c..512504534f0b18d87035aca83d3d6a6672d8af92 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -142,16 +143,46 @@ def test_openapi_schema(client: TestClient):
                     "required": ["username", "password"],
                     "type": "object",
                     "properties": {
-                        "grant_type": {
-                            "title": "Grant Type",
-                            "pattern": "password",
-                            "type": "string",
-                        },
+                        "grant_type": IsDict(
+                            {
+                                "title": "Grant Type",
+                                "anyOf": [
+                                    {"pattern": "password", "type": "string"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Grant Type",
+                                "pattern": "password",
+                                "type": "string",
+                            }
+                        ),
                         "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": {
index 4e4b6afe8003148fb84be9e52b49e968c0337174..22ae76f428f4aced3d3233b329fa8760a2474a3a 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.security.tutorial005 import (
@@ -270,9 +271,36 @@ def test_openapi_schema():
                     "type": "object",
                     "properties": {
                         "username": {"title": "Username", "type": "string"},
-                        "email": {"title": "Email", "type": "string"},
-                        "full_name": {"title": "Full Name", "type": "string"},
-                        "disabled": {"title": "Disabled", "type": "boolean"},
+                        "email": IsDict(
+                            {
+                                "title": "Email",
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "Email", "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"}
+                        ),
+                        "disabled": IsDict(
+                            {
+                                "title": "Disabled",
+                                "anyOf": [{"type": "boolean"}, {"type": "null"}],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "Disabled", "type": "boolean"}
+                        ),
                     },
                 },
                 "Token": {
@@ -289,16 +317,46 @@ def test_openapi_schema():
                     "required": ["username", "password"],
                     "type": "object",
                     "properties": {
-                        "grant_type": {
-                            "title": "Grant Type",
-                            "pattern": "password",
-                            "type": "string",
-                        },
+                        "grant_type": IsDict(
+                            {
+                                "title": "Grant Type",
+                                "anyOf": [
+                                    {"pattern": "password", "type": "string"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Grant Type",
+                                "pattern": "password",
+                                "type": "string",
+                            }
+                        ),
                         "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": {
index 51cc8329a3eeabd3caac36a222c0a3a1f5cee57e..07239cc8900515a4607afcd333fbbd0e15a80a2d 100644 (file)
@@ -1,3 +1,4 @@
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from docs_src.security.tutorial005_an import (
@@ -270,9 +271,36 @@ def test_openapi_schema():
                     "type": "object",
                     "properties": {
                         "username": {"title": "Username", "type": "string"},
-                        "email": {"title": "Email", "type": "string"},
-                        "full_name": {"title": "Full Name", "type": "string"},
-                        "disabled": {"title": "Disabled", "type": "boolean"},
+                        "email": IsDict(
+                            {
+                                "title": "Email",
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "Email", "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"}
+                        ),
+                        "disabled": IsDict(
+                            {
+                                "title": "Disabled",
+                                "anyOf": [{"type": "boolean"}, {"type": "null"}],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "Disabled", "type": "boolean"}
+                        ),
                     },
                 },
                 "Token": {
@@ -289,16 +317,46 @@ def test_openapi_schema():
                     "required": ["username", "password"],
                     "type": "object",
                     "properties": {
-                        "grant_type": {
-                            "title": "Grant Type",
-                            "pattern": "password",
-                            "type": "string",
-                        },
+                        "grant_type": IsDict(
+                            {
+                                "title": "Grant Type",
+                                "anyOf": [
+                                    {"pattern": "password", "type": "string"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Grant Type",
+                                "pattern": "password",
+                                "type": "string",
+                            }
+                        ),
                         "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": {
index b0d0fed1284daf3e08db83acc9c3766f0cbdce7c..1ab836639e6f9ef07d4f81051fe99d18d4f76e81 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -298,9 +299,36 @@ def test_openapi_schema(client: TestClient):
                     "type": "object",
                     "properties": {
                         "username": {"title": "Username", "type": "string"},
-                        "email": {"title": "Email", "type": "string"},
-                        "full_name": {"title": "Full Name", "type": "string"},
-                        "disabled": {"title": "Disabled", "type": "boolean"},
+                        "email": IsDict(
+                            {
+                                "title": "Email",
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "Email", "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"}
+                        ),
+                        "disabled": IsDict(
+                            {
+                                "title": "Disabled",
+                                "anyOf": [{"type": "boolean"}, {"type": "null"}],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "Disabled", "type": "boolean"}
+                        ),
                     },
                 },
                 "Token": {
@@ -317,16 +345,46 @@ def test_openapi_schema(client: TestClient):
                     "required": ["username", "password"],
                     "type": "object",
                     "properties": {
-                        "grant_type": {
-                            "title": "Grant Type",
-                            "pattern": "password",
-                            "type": "string",
-                        },
+                        "grant_type": IsDict(
+                            {
+                                "title": "Grant Type",
+                                "anyOf": [
+                                    {"pattern": "password", "type": "string"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Grant Type",
+                                "pattern": "password",
+                                "type": "string",
+                            }
+                        ),
                         "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": {
index 26deaaf3c2e4547493d0d7e1964ba34f3065b90c..6aabbe04acc796a05f11fa44735fd6dde5dbe7f8 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py39
@@ -298,9 +299,36 @@ def test_openapi_schema(client: TestClient):
                     "type": "object",
                     "properties": {
                         "username": {"title": "Username", "type": "string"},
-                        "email": {"title": "Email", "type": "string"},
-                        "full_name": {"title": "Full Name", "type": "string"},
-                        "disabled": {"title": "Disabled", "type": "boolean"},
+                        "email": IsDict(
+                            {
+                                "title": "Email",
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "Email", "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"}
+                        ),
+                        "disabled": IsDict(
+                            {
+                                "title": "Disabled",
+                                "anyOf": [{"type": "boolean"}, {"type": "null"}],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "Disabled", "type": "boolean"}
+                        ),
                     },
                 },
                 "Token": {
@@ -317,16 +345,46 @@ def test_openapi_schema(client: TestClient):
                     "required": ["username", "password"],
                     "type": "object",
                     "properties": {
-                        "grant_type": {
-                            "title": "Grant Type",
-                            "pattern": "password",
-                            "type": "string",
-                        },
+                        "grant_type": IsDict(
+                            {
+                                "title": "Grant Type",
+                                "anyOf": [
+                                    {"pattern": "password", "type": "string"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Grant Type",
+                                "pattern": "password",
+                                "type": "string",
+                            }
+                        ),
                         "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": {
index e93f34c3bc68785497f3a612b36cbdd3593291f3..c21884df83a9839f7ba713ef08c2a6a3fb312e32 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py310
@@ -298,9 +299,36 @@ def test_openapi_schema(client: TestClient):
                     "type": "object",
                     "properties": {
                         "username": {"title": "Username", "type": "string"},
-                        "email": {"title": "Email", "type": "string"},
-                        "full_name": {"title": "Full Name", "type": "string"},
-                        "disabled": {"title": "Disabled", "type": "boolean"},
+                        "email": IsDict(
+                            {
+                                "title": "Email",
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "Email", "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"}
+                        ),
+                        "disabled": IsDict(
+                            {
+                                "title": "Disabled",
+                                "anyOf": [{"type": "boolean"}, {"type": "null"}],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "Disabled", "type": "boolean"}
+                        ),
                     },
                 },
                 "Token": {
@@ -317,16 +345,46 @@ def test_openapi_schema(client: TestClient):
                     "required": ["username", "password"],
                     "type": "object",
                     "properties": {
-                        "grant_type": {
-                            "title": "Grant Type",
-                            "pattern": "password",
-                            "type": "string",
-                        },
+                        "grant_type": IsDict(
+                            {
+                                "title": "Grant Type",
+                                "anyOf": [
+                                    {"pattern": "password", "type": "string"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Grant Type",
+                                "pattern": "password",
+                                "type": "string",
+                            }
+                        ),
                         "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": {
index 737a8548fde6899121c45c0fc22680f371ed0187..170c5d60b867da654952b60d06c1f65654781678 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
 from ...utils import needs_py39
@@ -298,9 +299,36 @@ def test_openapi_schema(client: TestClient):
                     "type": "object",
                     "properties": {
                         "username": {"title": "Username", "type": "string"},
-                        "email": {"title": "Email", "type": "string"},
-                        "full_name": {"title": "Full Name", "type": "string"},
-                        "disabled": {"title": "Disabled", "type": "boolean"},
+                        "email": IsDict(
+                            {
+                                "title": "Email",
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "Email", "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"}
+                        ),
+                        "disabled": IsDict(
+                            {
+                                "title": "Disabled",
+                                "anyOf": [{"type": "boolean"}, {"type": "null"}],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "Disabled", "type": "boolean"}
+                        ),
                     },
                 },
                 "Token": {
@@ -317,16 +345,46 @@ def test_openapi_schema(client: TestClient):
                     "required": ["username", "password"],
                     "type": "object",
                     "properties": {
-                        "grant_type": {
-                            "title": "Grant Type",
-                            "pattern": "password",
-                            "type": "string",
-                        },
+                        "grant_type": IsDict(
+                            {
+                                "title": "Grant Type",
+                                "anyOf": [
+                                    {"pattern": "password", "type": "string"},
+                                    {"type": "null"},
+                                ],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {
+                                "title": "Grant Type",
+                                "pattern": "password",
+                                "type": "string",
+                            }
+                        ),
                         "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": {
index fd32b8766f649d44ef73baf8ae73bdeb87fc1e9a..eced88c044ba4032b34dc0b7f2aaa1e479c837bf 100644 (file)
@@ -1,17 +1,20 @@
-from fastapi.testclient import TestClient
 from pytest import MonkeyPatch
 
-from docs_src.settings.app02 import main, test_main
-
-client = TestClient(main.app)
+from ...utils import needs_pydanticv2
 
 
+@needs_pydanticv2
 def test_settings(monkeypatch: MonkeyPatch):
+    from docs_src.settings.app02 import main
+
     monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com")
     settings = main.get_settings()
     assert settings.app_name == "Awesome API"
     assert settings.items_per_user == 50
 
 
+@needs_pydanticv2
 def test_override_settings():
+    from docs_src.settings.app02 import test_main
+
     test_main.test_app()
diff --git a/tests/test_tutorial/test_settings/test_tutorial001.py b/tests/test_tutorial/test_settings/test_tutorial001.py
new file mode 100644 (file)
index 0000000..eb30dbc
--- /dev/null
@@ -0,0 +1,19 @@
+from fastapi.testclient import TestClient
+from pytest import MonkeyPatch
+
+from ...utils import needs_pydanticv2
+
+
+@needs_pydanticv2
+def test_settings(monkeypatch: MonkeyPatch):
+    monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com")
+    from docs_src.settings.tutorial001 import app
+
+    client = TestClient(app)
+    response = client.get("/info")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "app_name": "Awesome API",
+        "admin_email": "admin@example.com",
+        "items_per_user": 50,
+    }
diff --git a/tests/test_tutorial/test_settings/test_tutorial001_pv1.py b/tests/test_tutorial/test_settings/test_tutorial001_pv1.py
new file mode 100644 (file)
index 0000000..e4659de
--- /dev/null
@@ -0,0 +1,19 @@
+from fastapi.testclient import TestClient
+from pytest import MonkeyPatch
+
+from ...utils import needs_pydanticv1
+
+
+@needs_pydanticv1
+def test_settings(monkeypatch: MonkeyPatch):
+    monkeypatch.setenv("ADMIN_EMAIL", "admin@example.com")
+    from docs_src.settings.tutorial001_pv1 import app
+
+    client = TestClient(app)
+    response = client.get("/info")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "app_name": "Awesome API",
+        "admin_email": "admin@example.com",
+        "items_per_user": 50,
+    }
index d927940dab91504a71bcb284c21843a6a07e260d..03e74743341b4d9dd31797b09828793f8f016fae 100644 (file)
@@ -3,8 +3,11 @@ import os
 from pathlib import Path
 
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
+from ...utils import needs_pydanticv1
+
 
 @pytest.fixture(scope="module")
 def client(tmp_path_factory: pytest.TempPathFactory):
@@ -26,6 +29,8 @@ def client(tmp_path_factory: pytest.TempPathFactory):
     os.chdir(cwd)
 
 
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_create_user(client):
     test_user = {"email": "johndoe@example.com", "password": "secret"}
     response = client.post("/users/", json=test_user)
@@ -37,6 +42,8 @@ def test_create_user(client):
     assert response.status_code == 400, response.text
 
 
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_get_user(client):
     response = client.get("/users/1")
     assert response.status_code == 200, response.text
@@ -45,11 +52,15 @@ def test_get_user(client):
     assert "id" in data
 
 
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_inexistent_user(client):
     response = client.get("/users/999")
     assert response.status_code == 404, response.text
 
 
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_get_users(client):
     response = client.get("/users/")
     assert response.status_code == 200, response.text
@@ -58,6 +69,8 @@ def test_get_users(client):
     assert "id" in data[0]
 
 
+# TODO: pv2 add Pydantic v2 version
+@needs_pydanticv1
 def test_create_item(client):
     item = {"title": "Foo", "description": "Something that fights"}
     response = client.post("/users/1/items/", json=item)
@@ -75,6 +88,8 @@ def test_create_item(client):
     assert item_to_check["description"] == item["description"]
 
 
+# TODO: pv2 add Pydantic v2 version
+@needs_pydanticv1
 def test_read_items(client):
     response = client.get("/items/")
     assert response.status_code == 200, response.text
@@ -85,7 +100,9 @@ def test_read_items(client):
     assert "description" in first_item
 
 
-def test_openapi_schema(client):
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
+def test_openapi_schema(client: TestClient):
     response = client.get("/openapi.json")
     assert response.status_code == 200, response.text
     assert response.json() == {
@@ -313,7 +330,16 @@ def test_openapi_schema(client):
                     "type": "object",
                     "properties": {
                         "title": {"title": "Title", "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"}
+                        ),
                     },
                 },
                 "Item": {
@@ -322,7 +348,16 @@ def test_openapi_schema(client):
                     "type": "object",
                     "properties": {
                         "title": {"title": "Title", "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"},
+                        ),
                         "id": {"title": "Id", "type": "integer"},
                         "owner_id": {"title": "Owner Id", "type": "integer"},
                     },
index 08d7b353395e9a0ca73194754ac83e45cac1e5d4..a503ef2a6a495425d65832537d08866dd770c9bb 100644 (file)
@@ -2,8 +2,11 @@ import importlib
 from pathlib import Path
 
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
+from ...utils import needs_pydanticv1
+
 
 @pytest.fixture(scope="module")
 def client():
@@ -22,6 +25,8 @@ def client():
         test_db.unlink()
 
 
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_create_user(client):
     test_user = {"email": "johndoe@example.com", "password": "secret"}
     response = client.post("/users/", json=test_user)
@@ -33,6 +38,8 @@ def test_create_user(client):
     assert response.status_code == 400, response.text
 
 
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_get_user(client):
     response = client.get("/users/1")
     assert response.status_code == 200, response.text
@@ -41,11 +48,15 @@ def test_get_user(client):
     assert "id" in data
 
 
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_inexistent_user(client):
     response = client.get("/users/999")
     assert response.status_code == 404, response.text
 
 
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_get_users(client):
     response = client.get("/users/")
     assert response.status_code == 200, response.text
@@ -54,6 +65,8 @@ def test_get_users(client):
     assert "id" in data[0]
 
 
+# TODO: pv2 add Pydantic v2 version
+@needs_pydanticv1
 def test_create_item(client):
     item = {"title": "Foo", "description": "Something that fights"}
     response = client.post("/users/1/items/", json=item)
@@ -77,6 +90,8 @@ def test_create_item(client):
     assert item_to_check["description"] == item["description"]
 
 
+# TODO: pv2 add Pydantic v2 version
+@needs_pydanticv1
 def test_read_items(client):
     response = client.get("/items/")
     assert response.status_code == 200, response.text
@@ -87,7 +102,9 @@ def test_read_items(client):
     assert "description" in first_item
 
 
-def test_openapi_schema(client):
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
+def test_openapi_schema(client: TestClient):
     response = client.get("/openapi.json")
     assert response.status_code == 200, response.text
     assert response.json() == {
@@ -315,7 +332,16 @@ def test_openapi_schema(client):
                     "type": "object",
                     "properties": {
                         "title": {"title": "Title", "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"}
+                        ),
                     },
                 },
                 "Item": {
@@ -324,7 +350,16 @@ def test_openapi_schema(client):
                     "type": "object",
                     "properties": {
                         "title": {"title": "Title", "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"},
+                        ),
                         "id": {"title": "Id", "type": "integer"},
                         "owner_id": {"title": "Owner Id", "type": "integer"},
                     },
index 493fb3b6b938a4a94b3ce530e1652943cc791a16..d54cc65527778a52178402993b1372fdce289387 100644 (file)
@@ -3,9 +3,10 @@ import os
 from pathlib import Path
 
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
-from ...utils import needs_py310
+from ...utils import needs_py310, needs_pydanticv1
 
 
 @pytest.fixture(scope="module")
@@ -30,6 +31,8 @@ def client(tmp_path_factory: pytest.TempPathFactory):
 
 
 @needs_py310
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_create_user(client):
     test_user = {"email": "johndoe@example.com", "password": "secret"}
     response = client.post("/users/", json=test_user)
@@ -42,6 +45,8 @@ def test_create_user(client):
 
 
 @needs_py310
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_get_user(client):
     response = client.get("/users/1")
     assert response.status_code == 200, response.text
@@ -51,12 +56,16 @@ def test_get_user(client):
 
 
 @needs_py310
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_inexistent_user(client):
     response = client.get("/users/999")
     assert response.status_code == 404, response.text
 
 
 @needs_py310
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_get_users(client):
     response = client.get("/users/")
     assert response.status_code == 200, response.text
@@ -66,6 +75,8 @@ def test_get_users(client):
 
 
 @needs_py310
+# TODO: pv2 add Pydantic v2 version
+@needs_pydanticv1
 def test_create_item(client):
     item = {"title": "Foo", "description": "Something that fights"}
     response = client.post("/users/1/items/", json=item)
@@ -90,6 +101,8 @@ def test_create_item(client):
 
 
 @needs_py310
+# TODO: pv2 add Pydantic v2 version
+@needs_pydanticv1
 def test_read_items(client):
     response = client.get("/items/")
     assert response.status_code == 200, response.text
@@ -101,7 +114,9 @@ def test_read_items(client):
 
 
 @needs_py310
-def test_openapi_schema(client):
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
+def test_openapi_schema(client: TestClient):
     response = client.get("/openapi.json")
     assert response.status_code == 200, response.text
     assert response.json() == {
@@ -329,7 +344,16 @@ def test_openapi_schema(client):
                     "type": "object",
                     "properties": {
                         "title": {"title": "Title", "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"}
+                        ),
                     },
                 },
                 "Item": {
@@ -338,7 +362,16 @@ def test_openapi_schema(client):
                     "type": "object",
                     "properties": {
                         "title": {"title": "Title", "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"},
+                        ),
                         "id": {"title": "Id", "type": "integer"},
                         "owner_id": {"title": "Owner Id", "type": "integer"},
                     },
index 7b56685bc1bf7e31c8948589e4fc9d099fc90ca4..4e43995e638ab086e501788d9c117a87c27a3bc6 100644 (file)
@@ -3,9 +3,10 @@ import os
 from pathlib import Path
 
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
-from ...utils import needs_py39
+from ...utils import needs_py39, needs_pydanticv1
 
 
 @pytest.fixture(scope="module")
@@ -30,6 +31,8 @@ def client(tmp_path_factory: pytest.TempPathFactory):
 
 
 @needs_py39
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_create_user(client):
     test_user = {"email": "johndoe@example.com", "password": "secret"}
     response = client.post("/users/", json=test_user)
@@ -42,6 +45,8 @@ def test_create_user(client):
 
 
 @needs_py39
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_get_user(client):
     response = client.get("/users/1")
     assert response.status_code == 200, response.text
@@ -51,12 +56,16 @@ def test_get_user(client):
 
 
 @needs_py39
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_inexistent_user(client):
     response = client.get("/users/999")
     assert response.status_code == 404, response.text
 
 
 @needs_py39
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_get_users(client):
     response = client.get("/users/")
     assert response.status_code == 200, response.text
@@ -66,6 +75,8 @@ def test_get_users(client):
 
 
 @needs_py39
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_create_item(client):
     item = {"title": "Foo", "description": "Something that fights"}
     response = client.post("/users/1/items/", json=item)
@@ -90,6 +101,8 @@ def test_create_item(client):
 
 
 @needs_py39
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_read_items(client):
     response = client.get("/items/")
     assert response.status_code == 200, response.text
@@ -101,7 +114,9 @@ def test_read_items(client):
 
 
 @needs_py39
-def test_openapi_schema(client):
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
+def test_openapi_schema(client: TestClient):
     response = client.get("/openapi.json")
     assert response.status_code == 200, response.text
     assert response.json() == {
@@ -329,7 +344,16 @@ def test_openapi_schema(client):
                     "type": "object",
                     "properties": {
                         "title": {"title": "Title", "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"}
+                        ),
                     },
                 },
                 "Item": {
@@ -338,7 +362,16 @@ def test_openapi_schema(client):
                     "type": "object",
                     "properties": {
                         "title": {"title": "Title", "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"},
+                        ),
                         "id": {"title": "Id", "type": "integer"},
                         "owner_id": {"title": "Owner Id", "type": "integer"},
                     },
index 43c2b272fe4f0081839a95f9ba6d5826312fe61f..b89b8b0317b97a9f94daa3d97e41aafdc1620356 100644 (file)
@@ -3,9 +3,10 @@ import os
 from pathlib import Path
 
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
-from ...utils import needs_py310
+from ...utils import needs_py310, needs_pydanticv1
 
 
 @pytest.fixture(scope="module", name="client")
@@ -29,6 +30,8 @@ def get_client(tmp_path_factory: pytest.TempPathFactory):
 
 
 @needs_py310
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_create_user(client):
     test_user = {"email": "johndoe@example.com", "password": "secret"}
     response = client.post("/users/", json=test_user)
@@ -41,6 +44,8 @@ def test_create_user(client):
 
 
 @needs_py310
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_get_user(client):
     response = client.get("/users/1")
     assert response.status_code == 200, response.text
@@ -50,12 +55,16 @@ def test_get_user(client):
 
 
 @needs_py310
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_inexistent_user(client):
     response = client.get("/users/999")
     assert response.status_code == 404, response.text
 
 
 @needs_py310
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_get_users(client):
     response = client.get("/users/")
     assert response.status_code == 200, response.text
@@ -65,6 +74,8 @@ def test_get_users(client):
 
 
 @needs_py310
+# TODO: pv2 add Pydantic v2 version
+@needs_pydanticv1
 def test_create_item(client):
     item = {"title": "Foo", "description": "Something that fights"}
     response = client.post("/users/1/items/", json=item)
@@ -89,6 +100,8 @@ def test_create_item(client):
 
 
 @needs_py310
+# TODO: pv2 add Pydantic v2 version
+@needs_pydanticv1
 def test_read_items(client):
     response = client.get("/items/")
     assert response.status_code == 200, response.text
@@ -100,7 +113,9 @@ def test_read_items(client):
 
 
 @needs_py310
-def test_openapi_schema(client):
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
+def test_openapi_schema(client: TestClient):
     response = client.get("/openapi.json")
     assert response.status_code == 200, response.text
     assert response.json() == {
@@ -328,7 +343,16 @@ def test_openapi_schema(client):
                     "type": "object",
                     "properties": {
                         "title": {"title": "Title", "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"}
+                        ),
                     },
                 },
                 "Item": {
@@ -337,7 +361,16 @@ def test_openapi_schema(client):
                     "type": "object",
                     "properties": {
                         "title": {"title": "Title", "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"},
+                        ),
                         "id": {"title": "Id", "type": "integer"},
                         "owner_id": {"title": "Owner Id", "type": "integer"},
                     },
index fd33517db807b66ee2786be6e08307aaf2881426..13351bc810a142fe4c22e1e502208b6ca336cf0b 100644 (file)
@@ -3,9 +3,10 @@ import os
 from pathlib import Path
 
 import pytest
+from dirty_equals import IsDict
 from fastapi.testclient import TestClient
 
-from ...utils import needs_py39
+from ...utils import needs_py39, needs_pydanticv1
 
 
 @pytest.fixture(scope="module", name="client")
@@ -29,6 +30,8 @@ def get_client(tmp_path_factory: pytest.TempPathFactory):
 
 
 @needs_py39
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_create_user(client):
     test_user = {"email": "johndoe@example.com", "password": "secret"}
     response = client.post("/users/", json=test_user)
@@ -41,6 +44,8 @@ def test_create_user(client):
 
 
 @needs_py39
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_get_user(client):
     response = client.get("/users/1")
     assert response.status_code == 200, response.text
@@ -50,12 +55,16 @@ def test_get_user(client):
 
 
 @needs_py39
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_inexistent_user(client):
     response = client.get("/users/999")
     assert response.status_code == 404, response.text
 
 
 @needs_py39
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_get_users(client):
     response = client.get("/users/")
     assert response.status_code == 200, response.text
@@ -65,6 +74,8 @@ def test_get_users(client):
 
 
 @needs_py39
+# TODO: pv2 add Pydantic v2 version
+@needs_pydanticv1
 def test_create_item(client):
     item = {"title": "Foo", "description": "Something that fights"}
     response = client.post("/users/1/items/", json=item)
@@ -89,6 +100,8 @@ def test_create_item(client):
 
 
 @needs_py39
+# TODO: pv2 add Pydantic v2 version
+@needs_pydanticv1
 def test_read_items(client):
     response = client.get("/items/")
     assert response.status_code == 200, response.text
@@ -100,7 +113,9 @@ def test_read_items(client):
 
 
 @needs_py39
-def test_openapi_schema(client):
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
+def test_openapi_schema(client: TestClient):
     response = client.get("/openapi.json")
     assert response.status_code == 200, response.text
     assert response.json() == {
@@ -328,7 +343,16 @@ def test_openapi_schema(client):
                     "type": "object",
                     "properties": {
                         "title": {"title": "Title", "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"}
+                        ),
                     },
                 },
                 "Item": {
@@ -337,7 +361,16 @@ def test_openapi_schema(client):
                     "type": "object",
                     "properties": {
                         "title": {"title": "Title", "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"},
+                        ),
                         "id": {"title": "Id", "type": "integer"},
                         "owner_id": {"title": "Owner Id", "type": "integer"},
                     },
index 6f667dea03ce191393602b07ed5049d53619f66e..ce6ce230c84f4785e89daf7037d5ba21b4cb6405 100644 (file)
@@ -4,7 +4,11 @@ from pathlib import Path
 
 import pytest
 
+from ...utils import needs_pydanticv1
 
+
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_testing_dbs(tmp_path_factory: pytest.TempPathFactory):
     tmp_path = tmp_path_factory.mktemp("data")
     cwd = os.getcwd()
index 9e6b3f3e2ccb007eacaa28fb99124252259f180c..545d63c2a81558c57ac04a6f5430d777e825aa2c 100644 (file)
@@ -4,10 +4,12 @@ from pathlib import Path
 
 import pytest
 
-from ...utils import needs_py310
+from ...utils import needs_py310, needs_pydanticv1
 
 
 @needs_py310
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_testing_dbs_py39(tmp_path_factory: pytest.TempPathFactory):
     tmp_path = tmp_path_factory.mktemp("data")
     cwd = os.getcwd()
index 0b27adf44a9c55d0420eb42406da11252ddabc15..99bfd3fa8a9da6bfedf93c2c7c1103098ba6c377 100644 (file)
@@ -4,10 +4,12 @@ from pathlib import Path
 
 import pytest
 
-from ...utils import needs_py39
+from ...utils import needs_py39, needs_pydanticv1
 
 
 @needs_py39
+# TODO: pv2 add version with Pydantic v2
+@needs_pydanticv1
 def test_testing_dbs_py39(tmp_path_factory: pytest.TempPathFactory):
     tmp_path = tmp_path_factory.mktemp("data")
     cwd = os.getcwd()
index ac6c427ca585144bc27de4d6b1146e9f400d31a2..4350567d1ae99b2fba58b0d08b8e63e160187028 100644 (file)
@@ -5,6 +5,8 @@ from unittest.mock import MagicMock
 import pytest
 from fastapi.testclient import TestClient
 
+from ...utils import needs_pydanticv1
+
 
 @pytest.fixture(scope="module")
 def client():
@@ -17,6 +19,7 @@ def client():
     test_db.unlink()
 
 
+@needs_pydanticv1
 def test_create_user(client):
     test_user = {"email": "johndoe@example.com", "password": "secret"}
     response = client.post("/users/", json=test_user)
@@ -28,6 +31,7 @@ def test_create_user(client):
     assert response.status_code == 400, response.text
 
 
+@needs_pydanticv1
 def test_get_user(client):
     response = client.get("/users/1")
     assert response.status_code == 200, response.text
@@ -36,11 +40,13 @@ def test_get_user(client):
     assert "id" in data
 
 
+@needs_pydanticv1
 def test_inexistent_user(client):
     response = client.get("/users/999")
     assert response.status_code == 404, response.text
 
 
+@needs_pydanticv1
 def test_get_users(client):
     response = client.get("/users/")
     assert response.status_code == 200, response.text
@@ -52,6 +58,7 @@ def test_get_users(client):
 time.sleep = MagicMock()
 
 
+@needs_pydanticv1
 def test_get_slowusers(client):
     response = client.get("/slowusers/")
     assert response.status_code == 200, response.text
@@ -60,6 +67,7 @@ def test_get_slowusers(client):
     assert "id" in data[0]
 
 
+@needs_pydanticv1
 def test_create_item(client):
     item = {"title": "Foo", "description": "Something that fights"}
     response = client.post("/users/1/items/", json=item)
@@ -83,6 +91,7 @@ def test_create_item(client):
     assert item_to_check["description"] == item["description"]
 
 
+@needs_pydanticv1
 def test_read_items(client):
     response = client.get("/items/")
     assert response.status_code == 200, response.text
@@ -93,6 +102,7 @@ def test_read_items(client):
     assert "description" in first_item
 
 
+@needs_pydanticv1
 def test_openapi_schema(client):
     response = client.get("/openapi.json")
     assert response.status_code == 200, response.text
index 57a14b5748122d2d285c32a5f76cb299c0264943..c15acacd18d3d5f74903766f1459c098f169e9f3 100644 (file)
@@ -1,5 +1,6 @@
 from typing import Optional, Union
 
+from dirty_equals import IsDict
 from fastapi import FastAPI
 from fastapi.testclient import TestClient
 from pydantic import BaseModel
@@ -90,7 +91,18 @@ def test_openapi_schema():
                 "Item": {
                     "title": "Item",
                     "type": "object",
-                    "properties": {"name": {"title": "Name", "type": "string"}},
+                    "properties": IsDict(
+                        {
+                            "name": {
+                                "title": "Name",
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                            }
+                        }
+                    )
+                    | IsDict(
+                        # TODO: remove when deprecating Pydantic v1
+                        {"name": {"title": "Name", "type": "string"}}
+                    ),
                 },
                 "ValidationError": {
                     "title": "ValidationError",
index c2a37d3ddd20e7486ae7df49f0f71e7b6a7cabce..ef75d459ead1f71d571e835ad1b4ec612555299d 100644 (file)
@@ -1,5 +1,6 @@
 from typing import Optional, Union
 
+from dirty_equals import IsDict
 from fastapi import FastAPI
 from fastapi.testclient import TestClient
 from pydantic import BaseModel
@@ -84,14 +85,34 @@ def test_openapi_schema():
                 "Item": {
                     "title": "Item",
                     "type": "object",
-                    "properties": {"name": {"title": "Name", "type": "string"}},
+                    "properties": {
+                        "name": IsDict(
+                            {
+                                "title": "Name",
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "Name", "type": "string"}
+                        )
+                    },
                 },
                 "ExtendedItem": {
                     "title": "ExtendedItem",
                     "required": ["age"],
                     "type": "object",
                     "properties": {
-                        "name": {"title": "Name", "type": "string"},
+                        "name": IsDict(
+                            {
+                                "title": "Name",
+                                "anyOf": [{"type": "string"}, {"type": "null"}],
+                            }
+                        )
+                        | IsDict(
+                            # TODO: remove when deprecating Pydantic v1
+                            {"title": "Name", "type": "string"}
+                        ),
                         "age": {"title": "Age", "type": "integer"},
                     },
                 },
index 62f51c960ded32438e6ad94cf0d12870a81611b4..cd97007a44814206efb6da03e9254a3373f4d856 100644 (file)
@@ -2,8 +2,9 @@ from typing import List, Optional, Union
 
 import pytest
 from fastapi import FastAPI
+from fastapi.exceptions import ResponseValidationError
 from fastapi.testclient import TestClient
-from pydantic import BaseModel, ValidationError
+from pydantic import BaseModel
 
 app = FastAPI()
 
@@ -50,12 +51,12 @@ client = TestClient(app)
 
 
 def test_invalid():
-    with pytest.raises(ValidationError):
+    with pytest.raises(ResponseValidationError):
         client.get("/items/invalid")
 
 
 def test_invalid_none():
-    with pytest.raises(ValidationError):
+    with pytest.raises(ResponseValidationError):
         client.get("/items/invalidnone")
 
 
@@ -74,10 +75,10 @@ def test_valid_none_none():
 
 
 def test_double_invalid():
-    with pytest.raises(ValidationError):
+    with pytest.raises(ResponseValidationError):
         client.get("/items/innerinvalid")
 
 
 def test_invalid_list():
-    with pytest.raises(ValidationError):
+    with pytest.raises(ResponseValidationError):
         client.get("/items/invalidlist")
index f2cfa7a11b46d69a39d8a85a48e5e990ae093c9f..0415988a0b8b5ad62e8dcc0a192fa3ea373baa96 100644 (file)
@@ -2,8 +2,8 @@ from typing import List, Optional
 
 import pytest
 from fastapi import FastAPI
+from fastapi.exceptions import ResponseValidationError
 from fastapi.testclient import TestClient
-from pydantic import ValidationError
 from pydantic.dataclasses import dataclass
 
 app = FastAPI()
@@ -39,15 +39,15 @@ client = TestClient(app)
 
 
 def test_invalid():
-    with pytest.raises(ValidationError):
+    with pytest.raises(ResponseValidationError):
         client.get("/items/invalid")
 
 
 def test_double_invalid():
-    with pytest.raises(ValidationError):
+    with pytest.raises(ResponseValidationError):
         client.get("/items/innerinvalid")
 
 
 def test_invalid_list():
-    with pytest.raises(ValidationError):
+    with pytest.raises(ResponseValidationError):
         client.get("/items/invalidlist")
diff --git a/tests/test_validate_response_recursive/__init__.py b/tests/test_validate_response_recursive/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
similarity index 58%
rename from tests/test_validate_response_recursive.py
rename to tests/test_validate_response_recursive/app_pv1.py
index 3a4b10e0ca9f49f79ba1dac94c9e479d51d28944..4cfc4b3eef1fcfd59c6e8942d2125a10ea017e42 100644 (file)
@@ -1,7 +1,6 @@
 from typing import List
 
 from fastapi import FastAPI
-from fastapi.testclient import TestClient
 from pydantic import BaseModel
 
 app = FastAPI()
@@ -49,32 +48,3 @@ def get_recursive_submodel():
             }
         ],
     }
-
-
-client = TestClient(app)
-
-
-def test_recursive():
-    response = client.get("/items/recursive")
-    assert response.status_code == 200, response.text
-    assert response.json() == {
-        "sub_items": [{"name": "subitem", "sub_items": []}],
-        "name": "item",
-    }
-
-    response = client.get("/items/recursive-submodel")
-    assert response.status_code == 200, response.text
-    assert response.json() == {
-        "name": "item",
-        "sub_items1": [
-            {
-                "name": "subitem",
-                "sub_items2": [
-                    {
-                        "name": "subsubitem",
-                        "sub_items1": [{"name": "subsubsubitem", "sub_items2": []}],
-                    }
-                ],
-            }
-        ],
-    }
diff --git a/tests/test_validate_response_recursive/app_pv2.py b/tests/test_validate_response_recursive/app_pv2.py
new file mode 100644 (file)
index 0000000..8c93a83
--- /dev/null
@@ -0,0 +1,51 @@
+from typing import List
+
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class RecursiveItem(BaseModel):
+    sub_items: List["RecursiveItem"] = []
+    name: str
+
+
+RecursiveItem.model_rebuild()
+
+
+class RecursiveSubitemInSubmodel(BaseModel):
+    sub_items2: List["RecursiveItemViaSubmodel"] = []
+    name: str
+
+
+class RecursiveItemViaSubmodel(BaseModel):
+    sub_items1: List[RecursiveSubitemInSubmodel] = []
+    name: str
+
+
+RecursiveSubitemInSubmodel.model_rebuild()
+RecursiveItemViaSubmodel.model_rebuild()
+
+
+@app.get("/items/recursive", response_model=RecursiveItem)
+def get_recursive():
+    return {"name": "item", "sub_items": [{"name": "subitem", "sub_items": []}]}
+
+
+@app.get("/items/recursive-submodel", response_model=RecursiveItemViaSubmodel)
+def get_recursive_submodel():
+    return {
+        "name": "item",
+        "sub_items1": [
+            {
+                "name": "subitem",
+                "sub_items2": [
+                    {
+                        "name": "subsubitem",
+                        "sub_items1": [{"name": "subsubsubitem", "sub_items2": []}],
+                    }
+                ],
+            }
+        ],
+    }
diff --git a/tests/test_validate_response_recursive/test_validate_response_recursive_pv1.py b/tests/test_validate_response_recursive/test_validate_response_recursive_pv1.py
new file mode 100644 (file)
index 0000000..de578ae
--- /dev/null
@@ -0,0 +1,33 @@
+from fastapi.testclient import TestClient
+
+from ..utils import needs_pydanticv1
+
+
+@needs_pydanticv1
+def test_recursive():
+    from .app_pv1 import app
+
+    client = TestClient(app)
+    response = client.get("/items/recursive")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "sub_items": [{"name": "subitem", "sub_items": []}],
+        "name": "item",
+    }
+
+    response = client.get("/items/recursive-submodel")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "name": "item",
+        "sub_items1": [
+            {
+                "name": "subitem",
+                "sub_items2": [
+                    {
+                        "name": "subsubitem",
+                        "sub_items1": [{"name": "subsubsubitem", "sub_items2": []}],
+                    }
+                ],
+            }
+        ],
+    }
diff --git a/tests/test_validate_response_recursive/test_validate_response_recursive_pv2.py b/tests/test_validate_response_recursive/test_validate_response_recursive_pv2.py
new file mode 100644 (file)
index 0000000..7d45e7f
--- /dev/null
@@ -0,0 +1,33 @@
+from fastapi.testclient import TestClient
+
+from ..utils import needs_pydanticv2
+
+
+@needs_pydanticv2
+def test_recursive():
+    from .app_pv2 import app
+
+    client = TestClient(app)
+    response = client.get("/items/recursive")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "sub_items": [{"name": "subitem", "sub_items": []}],
+        "name": "item",
+    }
+
+    response = client.get("/items/recursive-submodel")
+    assert response.status_code == 200, response.text
+    assert response.json() == {
+        "name": "item",
+        "sub_items1": [
+            {
+                "name": "subitem",
+                "sub_items2": [
+                    {
+                        "name": "subsubitem",
+                        "sub_items1": [{"name": "subsubsubitem", "sub_items2": []}],
+                    }
+                ],
+            }
+        ],
+    }
index 5305424c4563b223be59d715b47ad4176976dd55..460c028f7f2a9a162fac1b99dc9a6bb02266b904 100644 (file)
@@ -1,8 +1,11 @@
 import sys
 
 import pytest
+from fastapi._compat import PYDANTIC_V2
 
 needs_py39 = pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python3.9+")
 needs_py310 = pytest.mark.skipif(
     sys.version_info < (3, 10), reason="requires python3.10+"
 )
+needs_pydanticv2 = pytest.mark.skipif(not PYDANTIC_V2, reason="requires Pydantic v2")
+needs_pydanticv1 = pytest.mark.skipif(PYDANTIC_V2, reason="requires Pydantic v1")