mirror of
https://github.com/fastapi/fastapi.git
synced 2026-01-26 06:51:40 -05:00
* WIP * ✨ Add compat layer, for Pydantic v1 and v2 * ✨ Re-export Pydantic needed internals from compat, to later patch them for v1 * ♻️ Refactor internals to use new compatibility layers and run with Pydantic v2 * 📝 Update examples to run with Pydantic v2 * ✅ Update tests to use Pydantic v2 * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * ✅ Temporarily disable Peewee tests, afterwards I'll enable them only for Pydantic v1 * 🐛 Fix JSON Schema generation and OpenAPI ref template * 🐛 Fix model field creation with defaults from Pydantic v2 * 🐛 Fix body field creation, with new FieldInfo * ✨ Use and check new ResponseValidationError for server validation errors * ✅ Fix test_schema_extra_examples tests with ResponseValidationError * ✅ Add dirty-equals to tests for compatibility with Pydantic v1 and v2 * ✨ Add util to regenerate errors with custom loc * ✨ Generate validation errors with loc * ✅ Update tests for compatibility with Pydantic v1 and v2 * ✅ Update tests for Pydantic v2 in tests/test_filter_pydantic_sub_model.py * ✅ Refactor tests in tests/test_dependency_overrides.py for Pydantic v2, separate parameterized into independent tests to use insert_assert * ✅ Refactor OpenAPI test for tests/test_infer_param_optionality.py for consistency, and make it compatible with Pydantic v1 and v2 * ✅ Update tests for tests/test_multi_query_errors.py for Pydantic v1 and v2 * ✅ Update tests for tests/test_multi_body_errors.py for Pydantic v1 and v2 * ✅ Update tests for tests/test_multi_body_errors.py for Pydantic v1 and v2 * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * ♻️ Refactor tests for tests/test_path.py to inline pytest parameters, to make it easier to make them compatible with Pydantic v2 * ✅ Refactor and udpate tests for tests/test_path.py for Pydantic v1 and v2 * ♻️ Refactor and update tests for tests/test_query.py with compatibility for Pydantic v1 and v2 * ✅ Fix test with optional field without default None * ✅ Update tests for compatibility with Pydantic v2 * ✅ Update tutorial tests for Pydantic v2 * ♻️ Update OAuth2 dependencies for Pydantic v2 * ♻️ Refactor str check when checking for sequence types * ♻️ Rename regex to pattern to keep in sync with Pydantic v2 * ♻️ Refactor _compat.py, start moving conditional imports and declarations to specifics of Pydantic v1 or v2 * ✅ Update tests for OAuth2 security optional * ✅ Refactor tests for OAuth2 optional for Pydantic v2 * ✅ Refactor tests for OAuth2 security for compatibility with Pydantic v2 * 🐛 Fix location in compat layer for Pydantic v2 ModelField * ✅ Refactor tests for Pydantic v2 in tests/test_tutorial/test_bigger_applications/test_main_an_py39.py * 🐛 Add missing markers in Python 3.9 tests * ✅ Refactor tests for bigger apps for consistency with annotated ones and with support for Pydantic v2 * 🐛 Fix jsonable_encoder with new Pydantic v2 data types and Url * 🐛 Fix invalid JSON error for compatibility with Pydantic v2 * ✅ Update tests for behind_a_proxy for Pydantic v2 * ✅ Update tests for tests/test_tutorial/test_body/test_tutorial001_py310.py for Pydantic v2 * ✅ Update tests for tests/test_tutorial/test_body/test_tutorial001.py with Pydantic v2 and consistency with Python 3.10 tests * ✅ Fix tests for tutorial/body_fields for Pydantic v2 * ✅ Refactor tests for tutorial/body_multiple_params with Pydantic v2 * ✅ Update tests for tutorial/body_nested_models for Pydantic v2 * ✅ Update tests for tutorial/body_updates for Pydantic v2 * ✅ Update test for tutorial/cookie_params for Pydantic v2 * ✅ Fix tests for tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py for Pydantic v2 * ✅ Update tests for tutorial/dataclasses for Pydantic v2 * ✅ Update tests for tutorial/dependencies for Pydantic v2 * ✅ Update tests for tutorial/extra_data_types for Pydantic v2 * ✅ Update tests for tutorial/handling_errors for Pydantic v2 * ✅ Fix test markers for Python 3.9 * ✅ Update tests for tutorial/header_params for Pydantic v2 * ✅ Update tests for Pydantic v2 in tests/test_tutorial/test_openapi_callbacks/test_tutorial001.py * ✅ Fix extra tests for Pydantic v2 * ✅ Refactor test for parameters, to later fix Pydantic v2 * ✅ Update tests for tutorial/query_params for Pydantic v2 * ♻️ Update examples in docs to use new pattern instead of the old regex * ✅ Fix several tests for Pydantic v2 * ✅ Update and fix test for ResponseValidationError * 🐛 Fix check for sequences vs scalars, include bytes as scalar * 🐛 Fix check for complex data types, include UploadFile * 🐛 Add list to sequence annotation types * 🐛 Fix checks for uploads and add utils to find if an annotation is an upload (or bytes) * ✨ Add UnionType and NoneType to compat layer * ✅ Update tests for request_files for compatibility with Pydantic v2 and consistency with other tests * ✅ Fix testsw for request_forms for Pydantic v2 * ✅ Fix tests for request_forms_and_files for Pydantic v2 * ✅ Fix tests in tutorial/security for compatibility with Pydantic v2 * ⬆️ Upgrade required version of email_validator * ✅ Fix tests for params repr * ✅ Add Pydantic v2 pytest markers * Use match_pydantic_error_url * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * Use field_serializer instead of encoders in some tests * Show Undefined as ... in repr * Mark custom encoders test with xfail * Update test to reflect new serialization of Decimal as str * Use `model_validate` instead of `from_orm` * Update JSON schema to reflect required nullable * Add dirty-equals to pyproject.toml * Fix locs and error creation for use with pydantic 2.0a4 * Use the type adapter for serialization. This is hacky. * 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks * ✅ Refactor test_multi_body_errors for compatibility with Pydantic v1 and v2 * ✅ Refactor test_custom_encoder for Pydantic v1 and v2 * ✅ Set input to None for now, for compatibility with current tests * 🐛 Fix passing serialization params to model field when handling the response * ♻️ Refactor exceptions to not depend on Pydantic ValidationError class * ♻️ Revert/refactor params to simplify repr * ✅ Tweak tests for custom class encoders for Pydantic v1 and v2 * ✅ Tweak tests for jsonable_encoder for Pydantic v1 and v2 * ✅ Tweak test for compatibility with Pydantic v1 and v2 * 🐛 Fix filtering data with subclasses * 🐛 Workaround examples in OpenAPI schema * ✅ Add skip marker for SQL tutorial, needs to be updated either way * ✅ Update test for broken JSON * ✅ Fix test for broken JSON * ✅ Update tests for timedeltas * ✅ Fix test for plain text validation errors * ✅ Add markers for Pydantic v1 exclusive tests (for now) * ✅ Update test for path_params with enums for compatibility with Pydantic v1 and v2 * ✅ Update tests for extra examples in OpenAPI * ✅ Fix tests for response_model with compatibility with Pydantic v1 and v2 * 🐛 Fix required double serialization for different types of models * ✅ Fix tests for response model with compatibility with new Pydantic v2 * 🐛 Import Undefined from compat layer * ✅ Fix tests for response_model for Pydantic v2 * ✅ Fix tests for schema_extra for Pydantic v2 * ✅ Add markers and update tests for Pydantic v2 * 💡 Comment out logic for double encoding that breaks other usecases * ✅ Update errors for int parsing * ♻️ Refactor re-enabling compatibility for Pydantic v1 * ♻️ Refactor OpenAPI utils to re-enable support for Pydantic v1 * ♻️ Refactor dependencies/utils and _compat for compatibility with Pydantic v1 * 🐛 Fix and tweak compatibility with Pydantic v1 and v2 in dependencies/utils * ✅ Tweak tests and examples for Pydantic v1 * ♻️ Tweak call to ModelField.validate for compatibility with Pydantic v1 * ✨ Use new global override TypeAdapter from_attributes * ✅ Update tests after updating from_attributes * 🔧 Update pytest config to avoid collecting tests from docs, useful for editor-integrated tests * ✅ Add test for data filtering, including inheritance and models in fields or lists of models * ♻️ Make OpenAPI models compatible with both Pydantic v1 and v2 * ♻️ Fix compatibility for Pydantic v1 and v2 in jsonable_encoder * ♻️ Fix compatibility in params with Pydantic v1 and v2 * ♻️ Fix compatibility when creating a FieldInfo in Pydantic v1 and v2 in utils.py * ♻️ Fix generation of flat_models and JSON Schema definitions in _compat.py for Pydantic v1 and v2 * ♻️ Update handling of ErrorWrappers for Pydantic v1 * ♻️ Refactor checks and handling of types an sequences * ♻️ Refactor and cleanup comments with compatibility for Pydantic v1 and v2 * ♻️ Update UploadFile for compatibility with both Pydantic v1 and v2 * 🔥 Remove commented out unneeded code * 🐛 Fix mock of get_annotation_from_field_info for Pydantic v1 and v2 * 🐛 Fix params with compatibility for Pydantic v1 and v2, with schemas and new pattern vs regex * 🐛 Fix check if field is sequence for Pydantic v1 * ✅ Fix tests for custom_schema_fields, for compatibility with Pydantic v1 and v2 * ✅ Simplify and fix tests for jsonable_encoder with compatibility for Pydantic v1 and v2 * ✅ Fix tests for orm_mode with Pydantic v1 and compatibility with Pydantic v2 * ♻️ Refactor logic for normalizing Pydantic v1 ErrorWrappers * ♻️ Workaround for params with examples, before defining what to deprecate in Pydantic v1 and v2 for examples with JSON Schema vs OpenAPI * ✅ Fix tests for Pydantic v1 and v2 for response_by_alias * ✅ Fix test for schema_extra with compatibility with Pydantic v1 and v2 * ♻️ Tweak error regeneration with loc * ♻️ Update error handling and serializationwith compatibility for Pydantic v1 and v2 * ♻️ Re-enable custom encoders for Pydantic v1 * ♻️ Update ErrorWrapper reserialization in Pydantic v1, do it outside of FastAPI ValidationExceptions * ✅ Update test for filter_submodel, re-structure to simplify testing while keeping division of Pydantic v1 and v2 * ✅ Refactor Pydantic v1 only test that requires modifying environment variables * 🔥 Update test for plaintext error responses, for Pydantic v1 and v2 * ⏪️ Revert changes in DB tutorial to use Pydantic v1 (the new guide will have SQLModel) * ✅ Mark current SQL DB tutorial tests as Pydantic only * ♻️ Update datastructures for compatibility with Pydantic v1, not requiring pydantic-core * ♻️ Update encoders.py for compatibility with Pydantic v1 * ⏪️ Revert changes to Peewee, the docs for that are gonna live in a new HowTo section, not in the main tutorials * ♻️ Simplify response body kwargs generation * 🔥 Clean up comments * 🔥 Clean some tests and comments * ✅ Refactor tests to match new Pydantic error string URLs * ✅ Refactor tests for recursive models for Pydantic v1 and v2 * ✅ Update tests for Peewee, re-enable, Pydantic-v1-only * ♻️ Update FastAPI params to take regex and pattern arguments * ⏪️ Revert tutorial examples for pattern, it will be done in a subsequent PR * ⏪️ Revert changes in schema extra examples, it will be added later in a docs-specific PR * 💡 Add TODO comment to document str validations with pattern * 🔥 Remove unneeded comment * 📌 Upgrade Pydantic pin dependency * ⬆️ Upgrade email_validator dependency * 🐛 Tweak type annotations in _compat.py * 🔇 Tweak mypy errors for compat, for Pydantic v1 re-imports * 🐛 Tweak and fix type annotations * ➕ Update requirements-test.txt, re-add dirty-equals * 🔥 Remove unnecessary config * 🐛 Tweak type annotations * 🔥 Remove unnecessary type in dependencies/utils.py * 💡 Update comment in routing.py --------- Co-authored-by: David Montague <35119617+dmontagu@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
289 lines
8.3 KiB
Python
289 lines
8.3 KiB
Python
from dataclasses import dataclass
|
|
from datetime import datetime, timezone
|
|
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
|
|
|
|
from .utils import needs_pydanticv1, needs_pydanticv2
|
|
|
|
|
|
class Person:
|
|
def __init__(self, name: str):
|
|
self.name = name
|
|
|
|
|
|
class Pet:
|
|
def __init__(self, owner: Person, name: str):
|
|
self.owner = owner
|
|
self.name = name
|
|
|
|
|
|
@dataclass
|
|
class Item:
|
|
name: str
|
|
count: int
|
|
|
|
|
|
class DictablePerson(Person):
|
|
def __iter__(self):
|
|
return ((k, v) for k, v in self.__dict__.items())
|
|
|
|
|
|
class DictablePet(Pet):
|
|
def __iter__(self):
|
|
return ((k, v) for k, v in self.__dict__.items())
|
|
|
|
|
|
class Unserializable:
|
|
def __iter__(self):
|
|
raise NotImplementedError()
|
|
|
|
@property
|
|
def __dict__(self):
|
|
raise NotImplementedError()
|
|
|
|
|
|
class RoleEnum(Enum):
|
|
admin = "admin"
|
|
normal = "normal"
|
|
|
|
|
|
class ModelWithConfig(BaseModel):
|
|
role: Optional[RoleEnum] = None
|
|
|
|
if PYDANTIC_V2:
|
|
model_config = {"use_enum_values": True}
|
|
else:
|
|
|
|
class Config:
|
|
use_enum_values = True
|
|
|
|
|
|
class ModelWithAlias(BaseModel):
|
|
foo: str = Field(alias="Foo")
|
|
|
|
|
|
class ModelWithDefault(BaseModel):
|
|
foo: str = ... # type: ignore
|
|
bar: str = "bar"
|
|
bla: str = "bla"
|
|
|
|
|
|
def test_encode_dict():
|
|
pet = {"name": "Firulais", "owner": {"name": "Foo"}}
|
|
assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}}
|
|
assert jsonable_encoder(pet, include={"name"}) == {"name": "Firulais"}
|
|
assert jsonable_encoder(pet, exclude={"owner"}) == {"name": "Firulais"}
|
|
assert jsonable_encoder(pet, include={}) == {}
|
|
assert jsonable_encoder(pet, exclude={}) == {
|
|
"name": "Firulais",
|
|
"owner": {"name": "Foo"},
|
|
}
|
|
|
|
|
|
def test_encode_class():
|
|
person = Person(name="Foo")
|
|
pet = Pet(owner=person, name="Firulais")
|
|
assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}}
|
|
assert jsonable_encoder(pet, include={"name"}) == {"name": "Firulais"}
|
|
assert jsonable_encoder(pet, exclude={"owner"}) == {"name": "Firulais"}
|
|
assert jsonable_encoder(pet, include={}) == {}
|
|
assert jsonable_encoder(pet, exclude={}) == {
|
|
"name": "Firulais",
|
|
"owner": {"name": "Foo"},
|
|
}
|
|
|
|
|
|
def test_encode_dictable():
|
|
person = DictablePerson(name="Foo")
|
|
pet = DictablePet(owner=person, name="Firulais")
|
|
assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}}
|
|
assert jsonable_encoder(pet, include={"name"}) == {"name": "Firulais"}
|
|
assert jsonable_encoder(pet, exclude={"owner"}) == {"name": "Firulais"}
|
|
assert jsonable_encoder(pet, include={}) == {}
|
|
assert jsonable_encoder(pet, exclude={}) == {
|
|
"name": "Firulais",
|
|
"owner": {"name": "Foo"},
|
|
}
|
|
|
|
|
|
def test_encode_dataclass():
|
|
item = Item(name="foo", count=100)
|
|
assert jsonable_encoder(item) == {"name": "foo", "count": 100}
|
|
assert jsonable_encoder(item, include={"name"}) == {"name": "foo"}
|
|
assert jsonable_encoder(item, exclude={"count"}) == {"name": "foo"}
|
|
assert jsonable_encoder(item, include={}) == {}
|
|
assert jsonable_encoder(item, exclude={}) == {"name": "foo", "count": 100}
|
|
|
|
|
|
def test_encode_unsupported():
|
|
unserializable = Unserializable()
|
|
with pytest.raises(ValueError):
|
|
jsonable_encoder(unserializable)
|
|
|
|
|
|
@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()
|
|
}
|
|
|
|
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():
|
|
model = ModelWithConfig(role=RoleEnum.admin)
|
|
assert jsonable_encoder(model) == {"role": "admin"}
|
|
|
|
|
|
def test_encode_model_with_alias_raises():
|
|
with pytest.raises(ValidationError):
|
|
ModelWithAlias(foo="Bar")
|
|
|
|
|
|
def test_encode_model_with_alias():
|
|
model = ModelWithAlias(Foo="Bar")
|
|
assert jsonable_encoder(model) == {"Foo": "Bar"}
|
|
|
|
|
|
def test_encode_model_with_default():
|
|
model = ModelWithDefault(foo="foo", bar="bar")
|
|
assert jsonable_encoder(model) == {"foo": "foo", "bar": "bar", "bla": "bla"}
|
|
assert jsonable_encoder(model, exclude_unset=True) == {"foo": "foo", "bar": "bar"}
|
|
assert jsonable_encoder(model, exclude_defaults=True) == {"foo": "foo"}
|
|
assert jsonable_encoder(model, exclude_unset=True, exclude_defaults=True) == {
|
|
"foo": "foo"
|
|
}
|
|
assert jsonable_encoder(model, include={"foo"}) == {"foo": "foo"}
|
|
assert jsonable_encoder(model, exclude={"bla"}) == {"foo": "foo", "bar": "bar"}
|
|
assert jsonable_encoder(model, include={}) == {}
|
|
assert jsonable_encoder(model, exclude={}) == {
|
|
"foo": "foo",
|
|
"bar": "bar",
|
|
"bla": "bla",
|
|
}
|
|
|
|
|
|
@needs_pydanticv1
|
|
def test_custom_encoders():
|
|
class safe_datetime(datetime):
|
|
pass
|
|
|
|
class MyModel(BaseModel):
|
|
dt_field: safe_datetime
|
|
|
|
instance = MyModel(dt_field=safe_datetime.now())
|
|
|
|
encoded_instance = jsonable_encoder(
|
|
instance, custom_encoder={safe_datetime: lambda o: o.isoformat()}
|
|
)
|
|
assert encoded_instance["dt_field"] == instance.dt_field.isoformat()
|
|
|
|
|
|
def test_custom_enum_encoders():
|
|
def custom_enum_encoder(v: Enum):
|
|
return v.value.lower()
|
|
|
|
class MyEnum(Enum):
|
|
ENUM_VAL_1 = "ENUM_VAL_1"
|
|
|
|
instance = MyEnum.ENUM_VAL_1
|
|
|
|
encoded_instance = jsonable_encoder(
|
|
instance, custom_encoder={MyEnum: custom_enum_encoder}
|
|
)
|
|
assert encoded_instance == custom_enum_encoder(instance)
|
|
|
|
|
|
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"
|