mirror of https://github.com/tiangolo/fastapi.git
Add tests for Query, Path, Cookie, Header parameters
This commit is contained in:
parent
1a2e4152ed
commit
dc621d1b0e
|
|
@ -0,0 +1,522 @@
|
|||
from typing import List, Union
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict, IsOneOf, IsPartialDict
|
||||
from fastapi import Body, FastAPI
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
from .utils import get_body_model_name
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.post("/required-list-str", operation_id="required_list_str")
|
||||
async def read_required_list_str(p: List[str] = Body(..., embed=True)):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelRequiredListStr(BaseModel):
|
||||
p: List[str]
|
||||
|
||||
|
||||
@app.post("/model-required-list-str", operation_id="model_required_list_str")
|
||||
def read_model_required_list_str(p: FormModelRequiredListStr):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-str", "/model-required-list-str"],
|
||||
)
|
||||
def test_required_list_str_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p": {
|
||||
"items": {"type": "string"},
|
||||
"title": "P",
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": ["p"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("json", [None, {}])
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-str", "/model-required-list-str"],
|
||||
)
|
||||
def test_required_list_str_missing(path: str, json: Union[dict, None]):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json=json)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": IsOneOf(["body", "p"], ["body"]),
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": IsOneOf(["body", "p"], ["body"]),
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-str", "/model-required-list-str"],
|
||||
)
|
||||
def test_required_list_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.post("/required-list-alias", operation_id="required_list_alias")
|
||||
async def read_required_list_alias(
|
||||
p: List[str] = Body(..., embed=True, alias="p_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelRequiredListAlias(BaseModel):
|
||||
p: List[str] = Field(..., alias="p_alias")
|
||||
|
||||
|
||||
@app.post("/model-required-list-alias", operation_id="model_required_list_alias")
|
||||
async def read_model_required_list_alias(p: FormModelRequiredListAlias):
|
||||
return {"p": p.p} # pragma: no cover
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-list-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 models",
|
||||
strict=False,
|
||||
),
|
||||
),
|
||||
"/model-required-list-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_str_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_alias": {
|
||||
"items": {"type": "string"},
|
||||
"title": "P Alias",
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": ["p_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("json", [None, {}])
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-alias", "/model-required-list-alias"],
|
||||
)
|
||||
def test_required_list_alias_missing(path: str, json: Union[dict, None]):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json=json)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": IsOneOf(["body", "p_alias"], ["body"]),
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": IsOneOf(["body", "p_alias"], ["body"]),
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-alias", "/model-required-list-alias"],
|
||||
)
|
||||
def test_required_list_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": ["hello", "world"]})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p": ["hello", "world"]}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-alias", "/model-required-list-alias"],
|
||||
)
|
||||
def test_required_list_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/required-list-validation-alias", operation_id="required_list_validation_alias"
|
||||
)
|
||||
def read_required_list_validation_alias(
|
||||
p: List[str] = Body(..., embed=True, validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelRequiredListValidationAlias(BaseModel):
|
||||
p: List[str] = Field(..., validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-required-list-validation-alias",
|
||||
operation_id="model_required_list_validation_alias",
|
||||
)
|
||||
async def read_model_required_list_validation_alias(
|
||||
p: FormModelRequiredListValidationAlias,
|
||||
):
|
||||
return {"p": p.p} # pragma: no cover
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-validation-alias", "/model-required-list-validation-alias"],
|
||||
)
|
||||
def test_required_list_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"items": {"type": "string"},
|
||||
"title": "P Val Alias",
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize("json", [None, {}])
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-list-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_validation_alias_missing(path: str, json: Union[dict, None]):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json=json)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": IsOneOf( # /required-validation-alias fails here
|
||||
["body"], ["body", "p_val_alias"]
|
||||
),
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-list-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": ["hello", "world"]})
|
||||
assert response.status_code == 422, (
|
||||
response.text # /required-list-validation-alias fails here
|
||||
)
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-list-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_val_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200, (
|
||||
response.text # /required-list-validation-alias fails here
|
||||
)
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/required-list-alias-and-validation-alias",
|
||||
operation_id="required_list_alias_and_validation_alias",
|
||||
)
|
||||
def read_required_list_alias_and_validation_alias(
|
||||
p: List[str] = Body(
|
||||
..., embed=True, alias="p_alias", validation_alias="p_val_alias"
|
||||
),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelRequiredListAliasAndValidationAlias(BaseModel):
|
||||
p: List[str] = Field(..., alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
operation_id="model_required_list_alias_and_validation_alias",
|
||||
)
|
||||
def read_model_required_list_alias_and_validation_alias(
|
||||
p: FormModelRequiredListAliasAndValidationAlias,
|
||||
):
|
||||
return {"p": p.p} # pragma: no cover
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"items": {"type": "string"},
|
||||
"title": "P Val Alias",
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize("json", [None, {}])
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-list-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_missing(path: str, json):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json=json)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": IsOneOf( # /required-list-alias-and-validation-alias fails here
|
||||
["body"], ["body", "p_val_alias"]
|
||||
),
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-list-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": ["hello", "world"]})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [ # /required-list-alias-and-validation-alias fails here
|
||||
"body",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p": ["hello", "world"]}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-list-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_alias": ["hello", "world"]})
|
||||
assert response.status_code == 422, response.text
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p_alias": ["hello", "world"]}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-list-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_val_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200, (
|
||||
response.text # /required-list-alias-and-validation-alias fails here
|
||||
)
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
|
@ -0,0 +1,594 @@
|
|||
from typing import List, Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import Body, FastAPI
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
from .utils import get_body_model_name
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.post("/optional-list-str", operation_id="optional_list_str")
|
||||
async def read_optional_list_str(p: Optional[List[str]] = Body(None, embed=True)):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelOptionalListStr(BaseModel):
|
||||
p: Optional[List[str]] = None
|
||||
|
||||
|
||||
@app.post("/model-optional-list-str", operation_id="model_optional_list_str")
|
||||
async def read_model_optional_list_str(p: FormModelOptionalListStr):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-str", "/model-optional-list-str"],
|
||||
)
|
||||
def test_optional_list_str_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p": {"items": {"type": "string"}, "type": "array", "title": "P"},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_optional_list_str_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/optional-list-str")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
def test_model_optional_list_str_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/model-optional-list-str")
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"input": None,
|
||||
"loc": ["body"],
|
||||
"msg": "Field required",
|
||||
"type": "missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
) | IsDict(
|
||||
{
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-str", "/model-optional-list-str"],
|
||||
)
|
||||
def test_optional_list_str_missing_empty_dict(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-str", "/model-optional-list-str"],
|
||||
)
|
||||
def test_optional_list_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.post("/optional-list-alias", operation_id="optional_list_alias")
|
||||
async def read_optional_list_alias(
|
||||
p: Optional[List[str]] = Body(None, embed=True, alias="p_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelOptionalListAlias(BaseModel):
|
||||
p: Optional[List[str]] = Field(None, alias="p_alias")
|
||||
|
||||
|
||||
@app.post("/model-optional-list-alias", operation_id="model_optional_list_alias")
|
||||
async def read_model_optional_list_alias(p: FormModelOptionalListAlias):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-list-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
strict=False,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2",
|
||||
),
|
||||
),
|
||||
"/model-optional-list-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_str_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p_alias": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p_alias": {
|
||||
"items": {"type": "string"},
|
||||
"type": "array",
|
||||
"title": "P Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_optional_list_alias_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/optional-list-alias")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
def test_model_optional_list_alias_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/model-optional-list-alias")
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"input": None,
|
||||
"loc": ["body"],
|
||||
"msg": "Field required",
|
||||
"type": "missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
) | IsDict(
|
||||
{
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-alias", "/model-optional-list-alias"],
|
||||
)
|
||||
def test_optional_list_alias_missing_empty_dict(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-alias", "/model-optional-list-alias"],
|
||||
)
|
||||
def test_optional_list_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-alias", "/model-optional-list-alias"],
|
||||
)
|
||||
def test_optional_list_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/optional-list-validation-alias", operation_id="optional_list_validation_alias"
|
||||
)
|
||||
def read_optional_list_validation_alias(
|
||||
p: Optional[List[str]] = Body(None, embed=True, validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelOptionalListValidationAlias(BaseModel):
|
||||
p: Optional[List[str]] = Field(None, validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-optional-list-validation-alias",
|
||||
operation_id="model_optional_list_validation_alias",
|
||||
)
|
||||
def read_model_optional_list_validation_alias(
|
||||
p: FormModelOptionalListValidationAlias,
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
|
||||
)
|
||||
def test_optional_list_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"items": {"type": "string"},
|
||||
"type": "array",
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_optional_list_validation_alias_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/optional-list-validation-alias")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
def test_model_optional_list_validation_alias_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/model-optional-list-validation-alias")
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"input": None,
|
||||
"loc": ["body"],
|
||||
"msg": "Field required",
|
||||
"type": "missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
) | IsDict(
|
||||
{
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
|
||||
)
|
||||
def test_optional_list_validation_alias_missing_empty_dict(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-list-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None} # /optional-list-validation-alias fails here
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-list-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_val_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == { # /optional-list-validation-alias fails here
|
||||
"p": ["hello", "world"]
|
||||
}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
operation_id="optional_list_alias_and_validation_alias",
|
||||
)
|
||||
def read_optional_list_alias_and_validation_alias(
|
||||
p: Optional[List[str]] = Body(
|
||||
None, embed=True, alias="p_alias", validation_alias="p_val_alias"
|
||||
),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelOptionalListAliasAndValidationAlias(BaseModel):
|
||||
p: Optional[List[str]] = Field(
|
||||
None, alias="p_alias", validation_alias="p_val_alias"
|
||||
)
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
operation_id="model_optional_list_alias_and_validation_alias",
|
||||
)
|
||||
def read_model_optional_list_alias_and_validation_alias(
|
||||
p: FormModelOptionalListAliasAndValidationAlias,
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"items": {"type": "string"},
|
||||
"type": "array",
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_optional_list_alias_and_validation_alias_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/optional-list-alias-and-validation-alias")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
def test_model_optional_list_alias_and_validation_alias_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/model-optional-list-alias-and-validation-alias")
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"input": None,
|
||||
"loc": ["body"],
|
||||
"msg": "Field required",
|
||||
"type": "missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
) | IsDict(
|
||||
{
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_missing_empty_dict(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"p": None # /optional-list-alias-and-validation-alias fails here
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_val_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
"p": [ # /optional-list-alias-and-validation-alias fails here
|
||||
"hello",
|
||||
"world",
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,566 @@
|
|||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import Body, FastAPI
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
from .utils import get_body_model_name
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.post("/optional-str", operation_id="optional_str")
|
||||
async def read_optional_str(p: Optional[str] = Body(None, embed=True)):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelOptionalStr(BaseModel):
|
||||
p: Optional[str] = None
|
||||
|
||||
|
||||
@app.post("/model-optional-str", operation_id="model_optional_str")
|
||||
async def read_model_optional_str(p: FormModelOptionalStr):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p": {"type": "string", "title": "P"},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_optional_str_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/optional-str")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
def test_model_optional_str_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/model-optional-str")
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"input": None,
|
||||
"loc": ["body"],
|
||||
"msg": "Field required",
|
||||
"type": "missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
) | IsDict(
|
||||
{
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str_missing_empty_dict(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.post("/optional-alias", operation_id="optional_alias")
|
||||
async def read_optional_alias(
|
||||
p: Optional[str] = Body(None, embed=True, alias="p_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelOptionalAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, alias="p_alias")
|
||||
|
||||
|
||||
@app.post("/model-optional-alias", operation_id="model_optional_alias")
|
||||
async def read_model_optional_alias(p: FormModelOptionalAlias):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
strict=False,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2",
|
||||
),
|
||||
),
|
||||
"/model-optional-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_str_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p_alias": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p_alias": {"type": "string", "title": "P Alias"},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_optional_alias_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/optional-alias")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
def test_model_optional_alias_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/model-optional-alias")
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"input": None,
|
||||
"loc": ["body"],
|
||||
"msg": "Field required",
|
||||
"type": "missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
) | IsDict(
|
||||
{
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_model_optional_alias_missing_empty_dict(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_alias": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.post("/optional-validation-alias", operation_id="optional_validation_alias")
|
||||
def read_optional_validation_alias(
|
||||
p: Optional[str] = Body(None, embed=True, validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelOptionalValidationAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-optional-validation-alias", operation_id="model_optional_validation_alias"
|
||||
)
|
||||
def read_model_optional_validation_alias(
|
||||
p: FormModelOptionalValidationAlias,
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-validation-alias", "/model-optional-validation-alias"],
|
||||
)
|
||||
def test_optional_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {"type": "string", "title": "P Val Alias"},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_optional_validation_alias_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/optional-validation-alias")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_model_optional_validation_alias_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/model-optional-validation-alias")
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"input": None,
|
||||
"loc": ["body"],
|
||||
"msg": "Field required",
|
||||
"type": "missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
) | IsDict(
|
||||
{
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-validation-alias", "/model-optional-validation-alias"],
|
||||
)
|
||||
def test_model_optional_validation_alias_missing_empty_dict(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None} # /optional-validation-alias fails here
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_val_alias": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"} # /optional-validation-alias fails here
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/optional-alias-and-validation-alias",
|
||||
operation_id="optional_alias_and_validation_alias",
|
||||
)
|
||||
def read_optional_alias_and_validation_alias(
|
||||
p: Optional[str] = Body(
|
||||
None, embed=True, alias="p_alias", validation_alias="p_val_alias"
|
||||
),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelOptionalAliasAndValidationAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
operation_id="model_optional_alias_and_validation_alias",
|
||||
)
|
||||
def read_model_optional_alias_and_validation_alias(
|
||||
p: FormModelOptionalAliasAndValidationAlias,
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {"type": "string", "title": "P Val Alias"},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_optional_alias_and_validation_alias_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/optional-alias-and-validation-alias")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
def test_model_optional_alias_and_validation_alias_missing():
|
||||
client = TestClient(app)
|
||||
response = client.post("/model-optional-alias-and-validation-alias")
|
||||
assert response.status_code == 422, response.text
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"input": None,
|
||||
"loc": ["body"],
|
||||
"msg": "Field required",
|
||||
"type": "missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
) | IsDict(
|
||||
{
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_model_optional_alias_and_validation_alias_missing_empty_dict(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_alias": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"p": None # /optional-alias-and-validation-alias fails here
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_val_alias": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"p": "hello" # /optional-alias-and-validation-alias fails here
|
||||
}
|
||||
|
|
@ -0,0 +1,509 @@
|
|||
from typing import Any, Dict, Union
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict, IsOneOf
|
||||
from fastapi import Body, FastAPI
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
from .utils import get_body_model_name
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.post("/required-str", operation_id="required_str")
|
||||
async def read_required_str(p: str = Body(..., embed=True)):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelRequiredStr(BaseModel):
|
||||
p: str
|
||||
|
||||
|
||||
@app.post("/model-required-str", operation_id="model_required_str")
|
||||
async def read_model_required_str(p: FormModelRequiredStr):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p": {"title": "P", "type": "string"},
|
||||
},
|
||||
"required": ["p"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("json", [None, {}])
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str_missing(path: str, json: Union[Dict[str, Any], None]):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json=json)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": IsOneOf(["body"], ["body", "p"]),
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": IsOneOf(["body"], ["body", "p"]),
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.post("/required-alias", operation_id="required_alias")
|
||||
async def read_required_alias(p: str = Body(..., embed=True, alias="p_alias")):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelRequiredAlias(BaseModel):
|
||||
p: str = Field(..., alias="p_alias")
|
||||
|
||||
|
||||
@app.post("/model-required-alias", operation_id="model_required_alias")
|
||||
async def read_model_required_alias(p: FormModelRequiredAlias):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2",
|
||||
strict=False,
|
||||
),
|
||||
),
|
||||
"/model-required-alias",
|
||||
],
|
||||
)
|
||||
def test_required_str_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_alias": {"title": "P Alias", "type": "string"},
|
||||
},
|
||||
"required": ["p_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("json", [None, {}])
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-alias", "/model-required-alias"],
|
||||
)
|
||||
def test_required_alias_missing(path: str, json: Union[Dict[str, Any], None]):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json=json)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": IsOneOf(["body", "p_alias"], ["body"]),
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": IsOneOf(["body", "p_alias"], ["body"]),
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-alias", "/model-required-alias"],
|
||||
)
|
||||
def test_required_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": "hello"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p": "hello"}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": IsOneOf(["body", "p_alias"], ["body"]),
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-alias", "/model-required-alias"],
|
||||
)
|
||||
def test_required_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_alias": "hello"})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.post("/required-validation-alias", operation_id="required_validation_alias")
|
||||
def read_required_validation_alias(
|
||||
p: str = Body(..., embed=True, validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelRequiredValidationAlias(BaseModel):
|
||||
p: str = Field(..., validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-required-validation-alias", operation_id="model_required_validation_alias"
|
||||
)
|
||||
def read_model_required_validation_alias(
|
||||
p: FormModelRequiredValidationAlias,
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-validation-alias", "/model-required-validation-alias"],
|
||||
)
|
||||
def test_required_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": {"title": "P Val Alias", "type": "string"},
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize("json", [None, {}])
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_missing(
|
||||
path: str, json: Union[Dict[str, Any], None]
|
||||
):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json=json)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": IsOneOf( # /required-validation-alias fails here
|
||||
["body", "p_val_alias"], ["body"]
|
||||
),
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": "hello"})
|
||||
assert response.status_code == 422, ( # /required-validation-alias fails here
|
||||
response.text
|
||||
)
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p": "hello"}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_val_alias": "hello"})
|
||||
assert response.status_code == 200, ( # /required-validation-alias fails here
|
||||
response.text
|
||||
)
|
||||
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/required-alias-and-validation-alias",
|
||||
operation_id="required_alias_and_validation_alias",
|
||||
)
|
||||
def read_required_alias_and_validation_alias(
|
||||
p: str = Body(..., embed=True, alias="p_alias", validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelRequiredAliasAndValidationAlias(BaseModel):
|
||||
p: str = Field(..., alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-required-alias-and-validation-alias",
|
||||
operation_id="model_required_alias_and_validation_alias",
|
||||
)
|
||||
def read_model_required_alias_and_validation_alias(
|
||||
p: FormModelRequiredAliasAndValidationAlias,
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": {"title": "P Val Alias", "type": "string"},
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize("json", [None, {}])
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_missing(
|
||||
path: str, json: Union[Dict[str, Any], None]
|
||||
):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json=json)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": IsOneOf( # /required-alias-and-validation-alias fails here
|
||||
["body"], ["body", "p_val_alias"]
|
||||
),
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p": "hello"})
|
||||
assert response.status_code == 422
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
"p_val_alias", # /required-alias-and-validation-alias fails here
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": {"p": "hello"},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_alias": "hello"})
|
||||
assert response.status_code == 422, (
|
||||
response.text # /required-alias-and-validation-alias fails here
|
||||
)
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p_alias": "hello"}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, json={"p_val_alias": "hello"})
|
||||
assert response.status_code == 200, (
|
||||
response.text # /required-alias-and-validation-alias fails here
|
||||
)
|
||||
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
from typing import Any, Dict
|
||||
|
||||
|
||||
def get_body_model_name(openapi: Dict[str, Any], path: str) -> str:
|
||||
body = openapi["paths"][path]["post"]["requestBody"]
|
||||
body_schema = body["content"]["application/json"]["schema"]
|
||||
return body_schema.get("$ref", "").split("/")[-1]
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Currently, there is no way to pass multiple cookies with the same name.
|
||||
# The only way to pass multiple values for cookie params is to serialize them using
|
||||
# a comma as a delimiter, but this is not currently supported by Starlette.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Currently, there is no way to pass multiple cookies with the same name.
|
||||
# The only way to pass multiple values for cookie params is to serialize them using
|
||||
# a comma as a delimiter, but this is not currently supported by Starlette.
|
||||
|
|
@ -0,0 +1,378 @@
|
|||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import Cookie, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.get("/optional-str")
|
||||
async def read_optional_str(p: Optional[str] = Cookie(None)):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class CookieModelOptionalStr(BaseModel):
|
||||
p: Optional[str] = None
|
||||
|
||||
|
||||
@app.get("/model-optional-str")
|
||||
async def read_model_optional_str(p: CookieModelOptionalStr = Cookie()):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
IsDict(
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P",
|
||||
},
|
||||
"name": "p",
|
||||
"in": "cookie",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "P", "type": "string"},
|
||||
"name": "p",
|
||||
"in": "cookie",
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.get("/optional-alias")
|
||||
async def read_optional_alias(p: Optional[str] = Cookie(None, alias="p_alias")):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class CookieModelOptionalAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, alias="p_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-alias")
|
||||
async def read_model_optional_alias(p: CookieModelOptionalAlias = Cookie()):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_str_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
IsDict(
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Alias",
|
||||
},
|
||||
"name": "p_alias",
|
||||
"in": "cookie",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "P Alias", "type": "string"},
|
||||
"name": "p_alias",
|
||||
"in": "cookie",
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias",
|
||||
pytest.param(
|
||||
"/model-optional-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_optional_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p_alias", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"} # /model-optional-alias fails here
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.get("/optional-validation-alias")
|
||||
def read_optional_validation_alias(
|
||||
p: Optional[str] = Cookie(None, validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class CookieModelOptionalValidationAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-validation-alias")
|
||||
def read_model_optional_validation_alias(
|
||||
p: CookieModelOptionalValidationAlias = Cookie(),
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-validation-alias", "/model-optional-validation-alias"],
|
||||
)
|
||||
def test_optional_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "cookie",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-validation-alias", "/model-optional-validation-alias"],
|
||||
)
|
||||
def test_optional_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p_val_alias", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"} # /optional-validation-alias fails here
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.get("/optional-alias-and-validation-alias")
|
||||
def read_optional_alias_and_validation_alias(
|
||||
p: Optional[str] = Cookie(None, alias="p_alias", validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class CookieModelOptionalAliasAndValidationAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-alias-and-validation-alias")
|
||||
def read_model_optional_alias_and_validation_alias(
|
||||
p: CookieModelOptionalAliasAndValidationAlias = Cookie(),
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "cookie",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p_alias", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"p": None # /optional-alias-and-validation-alias fails here
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p_val_alias", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"p": "hello" # /optional-alias-and-validation-alias fails here
|
||||
}
|
||||
|
|
@ -0,0 +1,502 @@
|
|||
import pytest
|
||||
from dirty_equals import IsDict, IsOneOf
|
||||
from fastapi import Cookie, FastAPI
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.get("/required-str")
|
||||
async def read_required_str(p: str = Cookie(...)):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class CookieModelRequiredStr(BaseModel):
|
||||
p: str
|
||||
|
||||
|
||||
@app.get("/model-required-str")
|
||||
async def read_model_required_str(p: CookieModelRequiredStr = Cookie()):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "P", "type": "string"},
|
||||
"name": "p",
|
||||
"in": "cookie",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["cookie", "p"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["cookie", "p"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.get("/required-alias")
|
||||
async def read_required_alias(p: str = Cookie(..., alias="p_alias")):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class CookieModelRequiredAlias(BaseModel):
|
||||
p: str = Field(..., alias="p_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-alias")
|
||||
async def read_model_required_alias(p: CookieModelRequiredAlias = Cookie()):
|
||||
return {"p": p.p} # pragma: no cover
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-alias", "/model-required-alias"],
|
||||
)
|
||||
def test_required_str_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "P Alias", "type": "string"},
|
||||
"name": "p_alias",
|
||||
"in": "cookie",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-alias", "/model-required-alias"],
|
||||
)
|
||||
def test_required_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["cookie", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["cookie", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias",
|
||||
pytest.param(
|
||||
"/model-required-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 models",
|
||||
strict=False,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_required_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["cookie", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(
|
||||
None,
|
||||
{"p": "hello"}, # /model-required-alias PDv2 fails here
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["cookie", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias",
|
||||
pytest.param(
|
||||
"/model-required-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_required_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p_alias", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200, ( # /model-required-alias fails here
|
||||
response.text
|
||||
)
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.get("/required-validation-alias")
|
||||
def read_required_validation_alias(
|
||||
p: str = Cookie(..., validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class CookieModelRequiredValidationAlias(BaseModel):
|
||||
p: str = Field(..., validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-validation-alias")
|
||||
def read_model_required_validation_alias(
|
||||
p: CookieModelRequiredValidationAlias = Cookie(),
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-validation-alias", "/model-required-validation-alias"],
|
||||
)
|
||||
def test_required_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "P Val Alias", "type": "string"},
|
||||
"name": "p_val_alias",
|
||||
"in": "cookie",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"cookie",
|
||||
"p_val_alias", # /required-validation-alias fails here
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422, ( # /required-validation-alias fails here
|
||||
response.text
|
||||
)
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["cookie", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p": "hello"}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p_val_alias", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200, ( # /required-validation-alias fails here
|
||||
response.text
|
||||
)
|
||||
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.get("/required-alias-and-validation-alias")
|
||||
def read_required_alias_and_validation_alias(
|
||||
p: str = Cookie(..., alias="p_alias", validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class CookieModelRequiredAliasAndValidationAlias(BaseModel):
|
||||
p: str = Field(..., alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-alias-and-validation-alias")
|
||||
def read_model_required_alias_and_validation_alias(
|
||||
p: CookieModelRequiredAliasAndValidationAlias = Cookie(),
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "P Val Alias", "type": "string"},
|
||||
"name": "p_val_alias",
|
||||
"in": "cookie",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"cookie",
|
||||
"p_val_alias", # /required-alias-and-validation-alias fails here
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"cookie",
|
||||
"p_val_alias", # /required-alias-and-validation-alias fails here
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf( # /model-alias-and-validation-alias fails here
|
||||
None,
|
||||
{"p": "hello"},
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p_alias", "hello")
|
||||
response = client.get(path)
|
||||
assert (
|
||||
response.status_code == 422 # /required-alias-and-validation-alias fails here
|
||||
)
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["cookie", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf( # /model-alias-and-validation-alias fails here
|
||||
None,
|
||||
{"p_alias": "hello"},
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
client.cookies.set("p_val_alias", "hello")
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200, (
|
||||
response.text # /required-alias-and-validation-alias fails here
|
||||
)
|
||||
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
|
@ -0,0 +1,816 @@
|
|||
from typing import List
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict, IsOneOf, IsPartialDict
|
||||
from fastapi import FastAPI, File, Form, UploadFile
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
from .utils import get_body_model_name
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.post("/list-bytes", operation_id="list_bytes")
|
||||
async def read_list_bytes(p: List[bytes] = File(...)):
|
||||
return {"file_size": [len(file) for file in p]}
|
||||
|
||||
|
||||
@app.post("/list-uploadfile", operation_id="list_uploadfile")
|
||||
async def read_list_uploadfile(p: List[UploadFile] = File(...)):
|
||||
return {"file_size": [file.size for file in p]}
|
||||
|
||||
|
||||
class FormModelListBytes(BaseModel):
|
||||
p: List[bytes] = File(...)
|
||||
|
||||
|
||||
@app.post("/model-list-bytes", operation_id="model_list_bytes")
|
||||
async def read_model_list_bytes(
|
||||
p: FormModelListBytes = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": [len(file) for file in p.p]}
|
||||
|
||||
|
||||
class FormModelListUploadFile(BaseModel):
|
||||
p: List[UploadFile] = File(...)
|
||||
|
||||
|
||||
@app.post("/model-list-uploadfile", operation_id="model_list_uploadfile")
|
||||
async def read_model_list_uploadfile(
|
||||
p: FormModelListUploadFile = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": [file.size for file in p.p]}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes",
|
||||
"/model-list-bytes",
|
||||
"/list-uploadfile",
|
||||
"/model-list-uploadfile",
|
||||
],
|
||||
)
|
||||
def test_list_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p": (
|
||||
IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P",
|
||||
},
|
||||
)
|
||||
| IsDict(
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
"title": "P",
|
||||
},
|
||||
)
|
||||
)
|
||||
},
|
||||
"required": ["p"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes",
|
||||
"/model-list-bytes",
|
||||
"/list-uploadfile",
|
||||
"/model-list-uploadfile",
|
||||
],
|
||||
)
|
||||
def test_list_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes",
|
||||
"/model-list-bytes",
|
||||
"/list-uploadfile",
|
||||
"/model-list-uploadfile",
|
||||
],
|
||||
)
|
||||
def test_list(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": [5, 5]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.post("/list-bytes-alias", operation_id="list_bytes_alias")
|
||||
async def read_list_bytes_alias(p: List[bytes] = File(..., alias="p_alias")):
|
||||
return {"file_size": [len(file) for file in p]}
|
||||
|
||||
|
||||
@app.post("/list-uploadfile-alias", operation_id="list_uploadfile_alias")
|
||||
async def read_list_uploadfile_alias(p: List[UploadFile] = File(..., alias="p_alias")):
|
||||
return {"file_size": [file.size for file in p]}
|
||||
|
||||
|
||||
class FormModelListBytesAlias(BaseModel):
|
||||
p: List[bytes] = File(..., alias="p_alias")
|
||||
|
||||
|
||||
@app.post("/model-list-bytes-alias", operation_id="model_list_bytes_alias")
|
||||
async def read_model_list_bytes_alias(
|
||||
p: FormModelListBytesAlias = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": [len(file) for file in p.p]}
|
||||
|
||||
|
||||
class FormModelListUploadFileAlias(BaseModel):
|
||||
p: List[UploadFile] = File(..., alias="p_alias")
|
||||
|
||||
|
||||
@app.post("/model-list-uploadfile-alias", operation_id="model_list_uploadfile_alias")
|
||||
async def read_model_list_uploadfile_alias(
|
||||
p: FormModelListUploadFileAlias = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": [file.size for file in p.p]}
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2",
|
||||
strict=False,
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes-alias",
|
||||
"/model-list-bytes-alias",
|
||||
"/list-uploadfile-alias",
|
||||
"/model-list-uploadfile-alias",
|
||||
],
|
||||
)
|
||||
def test_list_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_alias": (
|
||||
IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Alias",
|
||||
},
|
||||
)
|
||||
| IsDict(
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
"title": "P Alias",
|
||||
},
|
||||
)
|
||||
)
|
||||
},
|
||||
"required": ["p_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes-alias",
|
||||
pytest.param(
|
||||
"/model-list-bytes-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
strict=False,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 model",
|
||||
),
|
||||
),
|
||||
"/list-uploadfile-alias",
|
||||
pytest.param(
|
||||
"/model-list-uploadfile-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
strict=False,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 model",
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_list_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_alias"], # model-list-*-alias fail here
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes-alias",
|
||||
pytest.param(
|
||||
"/model-list-bytes-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 model",
|
||||
strict=False,
|
||||
),
|
||||
),
|
||||
"/list-uploadfile-alias",
|
||||
pytest.param(
|
||||
"/model-list-uploadfile-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 model",
|
||||
strict=False,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_list_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
|
||||
assert response.status_code == 422 # model-list-uploadfile-alias fail here
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf( # model-list-bytes-alias fail here
|
||||
None,
|
||||
{"p": [IsPartialDict({"size": 5}), IsPartialDict({"size": 5})]},
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes-alias",
|
||||
pytest.param(
|
||||
"/model-list-bytes-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
strict=False,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 model",
|
||||
),
|
||||
),
|
||||
"/list-uploadfile-alias",
|
||||
pytest.param(
|
||||
"/model-list-uploadfile-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
strict=False,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 model",
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_list_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")])
|
||||
assert response.status_code == 200, ( # model-list-*-alias fail here
|
||||
response.text
|
||||
)
|
||||
assert response.json() == {"file_size": [5, 5]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.post("/list-bytes-validation-alias", operation_id="list_bytes_validation_alias")
|
||||
def read_list_bytes_validation_alias(
|
||||
p: List[bytes] = File(..., validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"file_size": [len(file) for file in p]}
|
||||
|
||||
|
||||
@app.post(
|
||||
"/list-uploadfile-validation-alias",
|
||||
operation_id="list_uploadfile_validation_alias",
|
||||
)
|
||||
def read_list_uploadfile_validation_alias(
|
||||
p: List[UploadFile] = File(..., validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"file_size": [file.size for file in p]}
|
||||
|
||||
|
||||
class FormModelRequiredBytesValidationAlias(BaseModel):
|
||||
p: List[bytes] = File(..., validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-list-bytes-validation-alias",
|
||||
operation_id="model_list_bytes_validation_alias",
|
||||
)
|
||||
def read_model_list_bytes_validation_alias(
|
||||
p: FormModelRequiredBytesValidationAlias = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": [len(file) for file in p.p]} # pragma: no cover
|
||||
|
||||
|
||||
class FormModelRequiredUploadFileValidationAlias(BaseModel):
|
||||
p: List[UploadFile] = File(..., validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-list-uploadfile-validation-alias",
|
||||
operation_id="model_list_uploadfile_validation_alias",
|
||||
)
|
||||
def read_model_list_uploadfile_validation_alias(
|
||||
p: FormModelRequiredUploadFileValidationAlias = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": [file.size for file in p.p]} # pragma: no cover
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes-validation-alias",
|
||||
"/model-list-uploadfile-validation-alias",
|
||||
"/list-uploadfile-validation-alias",
|
||||
"/model-list-bytes-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_list_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": (
|
||||
IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
)
|
||||
| IsDict(
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
)
|
||||
)
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/list-bytes-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-list-bytes-validation-alias",
|
||||
pytest.param(
|
||||
"/list-uploadfile-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-list-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_list_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [ # /list-*-validation-alias fail here
|
||||
"body",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/list-bytes-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
pytest.param(
|
||||
"/model-list-bytes-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
pytest.param(
|
||||
"/list-uploadfile-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-list-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_list_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
|
||||
assert response.status_code == 422, ( # /list-*-validation-alias fail here
|
||||
response.text
|
||||
)
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf( # /model-list-bytes-validation-alias fails here
|
||||
None,
|
||||
{"p": [IsPartialDict({"size": 5}), IsPartialDict({"size": 5})]},
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes-validation-alias",
|
||||
"/model-list-bytes-validation-alias",
|
||||
"/list-uploadfile-validation-alias",
|
||||
"/model-list-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_list_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(
|
||||
path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")]
|
||||
)
|
||||
assert response.status_code == 200, response.text # all 4 fail here
|
||||
assert response.json() == {"file_size": [5, 5]} # pragma: no cover
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/list-bytes-alias-and-validation-alias",
|
||||
operation_id="list_bytes_alias_and_validation_alias",
|
||||
)
|
||||
def read_list_bytes_alias_and_validation_alias(
|
||||
p: List[bytes] = File(..., alias="p_alias", validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"file_size": [len(file) for file in p]}
|
||||
|
||||
|
||||
@app.post(
|
||||
"/list-uploadfile-alias-and-validation-alias",
|
||||
operation_id="list_uploadfile_alias_and_validation_alias",
|
||||
)
|
||||
def read_list_uploadfile_alias_and_validation_alias(
|
||||
p: List[UploadFile] = File(..., alias="p_alias", validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"file_size": [file.size for file in p]}
|
||||
|
||||
|
||||
class FormModelRequiredBytesAliasAndValidationAlias(BaseModel):
|
||||
p: List[bytes] = File(..., alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-list-bytes-alias-and-validation-alias",
|
||||
operation_id="model_list_bytes_alias_and_validation_alias",
|
||||
)
|
||||
def read_model_list_bytes_alias_and_validation_alias(
|
||||
p: FormModelRequiredBytesAliasAndValidationAlias = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": [len(file) for file in p.p]} # pragma: no cover
|
||||
|
||||
|
||||
class FormModelRequiredUploadFileAliasAndValidationAlias(BaseModel):
|
||||
p: List[UploadFile] = File(..., alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-list-uploadfile-alias-and-validation-alias",
|
||||
operation_id="model_list_uploadfile_alias_and_validation_alias",
|
||||
)
|
||||
def read_model_list_uploadfile_alias_and_validation_alias(
|
||||
p: FormModelRequiredUploadFileAliasAndValidationAlias = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": [file.size for file in p.p]} # pragma: no cover
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes-alias-and-validation-alias",
|
||||
"/model-list-bytes-alias-and-validation-alias",
|
||||
"/list-uploadfile-alias-and-validation-alias",
|
||||
"/model-list-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_list_alias_and_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": (
|
||||
IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
)
|
||||
| IsDict(
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
)
|
||||
)
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/list-bytes-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-list-bytes-alias-and-validation-alias",
|
||||
pytest.param(
|
||||
"/list-uploadfile-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-list-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_list_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
"p_val_alias", # /list-*-alias-and-validation-alias fail here
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes-alias-and-validation-alias",
|
||||
"/model-list-bytes-alias-and-validation-alias",
|
||||
"/list-uploadfile-alias-and-validation-alias",
|
||||
"/model-list-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_list_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", "hello"), ("p", "world")])
|
||||
assert response.status_code == 422
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
"p_val_alias", # /list-*-alias-and-validation-alias fail here
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(
|
||||
None,
|
||||
# /model-list-*-alias-and-validation-alias fail here
|
||||
{"p": [IsPartialDict({"size": 5}), IsPartialDict({"size": 5})]},
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/list-bytes-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
pytest.param(
|
||||
"/model-list-bytes-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
pytest.param(
|
||||
"/list-uploadfile-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-list-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_list_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")])
|
||||
assert response.status_code == 422, (
|
||||
response.text # /list-*-alias-and-validation-alias fails here
|
||||
)
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(
|
||||
None,
|
||||
# /model-list-bytes-alias-and-validation-alias fails here
|
||||
{
|
||||
"p_alias": [
|
||||
IsPartialDict({"size": 5}),
|
||||
IsPartialDict({"size": 5}),
|
||||
]
|
||||
},
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/list-bytes-alias-and-validation-alias",
|
||||
"/model-list-bytes-alias-and-validation-alias",
|
||||
"/list-uploadfile-alias-and-validation-alias",
|
||||
"/model-list-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_list_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(
|
||||
path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")]
|
||||
)
|
||||
assert response.status_code == 200, ( # all 4 fail here
|
||||
response.text
|
||||
)
|
||||
assert response.json() == {"file_size": [5, 5]} # pragma: no cover
|
||||
|
|
@ -0,0 +1,635 @@
|
|||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI, File, Form, UploadFile
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
from .utils import get_body_model_name
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.post("/optional-bytes", operation_id="optional_bytes")
|
||||
async def read_optional_bytes(p: Optional[bytes] = File(None)):
|
||||
return {"file_size": len(p) if p else None}
|
||||
|
||||
|
||||
@app.post("/optional-uploadfile", operation_id="optional_uploadfile")
|
||||
async def read_optional_uploadfile(p: Optional[UploadFile] = File(None)):
|
||||
return {"file_size": p.size if p else None}
|
||||
|
||||
|
||||
class FormModelOptionalBytes(BaseModel):
|
||||
p: Optional[bytes] = File(None)
|
||||
|
||||
|
||||
@app.post("/model-optional-bytes", operation_id="model_optional_bytes")
|
||||
async def read_model_optional_bytes(
|
||||
p: FormModelOptionalBytes = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": len(p.p) if p.p else None}
|
||||
|
||||
|
||||
class FormModelOptionalUploadFile(BaseModel):
|
||||
p: Optional[UploadFile] = File(None)
|
||||
|
||||
|
||||
@app.post("/model-optional-uploadfile", operation_id="model_optional_uploadfile")
|
||||
async def read_model_optional_uploadfile(
|
||||
p: FormModelOptionalUploadFile = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": p.p.size if p.p else None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes",
|
||||
"/model-optional-bytes",
|
||||
"/optional-uploadfile",
|
||||
"/model-optional-uploadfile",
|
||||
],
|
||||
)
|
||||
def test_optional_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p": (
|
||||
IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{"type": "string", "format": "binary"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "P", "type": "string", "format": "binary"}
|
||||
)
|
||||
),
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes",
|
||||
"/model-optional-bytes",
|
||||
"/optional-uploadfile",
|
||||
"/model-optional-uploadfile",
|
||||
],
|
||||
)
|
||||
def test_optional_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes",
|
||||
"/model-optional-bytes",
|
||||
"/optional-uploadfile",
|
||||
"/model-optional-uploadfile",
|
||||
],
|
||||
)
|
||||
def test_optional(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", b"hello")])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": 5}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.post("/optional-bytes-alias", operation_id="optional_bytes_alias")
|
||||
async def read_optional_bytes_alias(p: Optional[bytes] = File(None, alias="p_alias")):
|
||||
return {"file_size": len(p) if p else None}
|
||||
|
||||
|
||||
@app.post("/optional-uploadfile-alias", operation_id="optional_uploadfile_alias")
|
||||
async def read_optional_uploadfile_alias(
|
||||
p: Optional[UploadFile] = File(None, alias="p_alias"),
|
||||
):
|
||||
return {"file_size": p.size if p else None}
|
||||
|
||||
|
||||
class FormModelOptionalBytesAlias(BaseModel):
|
||||
p: Optional[bytes] = File(None, alias="p_alias")
|
||||
|
||||
|
||||
@app.post("/model-optional-bytes-alias", operation_id="model_optional_bytes_alias")
|
||||
async def read_model_optional_bytes_alias(
|
||||
p: FormModelOptionalBytesAlias = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": len(p.p) if p.p else None}
|
||||
|
||||
|
||||
class FormModelOptionalUploadFileAlias(BaseModel):
|
||||
p: Optional[UploadFile] = File(None, alias="p_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-optional-uploadfile-alias", operation_id="model_optional_uploadfile_alias"
|
||||
)
|
||||
async def read_model_optional_uploadfile_alias(
|
||||
p: FormModelOptionalUploadFileAlias = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": p.p.size if p.p else None}
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2",
|
||||
strict=False,
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes-alias",
|
||||
"/model-optional-bytes-alias",
|
||||
"/optional-uploadfile-alias",
|
||||
"/model-optional-uploadfile-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_alias": (
|
||||
IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{"type": "string", "format": "binary"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Alias",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "P Alias", "type": "string", "format": "binary"}
|
||||
)
|
||||
),
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes-alias",
|
||||
"/model-optional-bytes-alias",
|
||||
"/optional-uploadfile-alias",
|
||||
"/model-optional-uploadfile-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes-alias",
|
||||
pytest.param(
|
||||
"/model-optional-bytes-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 model",
|
||||
strict=False,
|
||||
),
|
||||
),
|
||||
"/optional-uploadfile-alias",
|
||||
pytest.param(
|
||||
"/model-optional-uploadfile-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 model",
|
||||
strict=False,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_optional_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", b"hello")])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": None} # model-optional-*-alias fail here
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes-alias",
|
||||
pytest.param(
|
||||
"/model-optional-bytes-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
strict=False,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 model",
|
||||
),
|
||||
),
|
||||
"/optional-uploadfile-alias",
|
||||
pytest.param(
|
||||
"/model-optional-uploadfile-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
strict=False,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 model",
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_optional_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p_alias", b"hello")])
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": 5} # model-optional-*-alias fail here
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/optional-bytes-validation-alias", operation_id="optional_bytes_validation_alias"
|
||||
)
|
||||
def read_optional_bytes_validation_alias(
|
||||
p: Optional[bytes] = File(None, validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"file_size": len(p) if p else None}
|
||||
|
||||
|
||||
@app.post(
|
||||
"/optional-uploadfile-validation-alias",
|
||||
operation_id="optional_uploadfile_validation_alias",
|
||||
)
|
||||
def read_optional_uploadfile_validation_alias(
|
||||
p: Optional[UploadFile] = File(None, validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"file_size": p.size if p else None}
|
||||
|
||||
|
||||
class FormModelOptionalBytesValidationAlias(BaseModel):
|
||||
p: Optional[bytes] = File(None, validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-optional-bytes-validation-alias",
|
||||
operation_id="model_optional_bytes_validation_alias",
|
||||
)
|
||||
def read_model_optional_bytes_validation_alias(
|
||||
p: FormModelOptionalBytesValidationAlias = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": len(p.p) if p.p else None}
|
||||
|
||||
|
||||
class FormModelOptionalUploadFileValidationAlias(BaseModel):
|
||||
p: Optional[UploadFile] = File(None, validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-optional-uploadfile-validation-alias",
|
||||
operation_id="model_optional_uploadfile_validation_alias",
|
||||
)
|
||||
def read_model_optional_uploadfile_validation_alias(
|
||||
p: FormModelOptionalUploadFileValidationAlias = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": p.p.size if p.p else None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes-validation-alias",
|
||||
"/model-optional-uploadfile-validation-alias",
|
||||
"/optional-uploadfile-validation-alias",
|
||||
"/model-optional-bytes-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": (
|
||||
IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{"type": "string", "format": "binary"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "P Val Alias", "type": "string", "format": "binary"}
|
||||
)
|
||||
),
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes-validation-alias",
|
||||
"/model-optional-bytes-validation-alias",
|
||||
"/optional-uploadfile-validation-alias",
|
||||
"/model-optional-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-bytes-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-bytes-validation-alias",
|
||||
pytest.param(
|
||||
"/optional-uploadfile-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", b"hello")])
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == { # /optional-*-validation-alias fail here
|
||||
"file_size": None
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-bytes-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
pytest.param(
|
||||
"/model-optional-bytes-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
pytest.param(
|
||||
"/optional-uploadfile-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p_val_alias", b"hello")])
|
||||
assert response.status_code == 200, (
|
||||
response.text # /model-optional-bytes-validation-alias fail here
|
||||
)
|
||||
assert response.json() == {"file_size": 5} # /optional-*-validation-alias fail here
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/optional-bytes-alias-and-validation-alias",
|
||||
operation_id="optional_bytes_alias_and_validation_alias",
|
||||
)
|
||||
def read_optional_bytes_alias_and_validation_alias(
|
||||
p: Optional[bytes] = File(None, alias="p_alias", validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"file_size": len(p) if p else None}
|
||||
|
||||
|
||||
@app.post(
|
||||
"/optional-uploadfile-alias-and-validation-alias",
|
||||
operation_id="optional_uploadfile_alias_and_validation_alias",
|
||||
)
|
||||
def read_optional_uploadfile_alias_and_validation_alias(
|
||||
p: Optional[UploadFile] = File(
|
||||
None, alias="p_alias", validation_alias="p_val_alias"
|
||||
),
|
||||
):
|
||||
return {"file_size": p.size if p else None}
|
||||
|
||||
|
||||
class FormModelOptionalBytesAliasAndValidationAlias(BaseModel):
|
||||
p: Optional[bytes] = File(None, alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-optional-bytes-alias-and-validation-alias",
|
||||
operation_id="model_optional_bytes_alias_and_validation_alias",
|
||||
)
|
||||
def read_model_optional_bytes_alias_and_validation_alias(
|
||||
p: FormModelOptionalBytesAliasAndValidationAlias = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": len(p.p) if p.p else None}
|
||||
|
||||
|
||||
class FormModelOptionalUploadFileAliasAndValidationAlias(BaseModel):
|
||||
p: Optional[UploadFile] = File(
|
||||
None, alias="p_alias", validation_alias="p_val_alias"
|
||||
)
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-optional-uploadfile-alias-and-validation-alias",
|
||||
operation_id="model_optional_uploadfile_alias_and_validation_alias",
|
||||
)
|
||||
def read_model_optional_uploadfile_alias_and_validation_alias(
|
||||
p: FormModelOptionalUploadFileAliasAndValidationAlias = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": p.p.size if p.p else None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes-alias-and-validation-alias",
|
||||
"/model-optional-bytes-alias-and-validation-alias",
|
||||
"/optional-uploadfile-alias-and-validation-alias",
|
||||
"/model-optional-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": (
|
||||
IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{"type": "string", "format": "binary"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{"title": "P Val Alias", "type": "string", "format": "binary"}
|
||||
)
|
||||
),
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes-alias-and-validation-alias",
|
||||
"/model-optional-bytes-alias-and-validation-alias",
|
||||
"/optional-uploadfile-alias-and-validation-alias",
|
||||
"/model-optional-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-bytes-alias-and-validation-alias",
|
||||
"/model-optional-bytes-alias-and-validation-alias",
|
||||
"/optional-uploadfile-alias-and-validation-alias",
|
||||
"/model-optional-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-bytes-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-bytes-alias-and-validation-alias",
|
||||
pytest.param(
|
||||
"/optional-uploadfile-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p_alias", b"hello")])
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
"file_size": None # model-optional-*-alias-and-validation-alias fail here
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-bytes-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
pytest.param(
|
||||
"/model-optional-bytes-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
pytest.param(
|
||||
"/optional-uploadfile-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p_val_alias", b"hello")])
|
||||
assert response.status_code == 200, (
|
||||
response.text # model-optional-bytes-alias-and-validation-alias fails here
|
||||
)
|
||||
assert response.json() == {
|
||||
"file_size": 5
|
||||
} # /optional-*-alias-and-validation-alias fail here
|
||||
|
|
@ -0,0 +1,684 @@
|
|||
from typing import List, Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI, File, Form, UploadFile
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
from .utils import get_body_model_name
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.post("/optional-list-bytes")
|
||||
async def read_optional_list_bytes(p: Optional[List[bytes]] = File(None)):
|
||||
return {"file_size": [len(file) for file in p] if p else None}
|
||||
|
||||
|
||||
@app.post("/optional-list-uploadfile")
|
||||
async def read_optional_list_uploadfile(p: Optional[List[UploadFile]] = File(None)):
|
||||
return {"file_size": [file.size for file in p] if p else None}
|
||||
|
||||
|
||||
class FormModelOptionalListBytes(BaseModel):
|
||||
p: Optional[List[bytes]] = File(None)
|
||||
|
||||
|
||||
@app.post("/model-optional-list-bytes")
|
||||
async def read_model_optional_list_bytes(
|
||||
p: FormModelOptionalListBytes = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": [len(file) for file in p.p] if p.p else None}
|
||||
|
||||
|
||||
class FormModelOptionalListUploadFile(BaseModel):
|
||||
p: Optional[List[UploadFile]] = File(None)
|
||||
|
||||
|
||||
@app.post("/model-optional-list-uploadfile")
|
||||
async def read_model_optional_list_uploadfile(
|
||||
p: FormModelOptionalListUploadFile = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": [file.size for file in p.p] if p.p else None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes",
|
||||
"/model-optional-list-bytes",
|
||||
"/optional-list-uploadfile",
|
||||
"/model-optional-list-uploadfile",
|
||||
],
|
||||
)
|
||||
def test_optional_list_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p": (
|
||||
IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"title": "P",
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
},
|
||||
)
|
||||
),
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes",
|
||||
"/model-optional-list-bytes",
|
||||
"/optional-list-uploadfile",
|
||||
"/model-optional-list-uploadfile",
|
||||
],
|
||||
)
|
||||
def test_optional_list_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-list-bytes",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=(TypeError, AssertionError),
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 due to #14297",
|
||||
strict=False,
|
||||
),
|
||||
),
|
||||
pytest.param(
|
||||
"/model-optional-list-bytes",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=(TypeError, AssertionError),
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 due to #14297",
|
||||
strict=False,
|
||||
),
|
||||
),
|
||||
"/optional-list-uploadfile",
|
||||
"/model-optional-list-uploadfile",
|
||||
],
|
||||
)
|
||||
def test_optional_list(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": [5, 5]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.post("/optional-list-bytes-alias")
|
||||
async def read_optional_list_bytes_alias(
|
||||
p: Optional[List[bytes]] = File(None, alias="p_alias"),
|
||||
):
|
||||
return {"file_size": [len(file) for file in p] if p else None}
|
||||
|
||||
|
||||
@app.post("/optional-list-uploadfile-alias")
|
||||
async def read_optional_list_uploadfile_alias(
|
||||
p: Optional[List[UploadFile]] = File(None, alias="p_alias"),
|
||||
):
|
||||
return {"file_size": [file.size for file in p] if p else None}
|
||||
|
||||
|
||||
class FormModelOptionalListBytesAlias(BaseModel):
|
||||
p: Optional[List[bytes]] = File(None, alias="p_alias")
|
||||
|
||||
|
||||
@app.post("/model-optional-list-bytes-alias")
|
||||
async def read_model_optional_list_bytes_alias(
|
||||
p: FormModelOptionalListBytesAlias = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": [len(file) for file in p.p] if p.p else None}
|
||||
|
||||
|
||||
class FormModelOptionalListUploadFileAlias(BaseModel):
|
||||
p: Optional[List[UploadFile]] = File(None, alias="p_alias")
|
||||
|
||||
|
||||
@app.post("/model-optional-list-uploadfile-alias")
|
||||
async def read_model_optional_list_uploadfile_alias(
|
||||
p: FormModelOptionalListUploadFileAlias = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": [file.size for file in p.p] if p.p else None}
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2",
|
||||
strict=False,
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes-alias",
|
||||
"/model-optional-list-bytes-alias",
|
||||
"/optional-list-uploadfile-alias",
|
||||
"/model-optional-list-uploadfile-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_alias": (
|
||||
IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Alias",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"title": "P Alias",
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
}
|
||||
)
|
||||
),
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes-alias",
|
||||
"/model-optional-list-bytes-alias",
|
||||
"/optional-list-uploadfile-alias",
|
||||
"/model-optional-list-uploadfile-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes-alias",
|
||||
pytest.param(
|
||||
"/model-optional-list-bytes-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 model",
|
||||
strict=False,
|
||||
),
|
||||
),
|
||||
"/optional-list-uploadfile-alias",
|
||||
pytest.param(
|
||||
"/model-optional-list-uploadfile-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 model",
|
||||
strict=False,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
|
||||
assert response.status_code == 200, (
|
||||
response.text # model-optional-list-*-alias fail here
|
||||
)
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-list-bytes-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=(TypeError, AssertionError),
|
||||
strict=False,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 model due to #14297",
|
||||
),
|
||||
),
|
||||
pytest.param(
|
||||
"/model-optional-list-bytes-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=(TypeError, AssertionError),
|
||||
strict=False,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 model due to #14297",
|
||||
),
|
||||
),
|
||||
"/optional-list-uploadfile-alias",
|
||||
pytest.param(
|
||||
"/model-optional-list-uploadfile-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
strict=False,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 model",
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")])
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {
|
||||
"file_size": [5, 5] # /model-optional-list-uploadfile-alias fails here
|
||||
}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.post("/optional-list-bytes-validation-alias")
|
||||
def read_optional_list_bytes_validation_alias(
|
||||
p: Optional[List[bytes]] = File(None, validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"file_size": [len(file) for file in p] if p else None}
|
||||
|
||||
|
||||
@app.post("/optional-list-uploadfile-validation-alias")
|
||||
def read_optional_list_uploadfile_validation_alias(
|
||||
p: Optional[List[UploadFile]] = File(None, validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"file_size": [file.size for file in p] if p else None}
|
||||
|
||||
|
||||
class FormModelOptionalListBytesValidationAlias(BaseModel):
|
||||
p: Optional[List[bytes]] = File(None, validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post("/model-optional-list-bytes-validation-alias")
|
||||
def read_model_optional_list_bytes_validation_alias(
|
||||
p: FormModelOptionalListBytesValidationAlias = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": [len(file) for file in p.p] if p.p else None}
|
||||
|
||||
|
||||
class FormModelOptionalListUploadFileValidationAlias(BaseModel):
|
||||
p: Optional[List[UploadFile]] = File(None, validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post("/model-optional-list-uploadfile-validation-alias")
|
||||
def read_model_optional_list_uploadfile_validation_alias(
|
||||
p: FormModelOptionalListUploadFileValidationAlias = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": [file.size for file in p.p] if p.p else None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes-validation-alias",
|
||||
"/model-optional-list-uploadfile-validation-alias",
|
||||
"/optional-list-uploadfile-validation-alias",
|
||||
"/model-optional-list-bytes-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": (
|
||||
IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"title": "P Val Alias",
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
}
|
||||
)
|
||||
),
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes-validation-alias",
|
||||
"/model-optional-list-bytes-validation-alias",
|
||||
"/optional-list-uploadfile-validation-alias",
|
||||
"/model-optional-list-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-list-bytes-validation-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=(TypeError, AssertionError),
|
||||
strict=False,
|
||||
reason="Fails due to #14297",
|
||||
),
|
||||
),
|
||||
pytest.param(
|
||||
"/model-optional-list-bytes-validation-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=(TypeError, AssertionError),
|
||||
strict=False,
|
||||
reason="Fails due to #14297",
|
||||
),
|
||||
),
|
||||
pytest.param(
|
||||
"/optional-list-uploadfile-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-list-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", b"hello"), ("p", b"world")])
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == { # /optional-list-uploadfile-validation-alias fails here
|
||||
"file_size": None
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes-validation-alias",
|
||||
"/model-optional-list-bytes-validation-alias",
|
||||
"/optional-list-uploadfile-validation-alias",
|
||||
"/model-optional-list-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(
|
||||
path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")]
|
||||
)
|
||||
assert response.status_code == 200, (
|
||||
response.text # /model-optional-list-*-validation-alias fail here
|
||||
)
|
||||
assert response.json() == {
|
||||
"file_size": [5, 5] # /optional-list-*-validation-alias fail here
|
||||
}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.post("/optional-list-bytes-alias-and-validation-alias")
|
||||
def read_optional_list_bytes_alias_and_validation_alias(
|
||||
p: Optional[List[bytes]] = File(
|
||||
None, alias="p_alias", validation_alias="p_val_alias"
|
||||
),
|
||||
):
|
||||
return {"file_size": [len(file) for file in p] if p else None}
|
||||
|
||||
|
||||
@app.post("/optional-list-uploadfile-alias-and-validation-alias")
|
||||
def read_optional_list_uploadfile_alias_and_validation_alias(
|
||||
p: Optional[List[UploadFile]] = File(
|
||||
None, alias="p_alias", validation_alias="p_val_alias"
|
||||
),
|
||||
):
|
||||
return {"file_size": [file.size for file in p] if p else None}
|
||||
|
||||
|
||||
class FormModelOptionalListBytesAliasAndValidationAlias(BaseModel):
|
||||
p: Optional[List[bytes]] = File(
|
||||
None, alias="p_alias", validation_alias="p_val_alias"
|
||||
)
|
||||
|
||||
|
||||
@app.post("/model-optional-list-bytes-alias-and-validation-alias")
|
||||
def read_model_optional_list_bytes_alias_and_validation_alias(
|
||||
p: FormModelOptionalListBytesAliasAndValidationAlias = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": [len(file) for file in p.p] if p.p else None}
|
||||
|
||||
|
||||
class FormModelOptionalListUploadFileAliasAndValidationAlias(BaseModel):
|
||||
p: Optional[List[UploadFile]] = File(
|
||||
None, alias="p_alias", validation_alias="p_val_alias"
|
||||
)
|
||||
|
||||
|
||||
@app.post("/model-optional-list-uploadfile-alias-and-validation-alias")
|
||||
def read_model_optional_list_uploadfile_alias_and_validation_alias(
|
||||
p: FormModelOptionalListUploadFileAliasAndValidationAlias = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": [file.size for file in p.p] if p.p else None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes-alias-and-validation-alias",
|
||||
"/model-optional-list-bytes-alias-and-validation-alias",
|
||||
"/optional-list-uploadfile-alias-and-validation-alias",
|
||||
"/model-optional-list-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": (
|
||||
IsDict(
|
||||
{
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"title": "P Val Alias",
|
||||
"type": "array",
|
||||
"items": {"type": "string", "format": "binary"},
|
||||
}
|
||||
)
|
||||
),
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes-alias-and-validation-alias",
|
||||
"/model-optional-list-bytes-alias-and-validation-alias",
|
||||
"/optional-list-uploadfile-alias-and-validation-alias",
|
||||
"/model-optional-list-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes-alias-and-validation-alias",
|
||||
"/model-optional-list-bytes-alias-and-validation-alias",
|
||||
"/optional-list-uploadfile-alias-and-validation-alias",
|
||||
"/model-optional-list-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-list-bytes-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=(TypeError, AssertionError),
|
||||
strict=False,
|
||||
reason="Fails due to #14297",
|
||||
),
|
||||
),
|
||||
pytest.param(
|
||||
"/model-optional-list-bytes-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=(TypeError, AssertionError),
|
||||
strict=False,
|
||||
reason="Fails due to #14297",
|
||||
),
|
||||
),
|
||||
pytest.param(
|
||||
"/optional-list-uploadfile-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-list-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p_alias", b"hello"), ("p_alias", b"world")])
|
||||
assert response.status_code == 200, response.text
|
||||
assert ( # /optional-list-uploadfile-alias-and-validation-alias fails here
|
||||
response.json() == {"file_size": None}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-bytes-alias-and-validation-alias",
|
||||
"/model-optional-list-bytes-alias-and-validation-alias",
|
||||
"/optional-list-uploadfile-alias-and-validation-alias",
|
||||
"/model-optional-list-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(
|
||||
path, files=[("p_val_alias", b"hello"), ("p_val_alias", b"world")]
|
||||
)
|
||||
assert response.status_code == 200, (
|
||||
response.text # /model-optional-list-*-alias-and-validation-alias fails here
|
||||
)
|
||||
assert response.json() == {
|
||||
"file_size": [5, 5] # /optional-list-*-alias-and-validation-alias fail here
|
||||
}
|
||||
|
|
@ -0,0 +1,756 @@
|
|||
import pytest
|
||||
from dirty_equals import IsDict, IsOneOf, IsPartialDict
|
||||
from fastapi import FastAPI, File, Form, UploadFile
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
from .utils import get_body_model_name
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.post("/required-bytes", operation_id="required_bytes")
|
||||
async def read_required_bytes(p: bytes = File(...)):
|
||||
return {"file_size": len(p)}
|
||||
|
||||
|
||||
@app.post("/required-uploadfile", operation_id="required_uploadfile")
|
||||
async def read_required_uploadfile(p: UploadFile = File(...)):
|
||||
return {"file_size": p.size}
|
||||
|
||||
|
||||
class FormModelRequiredBytes(BaseModel):
|
||||
p: bytes = File(...)
|
||||
|
||||
|
||||
@app.post("/model-required-bytes", operation_id="model_required_bytes")
|
||||
async def read_model_required_bytes(
|
||||
p: FormModelRequiredBytes = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": len(p.p)}
|
||||
|
||||
|
||||
class FormModelRequiredUploadFile(BaseModel):
|
||||
p: UploadFile = File(...)
|
||||
|
||||
|
||||
@app.post("/model-required-uploadfile", operation_id="model_required_uploadfile")
|
||||
async def read_model_required_uploadfile(
|
||||
p: FormModelRequiredUploadFile = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": p.p.size}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes",
|
||||
"/model-required-bytes",
|
||||
"/required-uploadfile",
|
||||
"/model-required-uploadfile",
|
||||
],
|
||||
)
|
||||
def test_required_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p": {"title": "P", "type": "string", "format": "binary"},
|
||||
},
|
||||
"required": ["p"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes",
|
||||
"/model-required-bytes",
|
||||
"/required-uploadfile",
|
||||
"/model-required-uploadfile",
|
||||
],
|
||||
)
|
||||
def test_required_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes",
|
||||
"/model-required-bytes",
|
||||
"/required-uploadfile",
|
||||
"/model-required-uploadfile",
|
||||
],
|
||||
)
|
||||
def test_required(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", b"hello")])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"file_size": 5}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.post("/required-bytes-alias", operation_id="required_bytes_alias")
|
||||
async def read_required_bytes_alias(p: bytes = File(..., alias="p_alias")):
|
||||
return {"file_size": len(p)}
|
||||
|
||||
|
||||
@app.post("/required-uploadfile-alias", operation_id="required_uploadfile_alias")
|
||||
async def read_required_uploadfile_alias(p: UploadFile = File(..., alias="p_alias")):
|
||||
return {"file_size": p.size}
|
||||
|
||||
|
||||
class FormModelRequiredBytesAlias(BaseModel):
|
||||
p: bytes = File(..., alias="p_alias")
|
||||
|
||||
|
||||
@app.post("/model-required-bytes-alias", operation_id="model_required_bytes_alias")
|
||||
async def read_model_required_bytes_alias(
|
||||
p: FormModelRequiredBytesAlias = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": len(p.p)}
|
||||
|
||||
|
||||
class FormModelRequiredUploadFileAlias(BaseModel):
|
||||
p: UploadFile = File(..., alias="p_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-required-uploadfile-alias", operation_id="model_required_uploadfile_alias"
|
||||
)
|
||||
async def read_model_required_uploadfile_alias(
|
||||
p: FormModelRequiredUploadFileAlias = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": p.p.size}
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2",
|
||||
strict=False,
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes-alias",
|
||||
"/model-required-bytes-alias",
|
||||
"/required-uploadfile-alias",
|
||||
"/model-required-uploadfile-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_alias": {"title": "P Alias", "type": "string", "format": "binary"},
|
||||
},
|
||||
"required": ["p_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes-alias",
|
||||
pytest.param(
|
||||
"/model-required-bytes-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
strict=False,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 model",
|
||||
),
|
||||
),
|
||||
"/required-uploadfile-alias",
|
||||
pytest.param(
|
||||
"/model-required-uploadfile-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
strict=False,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 model",
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_required_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_alias"], # model-required-*-alias fail here
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes-alias",
|
||||
pytest.param(
|
||||
"/model-required-bytes-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 model",
|
||||
strict=False,
|
||||
),
|
||||
),
|
||||
"/required-uploadfile-alias",
|
||||
pytest.param(
|
||||
"/model-required-uploadfile-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 model",
|
||||
strict=False,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_required_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", b"hello")])
|
||||
assert response.status_code == 422 # model-required-upload-alias fail here
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf( # model-required-bytes-alias fail here
|
||||
None,
|
||||
{"p": IsPartialDict({"size": 5})},
|
||||
{"p": b"hello"}, # ToDo: check this
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes-alias",
|
||||
pytest.param(
|
||||
"/model-required-bytes-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
strict=False,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 model",
|
||||
),
|
||||
),
|
||||
"/required-uploadfile-alias",
|
||||
pytest.param(
|
||||
"/model-required-uploadfile-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
strict=False,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 model",
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_required_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p_alias", b"hello")])
|
||||
assert response.status_code == 200, ( # model-required-*-alias fail here
|
||||
response.text
|
||||
)
|
||||
assert response.json() == {"file_size": 5}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/required-bytes-validation-alias", operation_id="required_bytes_validation_alias"
|
||||
)
|
||||
def read_required_bytes_validation_alias(
|
||||
p: bytes = File(..., validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"file_size": len(p)}
|
||||
|
||||
|
||||
@app.post(
|
||||
"/required-uploadfile-validation-alias",
|
||||
operation_id="required_uploadfile_validation_alias",
|
||||
)
|
||||
def read_required_uploadfile_validation_alias(
|
||||
p: UploadFile = File(..., validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"file_size": p.size}
|
||||
|
||||
|
||||
class FormModelRequiredBytesValidationAlias(BaseModel):
|
||||
p: bytes = File(..., validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-required-bytes-validation-alias",
|
||||
operation_id="model_required_bytes_validation_alias",
|
||||
)
|
||||
def read_model_required_bytes_validation_alias(
|
||||
p: FormModelRequiredBytesValidationAlias = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": len(p.p)} # pragma: no cover
|
||||
|
||||
|
||||
class FormModelRequiredUploadFileValidationAlias(BaseModel):
|
||||
p: UploadFile = File(..., validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-required-uploadfile-validation-alias",
|
||||
operation_id="model_required_uploadfile_validation_alias",
|
||||
)
|
||||
def read_model_required_uploadfile_validation_alias(
|
||||
p: FormModelRequiredUploadFileValidationAlias = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": p.p.size}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes-validation-alias",
|
||||
"/model-required-uploadfile-validation-alias",
|
||||
"/required-uploadfile-validation-alias",
|
||||
"/model-required-bytes-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"title": "P Val Alias",
|
||||
"type": "string",
|
||||
"format": "binary",
|
||||
},
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-bytes-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-bytes-validation-alias",
|
||||
pytest.param(
|
||||
"/required-uploadfile-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [ # /required-*-validation-alias fail here
|
||||
"body",
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-bytes-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
pytest.param(
|
||||
"/model-required-bytes-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
pytest.param(
|
||||
"/required-uploadfile-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p", b"hello")])
|
||||
assert response.status_code == 422, ( # /required-*-validation-alias fail here
|
||||
response.text
|
||||
)
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf( # /model-required-bytes-validation-alias fails here
|
||||
None, {"p": IsPartialDict({"size": 5})}
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-bytes-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
pytest.param(
|
||||
"/model-required-bytes-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
pytest.param(
|
||||
"/required-uploadfile-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-uploadfile-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p_val_alias", b"hello")])
|
||||
assert response.status_code == 200, ( # all 3 fail here
|
||||
response.text
|
||||
)
|
||||
assert response.json() == {"file_size": 5}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/required-bytes-alias-and-validation-alias",
|
||||
operation_id="required_bytes_alias_and_validation_alias",
|
||||
)
|
||||
def read_required_bytes_alias_and_validation_alias(
|
||||
p: bytes = File(..., alias="p_alias", validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"file_size": len(p)}
|
||||
|
||||
|
||||
@app.post(
|
||||
"/required-uploadfile-alias-and-validation-alias",
|
||||
operation_id="required_uploadfile_alias_and_validation_alias",
|
||||
)
|
||||
def read_required_uploadfile_alias_and_validation_alias(
|
||||
p: UploadFile = File(..., alias="p_alias", validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"file_size": p.size}
|
||||
|
||||
|
||||
class FormModelRequiredBytesAliasAndValidationAlias(BaseModel):
|
||||
p: bytes = File(..., alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-required-bytes-alias-and-validation-alias",
|
||||
operation_id="model_required_bytes_alias_and_validation_alias",
|
||||
)
|
||||
def read_model_required_bytes_alias_and_validation_alias(
|
||||
p: FormModelRequiredBytesAliasAndValidationAlias = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": len(p.p)} # pragma: no cover
|
||||
|
||||
|
||||
class FormModelRequiredUploadFileAliasAndValidationAlias(BaseModel):
|
||||
p: UploadFile = File(..., alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-required-uploadfile-alias-and-validation-alias",
|
||||
operation_id="model_required_uploadfile_alias_and_validation_alias",
|
||||
)
|
||||
def read_model_required_uploadfile_alias_and_validation_alias(
|
||||
p: FormModelRequiredUploadFileAliasAndValidationAlias = Form(
|
||||
media_type="multipart/form-data" # Remove media_type when https://github.com/fastapi/fastapi/pull/14343 is fixed
|
||||
),
|
||||
):
|
||||
return {"file_size": p.p.size}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-bytes-alias-and-validation-alias",
|
||||
"/model-required-bytes-alias-and-validation-alias",
|
||||
"/required-uploadfile-alias-and-validation-alias",
|
||||
"/model-required-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"title": "P Val Alias",
|
||||
"type": "string",
|
||||
"format": "binary",
|
||||
},
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-bytes-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-bytes-alias-and-validation-alias",
|
||||
pytest.param(
|
||||
"/required-uploadfile-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
"p_val_alias", # /required-*-alias-and-validation-alias fail here
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-bytes-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-bytes-alias-and-validation-alias",
|
||||
pytest.param(
|
||||
"/required-uploadfile-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files={"p": "hello"})
|
||||
assert response.status_code == 422
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
"p_val_alias", # /required-*-alias-and-validation-alias fail here
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p": IsPartialDict({"size": 5})}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-bytes-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
pytest.param(
|
||||
"/model-required-bytes-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
pytest.param(
|
||||
"/required-uploadfile-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p_alias", b"hello")])
|
||||
assert response.status_code == 422, (
|
||||
response.text # /required-*-alias-and-validation-alias fails here
|
||||
)
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(
|
||||
None,
|
||||
# /model-required-uploadfile-alias-and-validation-alias fails here
|
||||
{"p_alias": IsPartialDict({"size": 5})},
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-bytes-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
pytest.param(
|
||||
"/model-required-bytes-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
pytest.param(
|
||||
"/required-uploadfile-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-uploadfile-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, files=[("p_val_alias", b"hello")])
|
||||
assert response.status_code == 200, ( # all 3 fail here
|
||||
response.text
|
||||
)
|
||||
assert response.json() == {"file_size": 5}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
from typing import Any, Dict
|
||||
|
||||
|
||||
def get_body_model_name(openapi: Dict[str, Any], path: str) -> str:
|
||||
body = openapi["paths"][path]["post"]["requestBody"]
|
||||
body_schema = body["content"]["multipart/form-data"]["schema"]
|
||||
return body_schema.get("$ref", "").split("/")[-1]
|
||||
|
|
@ -0,0 +1,524 @@
|
|||
from typing import List
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict, IsOneOf, IsPartialDict
|
||||
from fastapi import FastAPI, Form
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
from .utils import get_body_model_name
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.post("/required-list-str", operation_id="required_list_str")
|
||||
async def read_required_list_str(p: List[str] = Form(...)):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelRequiredListStr(BaseModel):
|
||||
p: List[str]
|
||||
|
||||
|
||||
@app.post("/model-required-list-str", operation_id="model_required_list_str")
|
||||
def read_model_required_list_str(p: FormModelRequiredListStr = Form()):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-str", "/model-required-list-str"],
|
||||
)
|
||||
def test_required_list_str_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p": {
|
||||
"items": {"type": "string"},
|
||||
"title": "P",
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": ["p"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-str", "/model-required-list-str"],
|
||||
)
|
||||
def test_required_list_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-str", "/model-required-list-str"],
|
||||
)
|
||||
def test_required_list_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.post("/required-list-alias", operation_id="required_list_alias")
|
||||
async def read_required_list_alias(p: List[str] = Form(..., alias="p_alias")):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelRequiredListAlias(BaseModel):
|
||||
p: List[str] = Field(..., alias="p_alias")
|
||||
|
||||
|
||||
@app.post("/model-required-list-alias", operation_id="model_required_list_alias")
|
||||
async def read_model_required_list_alias(p: FormModelRequiredListAlias = Form()):
|
||||
return {"p": p.p} # pragma: no cover
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-list-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 models",
|
||||
strict=False,
|
||||
),
|
||||
),
|
||||
"/model-required-list-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_str_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_alias": {
|
||||
"items": {"type": "string"},
|
||||
"title": "P Alias",
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": ["p_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-alias", "/model-required-list-alias"],
|
||||
)
|
||||
def test_required_list_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias",
|
||||
pytest.param(
|
||||
"/model-required-list-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 models",
|
||||
strict=False,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": ["hello", "world"]})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf( # /model-required-list-alias with PDv2 fails here
|
||||
None, {"p": ["hello", "world"]}
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-alias", "/model-required-list-alias"],
|
||||
)
|
||||
def test_required_list_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/required-list-validation-alias", operation_id="required_list_validation_alias"
|
||||
)
|
||||
def read_required_list_validation_alias(
|
||||
p: List[str] = Form(..., validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelRequiredListValidationAlias(BaseModel):
|
||||
p: List[str] = Field(..., validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-required-list-validation-alias",
|
||||
operation_id="model_required_list_validation_alias",
|
||||
)
|
||||
async def read_model_required_list_validation_alias(
|
||||
p: FormModelRequiredListValidationAlias = Form(),
|
||||
):
|
||||
return {"p": p.p} # pragma: no cover
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-validation-alias", "/model-required-list-validation-alias"],
|
||||
)
|
||||
def test_required_list_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"items": {"type": "string"},
|
||||
"title": "P Val Alias",
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-list-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
"p_val_alias", # /required-list-validation-alias fails here
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-list-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": ["hello", "world"]})
|
||||
assert response.status_code == 422, (
|
||||
response.text # /required-list-validation-alias fails here
|
||||
)
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-validation-alias", "/model-required-list-validation-alias"],
|
||||
)
|
||||
def test_required_list_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_val_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200, response.text # both fail here
|
||||
|
||||
assert response.json() == {"p": ["hello", "world"]} # pragma: no cover
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/required-list-alias-and-validation-alias",
|
||||
operation_id="required_list_alias_and_validation_alias",
|
||||
)
|
||||
def read_required_list_alias_and_validation_alias(
|
||||
p: List[str] = Form(..., alias="p_alias", validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelRequiredListAliasAndValidationAlias(BaseModel):
|
||||
p: List[str] = Field(..., alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
operation_id="model_required_list_alias_and_validation_alias",
|
||||
)
|
||||
def read_model_required_list_alias_and_validation_alias(
|
||||
p: FormModelRequiredListAliasAndValidationAlias = Form(),
|
||||
):
|
||||
return {"p": p.p} # pragma: no cover
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"items": {"type": "string"},
|
||||
"title": "P Val Alias",
|
||||
"type": "array",
|
||||
},
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-list-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
# /required-list-alias-and-validation-alias fails here
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": ["hello", "world"]})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
# /required-list-alias-and-validation-alias fails here
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(
|
||||
None,
|
||||
# /model-required-list-alias-and-validation-alias fails here
|
||||
{"p": ["hello", "world"]},
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-list-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_alias": ["hello", "world"]})
|
||||
assert ( # /required-list-alias-and-validation-alias fails here
|
||||
response.status_code == 422
|
||||
)
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p_alias": ["hello", "world"]}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_val_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200, response.text # both fail here
|
||||
assert response.json() == {"p": ["hello", "world"]} # pragma: no cover
|
||||
|
|
@ -0,0 +1,449 @@
|
|||
from typing import List, Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI, Form
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
from .utils import get_body_model_name
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.post("/optional-list-str", operation_id="optional_list_str")
|
||||
async def read_optional_list_str(p: Optional[List[str]] = Form(None)):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelOptionalListStr(BaseModel):
|
||||
p: Optional[List[str]] = None
|
||||
|
||||
|
||||
@app.post("/model-optional-list-str", operation_id="model_optional_list_str")
|
||||
async def read_model_optional_list_str(p: FormModelOptionalListStr = Form()):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-str", "/model-optional-list-str"],
|
||||
)
|
||||
def test_optional_list_str_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p": {"items": {"type": "string"}, "type": "array", "title": "P"},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-str", "/model-optional-list-str"],
|
||||
)
|
||||
def test_optional_list_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-str", "/model-optional-list-str"],
|
||||
)
|
||||
def test_optional_list_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.post("/optional-list-alias", operation_id="optional_list_alias")
|
||||
async def read_optional_list_alias(
|
||||
p: Optional[List[str]] = Form(None, alias="p_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelOptionalListAlias(BaseModel):
|
||||
p: Optional[List[str]] = Field(None, alias="p_alias")
|
||||
|
||||
|
||||
@app.post("/model-optional-list-alias", operation_id="model_optional_list_alias")
|
||||
async def read_model_optional_list_alias(p: FormModelOptionalListAlias = Form()):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-list-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
strict=False,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2",
|
||||
),
|
||||
),
|
||||
"/model-optional-list-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_str_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p_alias": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p_alias": {
|
||||
"items": {"type": "string"},
|
||||
"type": "array",
|
||||
"title": "P Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-alias", "/model-optional-list-alias"],
|
||||
)
|
||||
def test_optional_list_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-alias", "/model-optional-list-alias"],
|
||||
)
|
||||
def test_optional_list_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-alias", "/model-optional-list-alias"],
|
||||
)
|
||||
def test_optional_list_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/optional-list-validation-alias", operation_id="optional_list_validation_alias"
|
||||
)
|
||||
def read_optional_list_validation_alias(
|
||||
p: Optional[List[str]] = Form(None, validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelOptionalListValidationAlias(BaseModel):
|
||||
p: Optional[List[str]] = Field(None, validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-optional-list-validation-alias",
|
||||
operation_id="model_optional_list_validation_alias",
|
||||
)
|
||||
def read_model_optional_list_validation_alias(
|
||||
p: FormModelOptionalListValidationAlias = Form(),
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
|
||||
)
|
||||
def test_optional_list_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"items": {"type": "string"},
|
||||
"type": "array",
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
|
||||
)
|
||||
def test_optional_list_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-list-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None} # /optional-list-validation-alias fails here
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
|
||||
)
|
||||
def test_optional_list_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_val_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200, (
|
||||
response.text # /model-optional-list-validation-alias fails here
|
||||
)
|
||||
assert response.json() == { # /optional-list-validation-alias fails here
|
||||
"p": ["hello", "world"]
|
||||
}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
operation_id="optional_list_alias_and_validation_alias",
|
||||
)
|
||||
def read_optional_list_alias_and_validation_alias(
|
||||
p: Optional[List[str]] = Form(
|
||||
None, alias="p_alias", validation_alias="p_val_alias"
|
||||
),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelOptionalListAliasAndValidationAlias(BaseModel):
|
||||
p: Optional[List[str]] = Field(
|
||||
None, alias="p_alias", validation_alias="p_val_alias"
|
||||
)
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
operation_id="model_optional_list_alias_and_validation_alias",
|
||||
)
|
||||
def read_model_optional_list_alias_and_validation_alias(
|
||||
p: FormModelOptionalListAliasAndValidationAlias = Form(),
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"items": {"type": "string"},
|
||||
"type": "array",
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"p": None # /optional-list-alias-and-validation-alias fails here
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_val_alias": ["hello", "world"]})
|
||||
assert response.status_code == 200, (
|
||||
response.text # /model-optional-list-alias-and-validation-alias fails here
|
||||
)
|
||||
assert response.json() == {
|
||||
"p": [ # /optional-list-alias-and-validation-alias fails here
|
||||
"hello",
|
||||
"world",
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,414 @@
|
|||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI, Form
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
from .utils import get_body_model_name
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.post("/optional-str", operation_id="optional_str")
|
||||
async def read_optional_str(p: Optional[str] = Form(None)):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelOptionalStr(BaseModel):
|
||||
p: Optional[str] = None
|
||||
|
||||
|
||||
@app.post("/model-optional-str", operation_id="model_optional_str")
|
||||
async def read_model_optional_str(p: FormModelOptionalStr = Form()):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p": {"type": "string", "title": "P"},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.post("/optional-alias", operation_id="optional_alias")
|
||||
async def read_optional_alias(p: Optional[str] = Form(None, alias="p_alias")):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelOptionalAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, alias="p_alias")
|
||||
|
||||
|
||||
@app.post("/model-optional-alias", operation_id="model_optional_alias")
|
||||
async def read_model_optional_alias(p: FormModelOptionalAlias = Form()):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
strict=False,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2",
|
||||
),
|
||||
),
|
||||
"/model-optional-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_str_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p_alias": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p_alias": {"type": "string", "title": "P Alias"},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_alias": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.post("/optional-validation-alias", operation_id="optional_validation_alias")
|
||||
def read_optional_validation_alias(
|
||||
p: Optional[str] = Form(None, validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelOptionalValidationAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-optional-validation-alias", operation_id="model_optional_validation_alias"
|
||||
)
|
||||
def read_model_optional_validation_alias(
|
||||
p: FormModelOptionalValidationAlias = Form(),
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-validation-alias", "/model-optional-validation-alias"],
|
||||
)
|
||||
def test_optional_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {"type": "string", "title": "P Val Alias"},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-validation-alias", "/model-optional-validation-alias"],
|
||||
)
|
||||
def test_optional_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None} # /optional-validation-alias fails here
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_val_alias": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"} # /optional-validation-alias fails here
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/optional-alias-and-validation-alias",
|
||||
operation_id="optional_alias_and_validation_alias",
|
||||
)
|
||||
def read_optional_alias_and_validation_alias(
|
||||
p: Optional[str] = Form(None, alias="p_alias", validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelOptionalAliasAndValidationAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
operation_id="model_optional_alias_and_validation_alias",
|
||||
)
|
||||
def read_model_optional_alias_and_validation_alias(
|
||||
p: FormModelOptionalAliasAndValidationAlias = Form(),
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == IsDict(
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"properties": {
|
||||
"p_val_alias": {"type": "string", "title": "P Val Alias"},
|
||||
},
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_alias": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"p": None # /optional-alias-and-validation-alias fails here
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_val_alias": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"p": "hello" # /optional-alias-and-validation-alias fails here
|
||||
}
|
||||
|
|
@ -0,0 +1,501 @@
|
|||
import pytest
|
||||
from dirty_equals import IsDict, IsOneOf
|
||||
from fastapi import FastAPI, Form
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
from .utils import get_body_model_name
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.post("/required-str", operation_id="required_str")
|
||||
async def read_required_str(p: str = Form(...)):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelRequiredStr(BaseModel):
|
||||
p: str
|
||||
|
||||
|
||||
@app.post("/model-required-str", operation_id="model_required_str")
|
||||
async def read_model_required_str(p: FormModelRequiredStr = Form()):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p": {"title": "P", "type": "string"},
|
||||
},
|
||||
"required": ["p"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.post("/required-alias", operation_id="required_alias")
|
||||
async def read_required_alias(p: str = Form(..., alias="p_alias")):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelRequiredAlias(BaseModel):
|
||||
p: str = Field(..., alias="p_alias")
|
||||
|
||||
|
||||
@app.post("/model-required-alias", operation_id="model_required_alias")
|
||||
async def read_model_required_alias(p: FormModelRequiredAlias = Form()):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2",
|
||||
strict=False,
|
||||
),
|
||||
),
|
||||
"/model-required-alias",
|
||||
],
|
||||
)
|
||||
def test_required_str_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_alias": {"title": "P Alias", "type": "string"},
|
||||
},
|
||||
"required": ["p_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-alias", "/model-required-alias"],
|
||||
)
|
||||
def test_required_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-alias", "/model-required-alias"],
|
||||
)
|
||||
def test_required_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": "hello"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p": "hello"}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["body", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-alias", "/model-required-alias"],
|
||||
)
|
||||
def test_required_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_alias": "hello"})
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.post("/required-validation-alias", operation_id="required_validation_alias")
|
||||
def read_required_validation_alias(
|
||||
p: str = Form(..., validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelRequiredValidationAlias(BaseModel):
|
||||
p: str = Field(..., validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-required-validation-alias", operation_id="model_required_validation_alias"
|
||||
)
|
||||
def read_model_required_validation_alias(
|
||||
p: FormModelRequiredValidationAlias = Form(),
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-validation-alias", "/model-required-validation-alias"],
|
||||
)
|
||||
def test_required_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": {"title": "P Val Alias", "type": "string"},
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
"p_val_alias", # /required-validation-alias fails here
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": "hello"})
|
||||
assert response.status_code == 422, ( # /required-validation-alias fails here
|
||||
response.text
|
||||
)
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p": "hello"}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_val_alias": "hello"})
|
||||
assert response.status_code == 200, ( # /required-validation-alias fails here
|
||||
response.text
|
||||
)
|
||||
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.post(
|
||||
"/required-alias-and-validation-alias",
|
||||
operation_id="required_alias_and_validation_alias",
|
||||
)
|
||||
def read_required_alias_and_validation_alias(
|
||||
p: str = Form(..., alias="p_alias", validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class FormModelRequiredAliasAndValidationAlias(BaseModel):
|
||||
p: str = Field(..., alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.post(
|
||||
"/model-required-alias-and-validation-alias",
|
||||
operation_id="model_required_alias_and_validation_alias",
|
||||
)
|
||||
def read_model_required_alias_and_validation_alias(
|
||||
p: FormModelRequiredAliasAndValidationAlias = Form(),
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_schema(path: str):
|
||||
openapi = app.openapi()
|
||||
body_model_name = get_body_model_name(openapi, path)
|
||||
|
||||
assert app.openapi()["components"]["schemas"][body_model_name] == {
|
||||
"properties": {
|
||||
"p_val_alias": {"title": "P Val Alias", "type": "string"},
|
||||
},
|
||||
"required": ["p_val_alias"],
|
||||
"title": body_model_name,
|
||||
"type": "object",
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
"p_val_alias", # /required-alias-and-validation-alias fails here
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p": "hello"})
|
||||
assert response.status_code == 422
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"body",
|
||||
"p_val_alias", # /required-alias-and-validation-alias fails here
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p": "hello"}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_alias": "hello"})
|
||||
assert response.status_code == 422, (
|
||||
response.text # /required-alias-and-validation-alias fails here
|
||||
)
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["body", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p_alias": "hello"}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"p_val_alias": "hello"})
|
||||
assert response.status_code == 200, (
|
||||
response.text # /required-alias-and-validation-alias fails here
|
||||
)
|
||||
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
from typing import Any, Dict
|
||||
|
||||
|
||||
def get_body_model_name(openapi: Dict[str, Any], path: str) -> str:
|
||||
body = openapi["paths"][path]["post"]["requestBody"]
|
||||
body_schema = body["content"]["application/x-www-form-urlencoded"]["schema"]
|
||||
return body_schema.get("$ref", "").split("/")[-1]
|
||||
|
|
@ -0,0 +1,502 @@
|
|||
from typing import List
|
||||
|
||||
import pytest
|
||||
from dirty_equals import AnyThing, IsDict, IsOneOf, IsPartialDict
|
||||
from fastapi import FastAPI, Header
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.get("/required-list-str")
|
||||
async def read_required_list_str(p: List[str] = Header(...)):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelRequiredListStr(BaseModel):
|
||||
p: List[str]
|
||||
|
||||
|
||||
@app.get("/model-required-list-str")
|
||||
def read_model_required_list_str(p: HeaderModelRequiredListStr = Header()):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-str", "/model-required-list-str"],
|
||||
)
|
||||
def test_required_list_str_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {
|
||||
"title": "P",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"name": "p",
|
||||
"in": "header",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-str", "/model-required-list-str"],
|
||||
)
|
||||
def test_required_list_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "p"],
|
||||
"msg": "Field required",
|
||||
"input": AnyThing,
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "p"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-str", "/model-required-list-str"],
|
||||
)
|
||||
def test_required_list_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.get("/required-list-alias")
|
||||
async def read_required_list_alias(p: List[str] = Header(..., alias="p_alias")):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelRequiredListAlias(BaseModel):
|
||||
p: List[str] = Field(..., alias="p_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-list-alias")
|
||||
async def read_model_required_list_alias(p: HeaderModelRequiredListAlias = Header()):
|
||||
return {"p": p.p} # pragma: no cover
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-alias", "/model-required-list-alias"],
|
||||
)
|
||||
def test_required_list_str_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {
|
||||
"title": "P Alias",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"name": "p_alias",
|
||||
"in": "header",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-alias", "/model-required-list-alias"],
|
||||
)
|
||||
def test_required_list_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": AnyThing,
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias",
|
||||
pytest.param(
|
||||
"/model-required-list-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 models",
|
||||
strict=False,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf( # /model-required-list-alias with PDv2 fails here
|
||||
None, IsPartialDict({"p": ["hello", "world"]})
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias",
|
||||
pytest.param(
|
||||
"/model-required-list-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")])
|
||||
assert response.status_code == 200, ( # /model-required-list-alias fails here
|
||||
response.text
|
||||
)
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.get("/required-list-validation-alias")
|
||||
def read_required_list_validation_alias(
|
||||
p: List[str] = Header(..., validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelRequiredListValidationAlias(BaseModel):
|
||||
p: List[str] = Field(..., validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-list-validation-alias")
|
||||
async def read_model_required_list_validation_alias(
|
||||
p: HeaderModelRequiredListValidationAlias = Header(),
|
||||
):
|
||||
return {"p": p.p} # pragma: no cover
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-validation-alias", "/model-required-list-validation-alias"],
|
||||
)
|
||||
def test_required_list_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {
|
||||
"title": "P Val Alias",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "header",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-list-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"header",
|
||||
"p_val_alias", # /required-list-validation-alias fails here
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": AnyThing,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-list-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
|
||||
assert response.status_code == 422 # /required-list-validation-alias fails here
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, IsPartialDict({"p": ["hello", "world"]})),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-validation-alias", "/model-required-list-validation-alias"],
|
||||
)
|
||||
def test_required_list_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(
|
||||
path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")]
|
||||
)
|
||||
assert response.status_code == 200, response.text # both fail here
|
||||
|
||||
assert response.json() == {"p": ["hello", "world"]} # pragma: no cover
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.get("/required-list-alias-and-validation-alias")
|
||||
def read_required_list_alias_and_validation_alias(
|
||||
p: List[str] = Header(..., alias="p_alias", validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelRequiredListAliasAndValidationAlias(BaseModel):
|
||||
p: List[str] = Field(..., alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-list-alias-and-validation-alias")
|
||||
def read_model_required_list_alias_and_validation_alias(
|
||||
p: HeaderModelRequiredListAliasAndValidationAlias = Header(),
|
||||
):
|
||||
return {"p": p.p} # pragma: no cover
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {
|
||||
"title": "P Val Alias",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "header",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-list-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"header",
|
||||
# /required-list-alias-and-validation-alias fails here
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": AnyThing,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"header",
|
||||
# /required-list-alias-and-validation-alias fails here
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(
|
||||
None,
|
||||
# /model-required-list-alias-and-validation-alias fails here
|
||||
IsPartialDict({"p": ["hello", "world"]}),
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")])
|
||||
assert ( # /required-list-alias-and-validation-alias fails here
|
||||
response.status_code == 422
|
||||
)
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(
|
||||
None,
|
||||
# /model-required-list-alias-and-validation-alias fails here
|
||||
IsPartialDict({"p_alias": ["hello", "world"]}),
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(
|
||||
path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")]
|
||||
)
|
||||
assert response.status_code == 200, response.text # both fail here
|
||||
assert response.json() == {"p": ["hello", "world"]} # pragma: no cover
|
||||
|
|
@ -0,0 +1,400 @@
|
|||
from typing import List, Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI, Header
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.get("/optional-list-str")
|
||||
async def read_optional_list_str(p: Optional[List[str]] = Header(None)):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelOptionalListStr(BaseModel):
|
||||
p: Optional[List[str]] = None
|
||||
|
||||
|
||||
@app.get("/model-optional-list-str")
|
||||
async def read_model_optional_list_str(p: HeaderModelOptionalListStr = Header()):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-str", "/model-optional-list-str"],
|
||||
)
|
||||
def test_optional_list_str_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
IsDict(
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P",
|
||||
},
|
||||
"name": "p",
|
||||
"in": "header",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"items": {"type": "string"}, "type": "array", "title": "P"},
|
||||
"name": "p",
|
||||
"in": "header",
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-str", "/model-optional-list-str"],
|
||||
)
|
||||
def test_optional_list_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-str", "/model-optional-list-str"],
|
||||
)
|
||||
def test_optional_list_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.get("/optional-list-alias")
|
||||
async def read_optional_list_alias(
|
||||
p: Optional[List[str]] = Header(None, alias="p_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelOptionalListAlias(BaseModel):
|
||||
p: Optional[List[str]] = Field(None, alias="p_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-list-alias")
|
||||
async def read_model_optional_list_alias(p: HeaderModelOptionalListAlias = Header()):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-alias", "/model-optional-list-alias"],
|
||||
)
|
||||
def test_optional_list_str_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
IsDict(
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Alias",
|
||||
},
|
||||
"name": "p_alias",
|
||||
"in": "header",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"items": {"type": "string"},
|
||||
"type": "array",
|
||||
"title": "P Alias",
|
||||
},
|
||||
"name": "p_alias",
|
||||
"in": "header",
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-alias", "/model-optional-list-alias"],
|
||||
)
|
||||
def test_optional_list_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-alias", "/model-optional-list-alias"],
|
||||
)
|
||||
def test_optional_list_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias",
|
||||
pytest.param(
|
||||
"/model-optional-list-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"p": ["hello", "world"] # /model-optional-list-alias fails here
|
||||
}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.get("/optional-list-validation-alias")
|
||||
def read_optional_list_validation_alias(
|
||||
p: Optional[List[str]] = Header(None, validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelOptionalListValidationAlias(BaseModel):
|
||||
p: Optional[List[str]] = Field(None, validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-list-validation-alias")
|
||||
def read_model_optional_list_validation_alias(
|
||||
p: HeaderModelOptionalListValidationAlias = Header(),
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
|
||||
)
|
||||
def test_optional_list_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "header",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
|
||||
)
|
||||
def test_optional_list_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-list-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None} # /optional-list-validation-alias fails here
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
|
||||
)
|
||||
def test_optional_list_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(
|
||||
path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")]
|
||||
)
|
||||
assert response.status_code == 200, (
|
||||
response.text # /model-optional-list-validation-alias fails here
|
||||
)
|
||||
assert response.json() == { # /optional-list-validation-alias fails here
|
||||
"p": ["hello", "world"]
|
||||
}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.get("/optional-list-alias-and-validation-alias")
|
||||
def read_optional_list_alias_and_validation_alias(
|
||||
p: Optional[List[str]] = Header(
|
||||
None, alias="p_alias", validation_alias="p_val_alias"
|
||||
),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelOptionalListAliasAndValidationAlias(BaseModel):
|
||||
p: Optional[List[str]] = Field(
|
||||
None, alias="p_alias", validation_alias="p_val_alias"
|
||||
)
|
||||
|
||||
|
||||
@app.get("/model-optional-list-alias-and-validation-alias")
|
||||
def read_model_optional_list_alias_and_validation_alias(
|
||||
p: HeaderModelOptionalListAliasAndValidationAlias = Header(),
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "header",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers=[("p", "hello"), ("p", "world")])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers=[("p_alias", "hello"), ("p_alias", "world")])
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"p": None # /optional-list-alias-and-validation-alias fails here
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(
|
||||
path, headers=[("p_val_alias", "hello"), ("p_val_alias", "world")]
|
||||
)
|
||||
assert response.status_code == 200, (
|
||||
response.text # /model-optional-list-alias-and-validation-alias fails here
|
||||
)
|
||||
assert response.json() == {
|
||||
"p": [ # /optional-list-alias-and-validation-alias fails here
|
||||
"hello",
|
||||
"world",
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,370 @@
|
|||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI, Header
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.get("/optional-str")
|
||||
async def read_optional_str(p: Optional[str] = Header(None)):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelOptionalStr(BaseModel):
|
||||
p: Optional[str] = None
|
||||
|
||||
|
||||
@app.get("/model-optional-str")
|
||||
async def read_model_optional_str(p: HeaderModelOptionalStr = Header()):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
IsDict(
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P",
|
||||
},
|
||||
"name": "p",
|
||||
"in": "header",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "P", "type": "string"},
|
||||
"name": "p",
|
||||
"in": "header",
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.get("/optional-alias")
|
||||
async def read_optional_alias(p: Optional[str] = Header(None, alias="p_alias")):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelOptionalAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, alias="p_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-alias")
|
||||
async def read_model_optional_alias(p: HeaderModelOptionalAlias = Header()):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_str_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
IsDict(
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Alias",
|
||||
},
|
||||
"name": "p_alias",
|
||||
"in": "header",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "P Alias", "type": "string"},
|
||||
"name": "p_alias",
|
||||
"in": "header",
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias",
|
||||
pytest.param(
|
||||
"/model-optional-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_optional_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p_alias": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"} # /model-optional-alias fails here
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.get("/optional-validation-alias")
|
||||
def read_optional_validation_alias(
|
||||
p: Optional[str] = Header(None, validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelOptionalValidationAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-validation-alias")
|
||||
def read_model_optional_validation_alias(
|
||||
p: HeaderModelOptionalValidationAlias = Header(),
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-validation-alias", "/model-optional-validation-alias"],
|
||||
)
|
||||
def test_optional_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "header",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-validation-alias", "/model-optional-validation-alias"],
|
||||
)
|
||||
def test_optional_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None} # /optional-validation-alias fails here
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p_val_alias": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"} # /optional-validation-alias fails here
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.get("/optional-alias-and-validation-alias")
|
||||
def read_optional_alias_and_validation_alias(
|
||||
p: Optional[str] = Header(None, alias="p_alias", validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelOptionalAliasAndValidationAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-alias-and-validation-alias")
|
||||
def read_model_optional_alias_and_validation_alias(
|
||||
p: HeaderModelOptionalAliasAndValidationAlias = Header(),
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "header",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p_alias": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"p": None # /optional-alias-and-validation-alias fails here
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p_val_alias": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"p": "hello" # /optional-alias-and-validation-alias fails here
|
||||
}
|
||||
|
|
@ -0,0 +1,491 @@
|
|||
import pytest
|
||||
from dirty_equals import AnyThing, IsDict, IsOneOf, IsPartialDict
|
||||
from fastapi import FastAPI, Header
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.get("/required-str")
|
||||
async def read_required_str(p: str = Header(...)):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelRequiredStr(BaseModel):
|
||||
p: str
|
||||
|
||||
|
||||
@app.get("/model-required-str")
|
||||
async def read_model_required_str(p: HeaderModelRequiredStr = Header()):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "P", "type": "string"},
|
||||
"name": "p",
|
||||
"in": "header",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "p"],
|
||||
"msg": "Field required",
|
||||
"input": AnyThing,
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "p"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p": "hello"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.get("/required-alias")
|
||||
async def read_required_alias(p: str = Header(..., alias="p_alias")):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelRequiredAlias(BaseModel):
|
||||
p: str = Field(..., alias="p_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-alias")
|
||||
async def read_model_required_alias(p: HeaderModelRequiredAlias = Header()):
|
||||
return {"p": p.p} # pragma: no cover
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-alias", "/model-required-alias"],
|
||||
)
|
||||
def test_required_str_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "P Alias", "type": "string"},
|
||||
"name": "p_alias",
|
||||
"in": "header",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-alias", "/model-required-alias"],
|
||||
)
|
||||
def test_required_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": AnyThing,
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias",
|
||||
pytest.param(
|
||||
"/model-required-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 models",
|
||||
strict=False,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_required_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p": "hello"})
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, IsPartialDict({"p": "hello"})),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["header", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias",
|
||||
pytest.param(
|
||||
"/model-required-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_required_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p_alias": "hello"})
|
||||
assert response.status_code == 200, ( # /model-required-alias fails here
|
||||
response.text
|
||||
)
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.get("/required-validation-alias")
|
||||
def read_required_validation_alias(
|
||||
p: str = Header(..., validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelRequiredValidationAlias(BaseModel):
|
||||
p: str = Field(..., validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-validation-alias")
|
||||
def read_model_required_validation_alias(
|
||||
p: HeaderModelRequiredValidationAlias = Header(),
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-validation-alias", "/model-required-validation-alias"],
|
||||
)
|
||||
def test_required_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "P Val Alias", "type": "string"},
|
||||
"name": "p_val_alias",
|
||||
"in": "header",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"header",
|
||||
"p_val_alias", # /required-validation-alias fails here
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": AnyThing,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p": "hello"})
|
||||
assert response.status_code == 422, ( # /required-validation-alias fails here
|
||||
response.text
|
||||
)
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, IsPartialDict({"p": "hello"})),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p_val_alias": "hello"})
|
||||
assert response.status_code == 200, ( # /required-validation-alias fails here
|
||||
response.text
|
||||
)
|
||||
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.get("/required-alias-and-validation-alias")
|
||||
def read_required_alias_and_validation_alias(
|
||||
p: str = Header(..., alias="p_alias", validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class HeaderModelRequiredAliasAndValidationAlias(BaseModel):
|
||||
p: str = Field(..., alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-alias-and-validation-alias")
|
||||
def read_model_required_alias_and_validation_alias(
|
||||
p: HeaderModelRequiredAliasAndValidationAlias = Header(),
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "P Val Alias", "type": "string"},
|
||||
"name": "p_val_alias",
|
||||
"in": "header",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"header",
|
||||
"p_val_alias", # /required-alias-and-validation-alias fails here
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": AnyThing,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p": "hello"})
|
||||
assert response.status_code == 422
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"header",
|
||||
"p_val_alias", # /required-alias-and-validation-alias fails here
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf( # /model-alias-and-validation-alias fails here
|
||||
None,
|
||||
IsPartialDict({"p": "hello"}),
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p_alias": "hello"})
|
||||
assert (
|
||||
response.status_code == 422 # /required-alias-and-validation-alias fails here
|
||||
)
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["header", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf( # /model-alias-and-validation-alias fails here
|
||||
None,
|
||||
IsPartialDict({"p_alias": "hello"}),
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path, headers={"p_val_alias": "hello"})
|
||||
assert response.status_code == 200, (
|
||||
response.text # /required-alias-and-validation-alias fails here
|
||||
)
|
||||
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
|
@ -0,0 +1 @@
|
|||
# FastAPI doesn't currently support non-scalar Path parameters
|
||||
|
|
@ -0,0 +1 @@
|
|||
# Optional Path parameters are not supported
|
||||
|
|
@ -0,0 +1 @@
|
|||
# Optional Path parameters are not supported
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import pytest
|
||||
from fastapi import FastAPI, Path
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/required-str/{p}")
|
||||
async def read_required_str(p: str = Path(...)):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
@app.get("/required-alias/{p_alias}")
|
||||
async def read_required_alias(p: str = Path(..., alias="p_alias")):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
@app.get("/required-validation-alias/{p_val_alias}")
|
||||
def read_required_validation_alias(
|
||||
p: str = Path(..., validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p} # pragma: no cover
|
||||
|
||||
|
||||
@app.get("/required-alias-and-validation-alias/{p_val_alias}")
|
||||
def read_required_alias_and_validation_alias(
|
||||
p: str = Path(..., alias="p_alias", validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p} # pragma: no cover
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("path", "expected_name", "expected_title"),
|
||||
[
|
||||
pytest.param("/required-str/{p}", "p", "P", id="required-str"),
|
||||
pytest.param(
|
||||
"/required-alias/{p_alias}", "p_alias", "P Alias", id="required-alias"
|
||||
),
|
||||
pytest.param(
|
||||
"/required-validation-alias/{p_val_alias}",
|
||||
"p_val_alias",
|
||||
"P Val Alias",
|
||||
id="required-validation-alias",
|
||||
marks=(
|
||||
needs_pydanticv2,
|
||||
pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
),
|
||||
pytest.param(
|
||||
"/required-alias-and-validation-alias/{p_val_alias}",
|
||||
"p_val_alias",
|
||||
"P Val Alias",
|
||||
id="required-alias-and-validation-alias",
|
||||
marks=(
|
||||
needs_pydanticv2,
|
||||
pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_schema(path: str, expected_name: str, expected_title: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": expected_title, "type": "string"},
|
||||
"name": expected_name,
|
||||
"in": "path",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param("/required-str", id="required-str"),
|
||||
pytest.param("/required-alias", id="required-alias"),
|
||||
pytest.param(
|
||||
"/required-validation-alias",
|
||||
id="required-validation-alias",
|
||||
marks=(
|
||||
needs_pydanticv2,
|
||||
pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
),
|
||||
pytest.param(
|
||||
"/required-alias-and-validation-alias",
|
||||
id="required-alias-and-validation-alias",
|
||||
marks=(
|
||||
needs_pydanticv2,
|
||||
pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_success(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}/hello")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
|
@ -0,0 +1,503 @@
|
|||
from typing import List
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict, IsOneOf
|
||||
from fastapi import FastAPI, Query
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.get("/required-list-str")
|
||||
async def read_required_list_str(p: List[str] = Query(...)):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelRequiredListStr(BaseModel):
|
||||
p: List[str]
|
||||
|
||||
|
||||
@app.get("/model-required-list-str")
|
||||
def read_model_required_list_str(p: QueryModelRequiredListStr = Query()):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-str", "/model-required-list-str"],
|
||||
)
|
||||
def test_required_list_str_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {
|
||||
"title": "P",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"name": "p",
|
||||
"in": "query",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-str", "/model-required-list-str"],
|
||||
)
|
||||
def test_required_list_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "p"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "p"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-str", "/model-required-list-str"],
|
||||
)
|
||||
def test_required_list_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello&p=world")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.get("/required-list-alias")
|
||||
async def read_required_list_alias(p: List[str] = Query(..., alias="p_alias")):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelRequiredListAlias(BaseModel):
|
||||
p: List[str] = Field(..., alias="p_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-list-alias")
|
||||
async def read_model_required_list_alias(p: QueryModelRequiredListAlias = Query()):
|
||||
return {"p": p.p} # pragma: no cover
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-alias", "/model-required-list-alias"],
|
||||
)
|
||||
def test_required_list_str_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {
|
||||
"title": "P Alias",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"name": "p_alias",
|
||||
"in": "query",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-alias", "/model-required-list-alias"],
|
||||
)
|
||||
def test_required_list_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias",
|
||||
pytest.param(
|
||||
"/model-required-list-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 models",
|
||||
strict=False,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello&p=world")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf( # /model-required-list-alias with PDv2 fails here
|
||||
None, {"p": ["hello", "world"]}
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias",
|
||||
pytest.param(
|
||||
"/model-required-list-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_alias=hello&p_alias=world")
|
||||
assert response.status_code == 200, ( # /model-required-list-alias fails here
|
||||
response.text
|
||||
)
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.get("/required-list-validation-alias")
|
||||
def read_required_list_validation_alias(
|
||||
p: List[str] = Query(..., validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelRequiredListValidationAlias(BaseModel):
|
||||
p: List[str] = Field(..., validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-list-validation-alias")
|
||||
async def read_model_required_list_validation_alias(
|
||||
p: QueryModelRequiredListValidationAlias = Query(),
|
||||
):
|
||||
return {"p": p.p} # pragma: no cover
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-validation-alias", "/model-required-list-validation-alias"],
|
||||
)
|
||||
def test_required_list_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {
|
||||
"title": "P Val Alias",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "query",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-list-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"query",
|
||||
"p_val_alias", # /required-list-validation-alias fails here
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-list-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello&p=world")
|
||||
assert response.status_code == 422 # /required-list-validation-alias fails here
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p": ["hello", "world"]}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-list-validation-alias", "/model-required-list-validation-alias"],
|
||||
)
|
||||
def test_required_list_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world")
|
||||
assert response.status_code == 200, response.text # both fail here
|
||||
|
||||
assert response.json() == {"p": ["hello", "world"]} # pragma: no cover
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.get("/required-list-alias-and-validation-alias")
|
||||
def read_required_list_alias_and_validation_alias(
|
||||
p: List[str] = Query(..., alias="p_alias", validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelRequiredListAliasAndValidationAlias(BaseModel):
|
||||
p: List[str] = Field(..., alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-list-alias-and-validation-alias")
|
||||
def read_model_required_list_alias_and_validation_alias(
|
||||
p: QueryModelRequiredListAliasAndValidationAlias = Query(),
|
||||
):
|
||||
return {"p": p.p} # pragma: no cover
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {
|
||||
"title": "P Val Alias",
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "query",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-list-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"query",
|
||||
# /required-list-alias-and-validation-alias fails here
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello&p=world")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"query",
|
||||
# /required-list-alias-and-validation-alias fails here
|
||||
"p_val_alias",
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(
|
||||
None,
|
||||
# /model-required-list-alias-and-validation-alias fails here
|
||||
{
|
||||
"p": [
|
||||
"hello",
|
||||
"world",
|
||||
]
|
||||
},
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_alias=hello&p_alias=world")
|
||||
assert ( # /required-list-alias-and-validation-alias fails here
|
||||
response.status_code == 422
|
||||
)
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(
|
||||
None,
|
||||
# /model-required-list-alias-and-validation-alias fails here
|
||||
{"p_alias": ["hello", "world"]},
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-list-alias-and-validation-alias",
|
||||
"/model-required-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_list_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world")
|
||||
assert response.status_code == 200, response.text # both fail here
|
||||
assert response.json() == {"p": ["hello", "world"]} # pragma: no cover
|
||||
|
|
@ -0,0 +1,396 @@
|
|||
from typing import List, Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI, Query
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.get("/optional-list-str")
|
||||
async def read_optional_list_str(p: Optional[List[str]] = Query(None)):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelOptionalListStr(BaseModel):
|
||||
p: Optional[List[str]] = None
|
||||
|
||||
|
||||
@app.get("/model-optional-list-str")
|
||||
async def read_model_optional_list_str(p: QueryModelOptionalListStr = Query()):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-str", "/model-optional-list-str"],
|
||||
)
|
||||
def test_optional_list_str_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
IsDict(
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P",
|
||||
},
|
||||
"name": "p",
|
||||
"in": "query",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"items": {"type": "string"}, "type": "array", "title": "P"},
|
||||
"name": "p",
|
||||
"in": "query",
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-str", "/model-optional-list-str"],
|
||||
)
|
||||
def test_optional_list_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-str", "/model-optional-list-str"],
|
||||
)
|
||||
def test_optional_list_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello&p=world")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": ["hello", "world"]}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.get("/optional-list-alias")
|
||||
async def read_optional_list_alias(
|
||||
p: Optional[List[str]] = Query(None, alias="p_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelOptionalListAlias(BaseModel):
|
||||
p: Optional[List[str]] = Field(None, alias="p_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-list-alias")
|
||||
async def read_model_optional_list_alias(p: QueryModelOptionalListAlias = Query()):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-alias", "/model-optional-list-alias"],
|
||||
)
|
||||
def test_optional_list_str_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
IsDict(
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Alias",
|
||||
},
|
||||
"name": "p_alias",
|
||||
"in": "query",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"items": {"type": "string"},
|
||||
"type": "array",
|
||||
"title": "P Alias",
|
||||
},
|
||||
"name": "p_alias",
|
||||
"in": "query",
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-alias", "/model-optional-list-alias"],
|
||||
)
|
||||
def test_optional_list_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-alias", "/model-optional-list-alias"],
|
||||
)
|
||||
def test_optional_list_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello&p=world")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias",
|
||||
pytest.param(
|
||||
"/model-optional-list-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_alias=hello&p_alias=world")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"p": ["hello", "world"] # /model-optional-list-alias fails here
|
||||
}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.get("/optional-list-validation-alias")
|
||||
def read_optional_list_validation_alias(
|
||||
p: Optional[List[str]] = Query(None, validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelOptionalListValidationAlias(BaseModel):
|
||||
p: Optional[List[str]] = Field(None, validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-list-validation-alias")
|
||||
def read_model_optional_list_validation_alias(
|
||||
p: QueryModelOptionalListValidationAlias = Query(),
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
|
||||
)
|
||||
def test_optional_list_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "query",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
|
||||
)
|
||||
def test_optional_list_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-list-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-list-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello&p=world")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None} # /optional-list-validation-alias fails here
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-list-validation-alias", "/model-optional-list-validation-alias"],
|
||||
)
|
||||
def test_optional_list_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world")
|
||||
assert response.status_code == 200, (
|
||||
response.text # /model-optional-list-validation-alias fails here
|
||||
)
|
||||
assert response.json() == { # /optional-list-validation-alias fails here
|
||||
"p": ["hello", "world"]
|
||||
}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.get("/optional-list-alias-and-validation-alias")
|
||||
def read_optional_list_alias_and_validation_alias(
|
||||
p: Optional[List[str]] = Query(
|
||||
None, alias="p_alias", validation_alias="p_val_alias"
|
||||
),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelOptionalListAliasAndValidationAlias(BaseModel):
|
||||
p: Optional[List[str]] = Field(
|
||||
None, alias="p_alias", validation_alias="p_val_alias"
|
||||
)
|
||||
|
||||
|
||||
@app.get("/model-optional-list-alias-and-validation-alias")
|
||||
def read_model_optional_list_alias_and_validation_alias(
|
||||
p: QueryModelOptionalListAliasAndValidationAlias = Query(),
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{"items": {"type": "string"}, "type": "array"},
|
||||
{"type": "null"},
|
||||
],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "query",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello&p=world")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_alias=hello&p_alias=world")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"p": None # /optional-list-alias-and-validation-alias fails here
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-list-alias-and-validation-alias",
|
||||
"/model-optional-list-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_list_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_val_alias=hello&p_val_alias=world")
|
||||
assert response.status_code == 200, (
|
||||
response.text # /model-optional-list-alias-and-validation-alias fails here
|
||||
)
|
||||
assert response.json() == {
|
||||
"p": [ # /optional-list-alias-and-validation-alias fails here
|
||||
"hello",
|
||||
"world",
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,370 @@
|
|||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from dirty_equals import IsDict
|
||||
from fastapi import FastAPI, Query
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.get("/optional-str")
|
||||
async def read_optional_str(p: Optional[str] = None):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelOptionalStr(BaseModel):
|
||||
p: Optional[str] = None
|
||||
|
||||
|
||||
@app.get("/model-optional-str")
|
||||
async def read_model_optional_str(p: QueryModelOptionalStr = Query()):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
IsDict(
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P",
|
||||
},
|
||||
"name": "p",
|
||||
"in": "query",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "P", "type": "string"},
|
||||
"name": "p",
|
||||
"in": "query",
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-str", "/model-optional-str"],
|
||||
)
|
||||
def test_optional_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.get("/optional-alias")
|
||||
async def read_optional_alias(p: Optional[str] = Query(None, alias="p_alias")):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelOptionalAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, alias="p_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-alias")
|
||||
async def read_model_optional_alias(p: QueryModelOptionalAlias = Query()):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_str_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
IsDict(
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Alias",
|
||||
},
|
||||
"name": "p_alias",
|
||||
"in": "query",
|
||||
}
|
||||
)
|
||||
| IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"required": False,
|
||||
"schema": {"title": "P Alias", "type": "string"},
|
||||
"name": "p_alias",
|
||||
"in": "query",
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-alias", "/model-optional-alias"],
|
||||
)
|
||||
def test_optional_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias",
|
||||
pytest.param(
|
||||
"/model-optional-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_optional_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_alias=hello")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"} # /model-optional-alias fails here
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.get("/optional-validation-alias")
|
||||
def read_optional_validation_alias(
|
||||
p: Optional[str] = Query(None, validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelOptionalValidationAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-validation-alias")
|
||||
def read_model_optional_validation_alias(
|
||||
p: QueryModelOptionalValidationAlias = Query(),
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-validation-alias", "/model-optional-validation-alias"],
|
||||
)
|
||||
def test_optional_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "query",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/optional-validation-alias", "/model-optional-validation-alias"],
|
||||
)
|
||||
def test_optional_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_val_alias=hello")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"} # /optional-validation-alias fails here
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.get("/optional-alias-and-validation-alias")
|
||||
def read_optional_alias_and_validation_alias(
|
||||
p: Optional[str] = Query(None, alias="p_alias", validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelOptionalAliasAndValidationAlias(BaseModel):
|
||||
p: Optional[str] = Field(None, alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-optional-alias-and-validation-alias")
|
||||
def read_model_optional_alias_and_validation_alias(
|
||||
p: QueryModelOptionalAliasAndValidationAlias = Query(),
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": False,
|
||||
"schema": {
|
||||
"anyOf": [{"type": "string"}, {"type": "null"}],
|
||||
"title": "P Val Alias",
|
||||
},
|
||||
"name": "p_val_alias",
|
||||
"in": "query",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/optional-alias-and-validation-alias",
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": None}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_alias=hello")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"p": None # /optional-alias-and-validation-alias fails here
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/optional-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-optional-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_optional_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_val_alias=hello")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"p": "hello" # /optional-alias-and-validation-alias fails here
|
||||
}
|
||||
|
|
@ -0,0 +1,494 @@
|
|||
import pytest
|
||||
from dirty_equals import IsDict, IsOneOf
|
||||
from fastapi import FastAPI, Query
|
||||
from fastapi._compat import PYDANTIC_V2
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from tests.utils import needs_pydanticv2
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# =====================================================================================
|
||||
# Without aliases
|
||||
|
||||
|
||||
@app.get("/required-str")
|
||||
async def read_required_str(p: str):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelRequiredStr(BaseModel):
|
||||
p: str
|
||||
|
||||
|
||||
@app.get("/model-required-str")
|
||||
async def read_model_required_str(p: QueryModelRequiredStr = Query()):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "P", "type": "string"},
|
||||
"name": "p",
|
||||
"in": "query",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "p"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "p"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-str", "/model-required-str"],
|
||||
)
|
||||
def test_required_str(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias
|
||||
|
||||
|
||||
@app.get("/required-alias")
|
||||
async def read_required_alias(p: str = Query(..., alias="p_alias")):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelRequiredAlias(BaseModel):
|
||||
p: str = Field(..., alias="p_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-alias")
|
||||
async def read_model_required_alias(p: QueryModelRequiredAlias = Query()):
|
||||
return {"p": p.p} # pragma: no cover
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-alias", "/model-required-alias"],
|
||||
)
|
||||
def test_required_str_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "P Alias", "type": "string"},
|
||||
"name": "p_alias",
|
||||
"in": "query",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-alias", "/model-required-alias"],
|
||||
)
|
||||
def test_required_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias",
|
||||
pytest.param(
|
||||
"/model-required-alias",
|
||||
marks=pytest.mark.xfail(
|
||||
raises=AssertionError,
|
||||
condition=PYDANTIC_V2,
|
||||
reason="Fails only with PDv2 models",
|
||||
strict=False,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_required_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello")
|
||||
assert response.status_code == 422
|
||||
assert response.json() == IsDict(
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "p_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(
|
||||
None,
|
||||
{"p": "hello"}, # /model-required-alias PDv2 fails here
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
) | IsDict(
|
||||
# TODO: remove when deprecating Pydantic v1
|
||||
{
|
||||
"detail": [
|
||||
{
|
||||
"loc": ["query", "p_alias"],
|
||||
"msg": "field required",
|
||||
"type": "value_error.missing",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias",
|
||||
pytest.param(
|
||||
"/model-required-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_required_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_alias=hello")
|
||||
assert response.status_code == 200, ( # /model-required-alias fails here
|
||||
response.text
|
||||
)
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Validation alias
|
||||
|
||||
|
||||
@app.get("/required-validation-alias")
|
||||
def read_required_validation_alias(
|
||||
p: str = Query(..., validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelRequiredValidationAlias(BaseModel):
|
||||
p: str = Field(..., validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-validation-alias")
|
||||
def read_model_required_validation_alias(
|
||||
p: QueryModelRequiredValidationAlias = Query(),
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
["/required-validation-alias", "/model-required-validation-alias"],
|
||||
)
|
||||
def test_required_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "P Val Alias", "type": "string"},
|
||||
"name": "p_val_alias",
|
||||
"in": "query",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"query",
|
||||
"p_val_alias", # /required-validation-alias fails here
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello")
|
||||
assert response.status_code == 422, ( # /required-validation-alias fails here
|
||||
response.text
|
||||
)
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {"p": "hello"}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_val_alias=hello")
|
||||
assert response.status_code == 200, ( # /required-validation-alias fails here
|
||||
response.text
|
||||
)
|
||||
|
||||
assert response.json() == {"p": "hello"}
|
||||
|
||||
|
||||
# =====================================================================================
|
||||
# Alias and validation alias
|
||||
|
||||
|
||||
@app.get("/required-alias-and-validation-alias")
|
||||
def read_required_alias_and_validation_alias(
|
||||
p: str = Query(..., alias="p_alias", validation_alias="p_val_alias"),
|
||||
):
|
||||
return {"p": p}
|
||||
|
||||
|
||||
class QueryModelRequiredAliasAndValidationAlias(BaseModel):
|
||||
p: str = Field(..., alias="p_alias", validation_alias="p_val_alias")
|
||||
|
||||
|
||||
@app.get("/model-required-alias-and-validation-alias")
|
||||
def read_model_required_alias_and_validation_alias(
|
||||
p: QueryModelRequiredAliasAndValidationAlias = Query(),
|
||||
):
|
||||
return {"p": p.p}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_schema(path: str):
|
||||
assert app.openapi()["paths"][path]["get"]["parameters"] == [
|
||||
{
|
||||
"required": True,
|
||||
"schema": {"title": "P Val Alias", "type": "string"},
|
||||
"name": "p_val_alias",
|
||||
"in": "query",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_missing(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(path)
|
||||
assert response.status_code == 422
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"query",
|
||||
"p_val_alias", # /required-alias-and-validation-alias fails here
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf(None, {}),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_name(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p=hello")
|
||||
assert response.status_code == 422
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": [
|
||||
"query",
|
||||
"p_val_alias", # /required-alias-and-validation-alias fails here
|
||||
],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf( # /model-alias-and-validation-alias fails here
|
||||
None,
|
||||
{"p": "hello"},
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.xfail(raises=AssertionError, strict=False)
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/required-alias-and-validation-alias",
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_alias=hello")
|
||||
assert (
|
||||
response.status_code == 422 # /required-alias-and-validation-alias fails here
|
||||
)
|
||||
|
||||
assert response.json() == {
|
||||
"detail": [
|
||||
{
|
||||
"type": "missing",
|
||||
"loc": ["query", "p_val_alias"],
|
||||
"msg": "Field required",
|
||||
"input": IsOneOf( # /model-alias-and-validation-alias fails here
|
||||
None,
|
||||
{"p_alias": "hello"},
|
||||
),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@needs_pydanticv2
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
pytest.param(
|
||||
"/required-alias-and-validation-alias",
|
||||
marks=pytest.mark.xfail(raises=AssertionError, strict=False),
|
||||
),
|
||||
"/model-required-alias-and-validation-alias",
|
||||
],
|
||||
)
|
||||
def test_required_alias_and_validation_alias_by_validation_alias(path: str):
|
||||
client = TestClient(app)
|
||||
response = client.get(f"{path}?p_val_alias=hello")
|
||||
assert response.status_code == 200, (
|
||||
response.text # /required-alias-and-validation-alias fails here
|
||||
)
|
||||
|
||||
assert response.json() == {"p": "hello"}
|
||||
Loading…
Reference in New Issue