pythonasyncioapiasyncfastapiframeworkjsonjson-schemaopenapiopenapi3pydanticpython-typespython3redocreststarletteswaggerswagger-uiuvicornweb
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
214 lines
5.7 KiB
214 lines
5.7 KiB
from collections.abc import Iterator
|
|
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi._compat import PYDANTIC_VERSION_MINOR_TUPLE
|
|
from fastapi.responses import JSONResponse
|
|
from fastapi.testclient import TestClient
|
|
from pydantic import BaseModel
|
|
|
|
|
|
class User(BaseModel):
|
|
name: str
|
|
|
|
|
|
class UserLogin(User):
|
|
password: str
|
|
|
|
|
|
class OuterModel(BaseModel):
|
|
user: User
|
|
|
|
|
|
class Item(BaseModel):
|
|
name: str
|
|
description: str
|
|
|
|
|
|
class ItemWithPrice(Item):
|
|
price: float
|
|
|
|
|
|
requires_pydantic_v2_13 = pytest.mark.skipif(
|
|
PYDANTIC_VERSION_MINOR_TUPLE < (2, 13),
|
|
reason="polymorphic_serialization requires Pydantic >= 2.13",
|
|
)
|
|
|
|
only_for_pydantic_below_v2_13 = pytest.mark.skipif(
|
|
PYDANTIC_VERSION_MINOR_TUPLE >= (2, 13),
|
|
reason="This test only applies to Pydantic < 2.13",
|
|
)
|
|
requires_pydantic_v2_13 = pytest.mark.skipif(
|
|
PYDANTIC_VERSION_MINOR_TUPLE < (2, 13),
|
|
reason="polymorphic_serialization requires Pydantic >= 2.13",
|
|
)
|
|
|
|
only_for_pydantic_below_v2_13 = pytest.mark.skipif(
|
|
PYDANTIC_VERSION_MINOR_TUPLE >= (2, 13),
|
|
reason="This test only applies to Pydantic < 2.13",
|
|
)
|
|
|
|
|
|
def test_default_behavior_filters_subclass_fields():
|
|
app = FastAPI()
|
|
|
|
@app.get("/user", response_model=OuterModel)
|
|
def get_user():
|
|
return OuterModel(user=UserLogin(name="pydantic", password="password"))
|
|
|
|
response = TestClient(app).get("/user")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {"user": {"name": "pydantic"}}
|
|
|
|
|
|
@requires_pydantic_v2_13
|
|
def test_polymorphic_flag_includes_subclass_fields():
|
|
app = FastAPI()
|
|
|
|
@app.get(
|
|
"/user",
|
|
response_model=OuterModel,
|
|
response_model_polymorphic_serialization=True,
|
|
)
|
|
def get_user():
|
|
return OuterModel(user=UserLogin(name="pydantic", password="password"))
|
|
|
|
response = TestClient(app).get("/user")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {"user": {"name": "pydantic", "password": "password"}}
|
|
|
|
|
|
@requires_pydantic_v2_13
|
|
def test_polymorphic_with_inferred_response_model():
|
|
app = FastAPI()
|
|
|
|
@app.get("/user", response_model_polymorphic_serialization=True)
|
|
def get_user() -> OuterModel:
|
|
return OuterModel(user=UserLogin(name="pydantic", password="password"))
|
|
|
|
response = TestClient(app).get("/user")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {"user": {"name": "pydantic", "password": "password"}}
|
|
|
|
|
|
@requires_pydantic_v2_13
|
|
def test_polymorphic_with_fast_json_path():
|
|
app = FastAPI()
|
|
|
|
@app.post(
|
|
"/item",
|
|
response_model=Item,
|
|
response_model_polymorphic_serialization=True,
|
|
status_code=201,
|
|
)
|
|
def create_item() -> Item:
|
|
return ItemWithPrice(name="Widget", description="A useful widget", price=9.99)
|
|
|
|
response = TestClient(app).post("/item")
|
|
|
|
assert response.status_code == 201
|
|
assert response.json() == {
|
|
"name": "Widget",
|
|
"description": "A useful widget",
|
|
"price": 9.99,
|
|
}
|
|
|
|
|
|
@requires_pydantic_v2_13
|
|
def test_polymorphic_with_custom_response_class():
|
|
app = FastAPI()
|
|
|
|
class CustomJSONResponse(JSONResponse):
|
|
media_type = "application/json"
|
|
|
|
@app.get(
|
|
"/user",
|
|
response_model=OuterModel,
|
|
response_model_polymorphic_serialization=True,
|
|
response_class=CustomJSONResponse,
|
|
)
|
|
def get_user():
|
|
return OuterModel(user=UserLogin(name="pydantic", password="password"))
|
|
|
|
response = TestClient(app).get("/user")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {"user": {"name": "pydantic", "password": "password"}}
|
|
|
|
|
|
@requires_pydantic_v2_13
|
|
def test_polymorphic_composes_with_exclude():
|
|
app = FastAPI()
|
|
|
|
@app.get(
|
|
"/user",
|
|
response_model=OuterModel,
|
|
response_model_polymorphic_serialization=True,
|
|
response_model_exclude={"user": {"password"}},
|
|
)
|
|
def get_user():
|
|
return OuterModel(user=UserLogin(name="pydantic", password="password"))
|
|
|
|
response = TestClient(app).get("/user")
|
|
|
|
assert response.status_code == 200
|
|
assert response.json() == {"user": {"name": "pydantic"}}
|
|
|
|
|
|
def test_list_response_filters_subclass_fields_by_default():
|
|
app = FastAPI()
|
|
|
|
def generate_items() -> Iterator[Item]:
|
|
yield ItemWithPrice(name="Item1", description="First item", price=10.0)
|
|
|
|
@app.get("/items/stream", response_model=list[Item])
|
|
def stream_items():
|
|
return list(generate_items())
|
|
|
|
response = TestClient(app).get("/items/stream")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data) == 1
|
|
assert data[0] == {"name": "Item1", "description": "First item"}
|
|
|
|
|
|
@only_for_pydantic_below_v2_13
|
|
def test_raises_error_for_unsupported_pydantic_version():
|
|
app = FastAPI()
|
|
|
|
@app.get(
|
|
"/user",
|
|
response_model=OuterModel,
|
|
response_model_polymorphic_serialization=True,
|
|
)
|
|
def get_user():
|
|
return OuterModel(user=UserLogin(name="pydantic", password="password"))
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
TestClient(app).get("/user")
|
|
|
|
assert "polymorphic_serialization requires Pydantic >= 2.13" in str(exc_info.value)
|
|
|
|
|
|
def test_flag_defaults_to_false():
|
|
app = FastAPI()
|
|
|
|
@app.get("/user1", response_model=OuterModel)
|
|
def get_user1():
|
|
return OuterModel(user=UserLogin(name="user1", password="pass1"))
|
|
|
|
@app.post("/user2", response_model=OuterModel)
|
|
def post_user2():
|
|
return OuterModel(user=UserLogin(name="user2", password="pass2"))
|
|
|
|
client = TestClient(app)
|
|
|
|
response1 = client.get("/user1")
|
|
assert response1.json() == {"user": {"name": "user1"}}
|
|
|
|
response2 = client.post("/user2")
|
|
assert response2.json() == {"user": {"name": "user2"}}
|
|
|