committed by
GitHub
39 changed files with 10747 additions and 0 deletions
@ -0,0 +1,523 @@ |
|||
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 typing_extensions import Annotated |
|||
|
|||
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: Annotated[List[str], Body(embed=True)]): |
|||
return {"p": p} |
|||
|
|||
|
|||
class BodyModelRequiredListStr(BaseModel): |
|||
p: List[str] |
|||
|
|||
|
|||
@app.post("/model-required-list-str", operation_id="model_required_list_str") |
|||
def read_model_required_list_str(p: BodyModelRequiredListStr): |
|||
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: Annotated[List[str], Body(embed=True, alias="p_alias")], |
|||
): |
|||
return {"p": p} |
|||
|
|||
|
|||
class BodyModelRequiredListAlias(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: BodyModelRequiredListAlias): |
|||
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: Annotated[List[str], Body(embed=True, validation_alias="p_val_alias")], |
|||
): |
|||
return {"p": p} |
|||
|
|||
|
|||
class BodyModelRequiredListValidationAlias(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: BodyModelRequiredListValidationAlias, |
|||
): |
|||
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: Annotated[ |
|||
List[str], Body(embed=True, alias="p_alias", validation_alias="p_val_alias") |
|||
], |
|||
): |
|||
return {"p": p} |
|||
|
|||
|
|||
class BodyModelRequiredListAliasAndValidationAlias(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: BodyModelRequiredListAliasAndValidationAlias, |
|||
): |
|||
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,600 @@ |
|||
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 typing_extensions import Annotated |
|||
|
|||
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: Annotated[Optional[List[str]], Body(embed=True)] = None, |
|||
): |
|||
return {"p": p} |
|||
|
|||
|
|||
class BodyModelOptionalListStr(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: BodyModelOptionalListStr): |
|||
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: Annotated[Optional[List[str]], Body(embed=True, alias="p_alias")] = None, |
|||
): |
|||
return {"p": p} |
|||
|
|||
|
|||
class BodyModelOptionalListAlias(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: BodyModelOptionalListAlias): |
|||
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: Annotated[ |
|||
Optional[List[str]], Body(embed=True, validation_alias="p_val_alias") |
|||
] = None, |
|||
): |
|||
return {"p": p} |
|||
|
|||
|
|||
class BodyModelOptionalListValidationAlias(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: BodyModelOptionalListValidationAlias, |
|||
): |
|||
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: Annotated[ |
|||
Optional[List[str]], |
|||
Body(embed=True, alias="p_alias", validation_alias="p_val_alias"), |
|||
] = None, |
|||
): |
|||
return {"p": p} |
|||
|
|||
|
|||
class BodyModelOptionalListAliasAndValidationAlias(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: BodyModelOptionalListAliasAndValidationAlias, |
|||
): |
|||
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,569 @@ |
|||
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 typing_extensions import Annotated |
|||
|
|||
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: Annotated[Optional[str], Body(embed=True)] = None): |
|||
return {"p": p} |
|||
|
|||
|
|||
class BodyModelOptionalStr(BaseModel): |
|||
p: Optional[str] = None |
|||
|
|||
|
|||
@app.post("/model-optional-str", operation_id="model_optional_str") |
|||
async def read_model_optional_str(p: BodyModelOptionalStr): |
|||
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: Annotated[Optional[str], Body(embed=True, alias="p_alias")] = None, |
|||
): |
|||
return {"p": p} |
|||
|
|||
|
|||
class BodyModelOptionalAlias(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: BodyModelOptionalAlias): |
|||
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: Annotated[ |
|||
Optional[str], Body(embed=True, validation_alias="p_val_alias") |
|||
] = None, |
|||
): |
|||
return {"p": p} |
|||
|
|||
|
|||
class BodyModelOptionalValidationAlias(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: BodyModelOptionalValidationAlias, |
|||
): |
|||
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: Annotated[ |
|||
Optional[str], Body(embed=True, alias="p_alias", validation_alias="p_val_alias") |
|||
] = None, |
|||
): |
|||
return {"p": p} |
|||
|
|||
|
|||
class BodyModelOptionalAliasAndValidationAlias(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: BodyModelOptionalAliasAndValidationAlias, |
|||
): |
|||
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,514 @@ |
|||
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 typing_extensions import Annotated |
|||
|
|||
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: Annotated[str, Body(embed=True)]): |
|||
return {"p": p} |
|||
|
|||
|
|||
class BodyModelRequiredStr(BaseModel): |
|||
p: str |
|||
|
|||
|
|||
@app.post("/model-required-str", operation_id="model_required_str") |
|||
async def read_model_required_str(p: BodyModelRequiredStr): |
|||
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: Annotated[str, Body(embed=True, alias="p_alias")], |
|||
): |
|||
return {"p": p} |
|||
|
|||
|
|||
class BodyModelRequiredAlias(BaseModel): |
|||
p: str = Field(alias="p_alias") |
|||
|
|||
|
|||
@app.post("/model-required-alias", operation_id="model_required_alias") |
|||
async def read_model_required_alias(p: BodyModelRequiredAlias): |
|||
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: Annotated[str, Body(embed=True, validation_alias="p_val_alias")], |
|||
): |
|||
return {"p": p} |
|||
|
|||
|
|||
class BodyModelRequiredValidationAlias(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: BodyModelRequiredValidationAlias, |
|||
): |
|||
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: Annotated[ |
|||
str, Body(embed=True, alias="p_alias", validation_alias="p_val_alias") |
|||
], |
|||
): |
|||
return {"p": p} |
|||
|
|||
|
|||
class BodyModelRequiredAliasAndValidationAlias(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: BodyModelRequiredAliasAndValidationAlias, |
|||
): |
|||
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": 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, 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,383 @@ |
|||
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 typing_extensions import Annotated |
|||
|
|||
from tests.utils import needs_pydanticv2 |
|||
|
|||
app = FastAPI() |
|||
|
|||
# ===================================================================================== |
|||
# Without aliases |
|||
|
|||
|
|||
@app.get("/optional-str") |
|||
async def read_optional_str(p: Annotated[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: Annotated[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: Annotated[Optional[str], Cookie(alias="p_alias")] = None, |
|||
): |
|||
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: Annotated[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: Annotated[Optional[str], Cookie(validation_alias="p_val_alias")] = None, |
|||
): |
|||
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: Annotated[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: Annotated[ |
|||
Optional[str], Cookie(alias="p_alias", validation_alias="p_val_alias") |
|||
] = None, |
|||
): |
|||
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: Annotated[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,503 @@ |
|||
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 typing_extensions import Annotated |
|||
|
|||
from tests.utils import needs_pydanticv2 |
|||
|
|||
app = FastAPI() |
|||
|
|||
# ===================================================================================== |
|||
# Without aliases |
|||
|
|||
|
|||
@app.get("/required-str") |
|||
async def read_required_str(p: Annotated[str, Cookie()]): |
|||
return {"p": p} |
|||
|
|||
|
|||
class CookieModelRequiredStr(BaseModel): |
|||
p: str |
|||
|
|||
|
|||
@app.get("/model-required-str") |
|||
async def read_model_required_str(p: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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,597 @@ |
|||
from typing import List |
|||
|
|||
import pytest |
|||
from dirty_equals import IsDict |
|||
from fastapi import FastAPI, File, UploadFile |
|||
from fastapi._compat import PYDANTIC_V2 |
|||
from fastapi.testclient import TestClient |
|||
from typing_extensions import Annotated |
|||
|
|||
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: Annotated[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: Annotated[List[UploadFile], File()]): |
|||
return {"file_size": [file.size for file in p]} |
|||
|
|||
|
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
"/list-bytes", |
|||
"/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", |
|||
"/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": 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", |
|||
"/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: Annotated[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: Annotated[List[UploadFile], File(alias="p_alias")], |
|||
): |
|||
return {"file_size": [file.size for file in p]} |
|||
|
|||
|
|||
@pytest.mark.xfail( |
|||
raises=AssertionError, |
|||
condition=PYDANTIC_V2, |
|||
reason="Fails only with PDv2", |
|||
strict=False, |
|||
) |
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
"/list-bytes-alias", |
|||
"/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", |
|||
"/list-uploadfile-alias", |
|||
], |
|||
) |
|||
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"], |
|||
"msg": "Field required", |
|||
"input": 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", |
|||
"/list-uploadfile-alias", |
|||
], |
|||
) |
|||
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 |
|||
assert response.json() == IsDict( |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "p_alias"], |
|||
"msg": "Field required", |
|||
"input": 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", |
|||
"/list-uploadfile-alias", |
|||
], |
|||
) |
|||
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, 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: Annotated[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: Annotated[List[UploadFile], File(validation_alias="p_val_alias")], |
|||
): |
|||
return {"file_size": [file.size for file in p]} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
"/list-bytes-validation-alias", |
|||
"/list-uploadfile-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), |
|||
), |
|||
pytest.param( |
|||
"/list-uploadfile-validation-alias", |
|||
marks=pytest.mark.xfail(raises=AssertionError, strict=False), |
|||
), |
|||
], |
|||
) |
|||
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": None, |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
pytest.param( |
|||
"/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), |
|||
), |
|||
], |
|||
) |
|||
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() == { # pragma: no cover |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "p_val_alias"], |
|||
"msg": "Field required", |
|||
"input": None, |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
@pytest.mark.xfail(raises=AssertionError, strict=False) |
|||
@needs_pydanticv2 |
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
"/list-bytes-validation-alias", |
|||
"/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 2 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: Annotated[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: Annotated[ |
|||
List[UploadFile], File(alias="p_alias", validation_alias="p_val_alias") |
|||
], |
|||
): |
|||
return {"file_size": [file.size for file in p]} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
"/list-bytes-alias-and-validation-alias", |
|||
"/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), |
|||
), |
|||
pytest.param( |
|||
"/list-uploadfile-alias-and-validation-alias", |
|||
marks=pytest.mark.xfail(raises=AssertionError, strict=False), |
|||
), |
|||
], |
|||
) |
|||
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": None, |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
@pytest.mark.xfail(raises=AssertionError, strict=False) |
|||
@needs_pydanticv2 |
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
"/list-bytes-alias-and-validation-alias", |
|||
"/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": None, |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
pytest.param( |
|||
"/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), |
|||
), |
|||
], |
|||
) |
|||
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() == { # pragma: no cover |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "p_val_alias"], |
|||
"msg": "Field required", |
|||
"input": None, |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
@pytest.mark.xfail(raises=AssertionError, strict=False) |
|||
@needs_pydanticv2 |
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
"/list-bytes-alias-and-validation-alias", |
|||
"/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 2 fail here |
|||
response.text |
|||
) |
|||
assert response.json() == {"file_size": [5, 5]} # pragma: no cover |
|||
@ -0,0 +1,443 @@ |
|||
from typing import Optional |
|||
|
|||
import pytest |
|||
from dirty_equals import IsDict |
|||
from fastapi import FastAPI, File, UploadFile |
|||
from fastapi._compat import PYDANTIC_V2 |
|||
from fastapi.testclient import TestClient |
|||
from typing_extensions import Annotated |
|||
|
|||
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: Annotated[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: Annotated[Optional[UploadFile], File()] = None): |
|||
return {"file_size": p.size if p else None} |
|||
|
|||
|
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
"/optional-bytes", |
|||
"/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", |
|||
"/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", |
|||
"/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: Annotated[Optional[bytes], File(alias="p_alias")] = None, |
|||
): |
|||
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: Annotated[Optional[UploadFile], File(alias="p_alias")] = None, |
|||
): |
|||
return {"file_size": p.size if 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", |
|||
"/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", |
|||
"/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", |
|||
"/optional-uploadfile-alias", |
|||
], |
|||
) |
|||
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} |
|||
|
|||
|
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
"/optional-bytes-alias", |
|||
"/optional-uploadfile-alias", |
|||
], |
|||
) |
|||
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} |
|||
|
|||
|
|||
# ===================================================================================== |
|||
# Validation alias |
|||
|
|||
|
|||
@app.post( |
|||
"/optional-bytes-validation-alias", operation_id="optional_bytes_validation_alias" |
|||
) |
|||
def read_optional_bytes_validation_alias( |
|||
p: Annotated[Optional[bytes], File(validation_alias="p_val_alias")] = None, |
|||
): |
|||
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: Annotated[Optional[UploadFile], File(validation_alias="p_val_alias")] = None, |
|||
): |
|||
return {"file_size": p.size if p else None} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
"/optional-bytes-validation-alias", |
|||
"/optional-uploadfile-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", |
|||
"/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), |
|||
), |
|||
pytest.param( |
|||
"/optional-uploadfile-validation-alias", |
|||
marks=pytest.mark.xfail(raises=AssertionError, strict=False), |
|||
), |
|||
], |
|||
) |
|||
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( |
|||
"/optional-uploadfile-validation-alias", |
|||
marks=pytest.mark.xfail(raises=AssertionError, strict=False), |
|||
), |
|||
], |
|||
) |
|||
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 |
|||
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: Annotated[ |
|||
Optional[bytes], File(alias="p_alias", validation_alias="p_val_alias") |
|||
] = None, |
|||
): |
|||
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: Annotated[ |
|||
Optional[UploadFile], File(alias="p_alias", validation_alias="p_val_alias") |
|||
] = None, |
|||
): |
|||
return {"file_size": p.size if p else None} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
"/optional-bytes-alias-and-validation-alias", |
|||
"/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", |
|||
"/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", |
|||
"/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), |
|||
), |
|||
pytest.param( |
|||
"/optional-uploadfile-alias-and-validation-alias", |
|||
marks=pytest.mark.xfail(raises=AssertionError, strict=False), |
|||
), |
|||
], |
|||
) |
|||
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} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
pytest.param( |
|||
"/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), |
|||
), |
|||
], |
|||
) |
|||
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 |
|||
assert response.json() == { |
|||
"file_size": 5 |
|||
} # /optional-*-alias-and-validation-alias fail here |
|||
@ -0,0 +1,487 @@ |
|||
from typing import List, Optional |
|||
|
|||
import pytest |
|||
from dirty_equals import IsDict |
|||
from fastapi import FastAPI, File, UploadFile |
|||
from fastapi._compat import PYDANTIC_V2 |
|||
from fastapi.testclient import TestClient |
|||
from typing_extensions import Annotated |
|||
|
|||
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: Annotated[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: Annotated[Optional[List[UploadFile]], File()] = None, |
|||
): |
|||
return {"file_size": [file.size for file in p] if p else None} |
|||
|
|||
|
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
"/optional-list-bytes", |
|||
"/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", |
|||
"/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, |
|||
), |
|||
), |
|||
"/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: Annotated[Optional[List[bytes]], File(alias="p_alias")] = None, |
|||
): |
|||
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: Annotated[Optional[List[UploadFile]], File(alias="p_alias")] = None, |
|||
): |
|||
return {"file_size": [file.size for file in p] if 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", |
|||
"/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", |
|||
"/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", |
|||
"/optional-list-uploadfile-alias", |
|||
], |
|||
) |
|||
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 |
|||
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", |
|||
), |
|||
), |
|||
"/optional-list-uploadfile-alias", |
|||
], |
|||
) |
|||
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]} |
|||
|
|||
|
|||
# ===================================================================================== |
|||
# Validation alias |
|||
|
|||
|
|||
@app.post("/optional-list-bytes-validation-alias") |
|||
def read_optional_list_bytes_validation_alias( |
|||
p: Annotated[Optional[List[bytes]], File(validation_alias="p_val_alias")] = None, |
|||
): |
|||
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: Annotated[ |
|||
Optional[List[UploadFile]], File(validation_alias="p_val_alias") |
|||
] = None, |
|||
): |
|||
return {"file_size": [file.size for file in p] if p else None} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
"/optional-list-bytes-validation-alias", |
|||
"/optional-list-uploadfile-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", |
|||
"/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( |
|||
"/optional-list-uploadfile-validation-alias", |
|||
marks=pytest.mark.xfail(raises=AssertionError, strict=False), |
|||
), |
|||
], |
|||
) |
|||
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", |
|||
"/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 |
|||
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: Annotated[ |
|||
Optional[List[bytes]], File(alias="p_alias", validation_alias="p_val_alias") |
|||
] = None, |
|||
): |
|||
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: Annotated[ |
|||
Optional[List[UploadFile]], |
|||
File(alias="p_alias", validation_alias="p_val_alias"), |
|||
] = None, |
|||
): |
|||
return {"file_size": [file.size for file in p] if p else None} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
"/optional-list-bytes-alias-and-validation-alias", |
|||
"/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", |
|||
"/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", |
|||
"/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( |
|||
"/optional-list-uploadfile-alias-and-validation-alias", |
|||
marks=pytest.mark.xfail(raises=AssertionError, strict=False), |
|||
), |
|||
], |
|||
) |
|||
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", |
|||
"/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 |
|||
assert response.json() == { |
|||
"file_size": [5, 5] # /optional-list-*-alias-and-validation-alias fail here |
|||
} |
|||
@ -0,0 +1,536 @@ |
|||
import pytest |
|||
from dirty_equals import IsDict |
|||
from fastapi import FastAPI, File, UploadFile |
|||
from fastapi._compat import PYDANTIC_V2 |
|||
from fastapi.testclient import TestClient |
|||
from typing_extensions import Annotated |
|||
|
|||
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: Annotated[bytes, File()]): |
|||
return {"file_size": len(p)} |
|||
|
|||
|
|||
@app.post("/required-uploadfile", operation_id="required_uploadfile") |
|||
async def read_required_uploadfile(p: Annotated[UploadFile, File()]): |
|||
return {"file_size": p.size} |
|||
|
|||
|
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
"/required-bytes", |
|||
"/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", |
|||
"/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": 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", |
|||
"/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: Annotated[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: Annotated[UploadFile, File(alias="p_alias")], |
|||
): |
|||
return {"file_size": p.size} |
|||
|
|||
|
|||
@pytest.mark.xfail( |
|||
raises=AssertionError, |
|||
condition=PYDANTIC_V2, |
|||
reason="Fails only with PDv2", |
|||
strict=False, |
|||
) |
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
"/required-bytes-alias", |
|||
"/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", |
|||
"/required-uploadfile-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": 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", |
|||
"/required-uploadfile-alias", |
|||
], |
|||
) |
|||
def test_required_alias_by_name(path: str): |
|||
client = TestClient(app) |
|||
response = client.post(path, files=[("p", b"hello")]) |
|||
assert response.status_code == 422 |
|||
assert response.json() == IsDict( |
|||
{ |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "p_alias"], |
|||
"msg": "Field required", |
|||
"input": 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", |
|||
"/required-uploadfile-alias", |
|||
], |
|||
) |
|||
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, 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: Annotated[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: Annotated[UploadFile, File(validation_alias="p_val_alias")], |
|||
): |
|||
return {"file_size": p.size} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
"/required-bytes-validation-alias", |
|||
"/required-uploadfile-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), |
|||
), |
|||
pytest.param( |
|||
"/required-uploadfile-validation-alias", |
|||
marks=pytest.mark.xfail(raises=AssertionError, strict=False), |
|||
), |
|||
], |
|||
) |
|||
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": None, |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
pytest.param( |
|||
"/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), |
|||
), |
|||
], |
|||
) |
|||
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() == { # pragma: no cover |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "p_val_alias"], |
|||
"msg": "Field required", |
|||
"input": None, |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
pytest.param( |
|||
"/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), |
|||
), |
|||
], |
|||
) |
|||
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 2 fail here |
|||
response.text |
|||
) |
|||
assert response.json() == {"file_size": 5} # pragma: no cover |
|||
|
|||
|
|||
# ===================================================================================== |
|||
# 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: Annotated[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: Annotated[UploadFile, File(alias="p_alias", validation_alias="p_val_alias")], |
|||
): |
|||
return {"file_size": p.size} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
"/required-bytes-alias-and-validation-alias", |
|||
"/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), |
|||
), |
|||
pytest.param( |
|||
"/required-uploadfile-alias-and-validation-alias", |
|||
marks=pytest.mark.xfail(raises=AssertionError, strict=False), |
|||
), |
|||
], |
|||
) |
|||
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": None, |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
pytest.param( |
|||
"/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), |
|||
), |
|||
], |
|||
) |
|||
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": None, |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
pytest.param( |
|||
"/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), |
|||
), |
|||
], |
|||
) |
|||
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() == { # pragma: no cover |
|||
"detail": [ |
|||
{ |
|||
"type": "missing", |
|||
"loc": ["body", "p_val_alias"], |
|||
"msg": "Field required", |
|||
"input": None, |
|||
} |
|||
] |
|||
} |
|||
|
|||
|
|||
@needs_pydanticv2 |
|||
@pytest.mark.parametrize( |
|||
"path", |
|||
[ |
|||
pytest.param( |
|||
"/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), |
|||
), |
|||
], |
|||
) |
|||
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 2 fail here |
|||
response.text |
|||
) |
|||
assert response.json() == {"file_size": 5} # pragma: no cover |
|||
@ -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,527 @@ |
|||
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 typing_extensions import Annotated |
|||
|
|||
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: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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,454 @@ |
|||
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 typing_extensions import Annotated |
|||
|
|||
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: Annotated[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: Annotated[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: Annotated[Optional[List[str]], Form(alias="p_alias")] = None, |
|||
): |
|||
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: Annotated[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: Annotated[Optional[List[str]], Form(validation_alias="p_val_alias")] = None, |
|||
): |
|||
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: Annotated[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: Annotated[ |
|||
Optional[List[str]], Form(alias="p_alias", validation_alias="p_val_alias") |
|||
] = None, |
|||
): |
|||
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: Annotated[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,419 @@ |
|||
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 typing_extensions import Annotated |
|||
|
|||
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: Annotated[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: Annotated[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: Annotated[Optional[str], Form(alias="p_alias")] = None, |
|||
): |
|||
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: Annotated[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: Annotated[Optional[str], Form(validation_alias="p_val_alias")] = None, |
|||
): |
|||
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: Annotated[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: Annotated[ |
|||
Optional[str], Form(alias="p_alias", validation_alias="p_val_alias") |
|||
] = None, |
|||
): |
|||
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: Annotated[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,502 @@ |
|||
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 typing_extensions import Annotated |
|||
|
|||
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: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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,505 @@ |
|||
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 typing_extensions import Annotated |
|||
|
|||
from tests.utils import needs_pydanticv2 |
|||
|
|||
app = FastAPI() |
|||
|
|||
# ===================================================================================== |
|||
# Without aliases |
|||
|
|||
|
|||
@app.get("/required-list-str") |
|||
async def read_required_list_str(p: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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,407 @@ |
|||
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 typing_extensions import Annotated |
|||
|
|||
from tests.utils import needs_pydanticv2 |
|||
|
|||
app = FastAPI() |
|||
|
|||
# ===================================================================================== |
|||
# Without aliases |
|||
|
|||
|
|||
@app.get("/optional-list-str") |
|||
async def read_optional_list_str( |
|||
p: Annotated[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: Annotated[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: Annotated[Optional[List[str]], Header(alias="p_alias")] = None, |
|||
): |
|||
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: Annotated[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: Annotated[Optional[List[str]], Header(validation_alias="p_val_alias")] = None, |
|||
): |
|||
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: Annotated[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: Annotated[ |
|||
Optional[List[str]], Header(alias="p_alias", validation_alias="p_val_alias") |
|||
] = None, |
|||
): |
|||
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: Annotated[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,375 @@ |
|||
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 typing_extensions import Annotated |
|||
|
|||
from tests.utils import needs_pydanticv2 |
|||
|
|||
app = FastAPI() |
|||
|
|||
# ===================================================================================== |
|||
# Without aliases |
|||
|
|||
|
|||
@app.get("/optional-str") |
|||
async def read_optional_str(p: Annotated[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: Annotated[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: Annotated[Optional[str], Header(alias="p_alias")] = None, |
|||
): |
|||
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: Annotated[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: Annotated[Optional[str], Header(validation_alias="p_val_alias")] = None, |
|||
): |
|||
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: Annotated[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: Annotated[ |
|||
Optional[str], Header(alias="p_alias", validation_alias="p_val_alias") |
|||
] = None, |
|||
): |
|||
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: Annotated[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,492 @@ |
|||
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 typing_extensions import Annotated |
|||
|
|||
from tests.utils import needs_pydanticv2 |
|||
|
|||
app = FastAPI() |
|||
|
|||
# ===================================================================================== |
|||
# Without aliases |
|||
|
|||
|
|||
@app.get("/required-str") |
|||
async def read_required_str(p: Annotated[str, Header()]): |
|||
return {"p": p} |
|||
|
|||
|
|||
class HeaderModelRequiredStr(BaseModel): |
|||
p: str |
|||
|
|||
|
|||
@app.get("/model-required-str") |
|||
async def read_model_required_str(p: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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,102 @@ |
|||
import pytest |
|||
from fastapi import FastAPI, Path |
|||
from fastapi.testclient import TestClient |
|||
from typing_extensions import Annotated |
|||
|
|||
from tests.utils import needs_pydanticv2 |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
@app.get("/required-str/{p}") |
|||
async def read_required_str(p: Annotated[str, Path()]): |
|||
return {"p": p} |
|||
|
|||
|
|||
@app.get("/required-alias/{p_alias}") |
|||
async def read_required_alias(p: Annotated[str, Path(alias="p_alias")]): |
|||
return {"p": p} |
|||
|
|||
|
|||
@app.get("/required-validation-alias/{p_val_alias}") |
|||
def read_required_validation_alias( |
|||
p: Annotated[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: Annotated[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,506 @@ |
|||
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 typing_extensions import Annotated |
|||
|
|||
from tests.utils import needs_pydanticv2 |
|||
|
|||
app = FastAPI() |
|||
|
|||
# ===================================================================================== |
|||
# Without aliases |
|||
|
|||
|
|||
@app.get("/required-list-str") |
|||
async def read_required_list_str(p: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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,403 @@ |
|||
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 typing_extensions import Annotated |
|||
|
|||
from tests.utils import needs_pydanticv2 |
|||
|
|||
app = FastAPI() |
|||
|
|||
# ===================================================================================== |
|||
# Without aliases |
|||
|
|||
|
|||
@app.get("/optional-list-str") |
|||
async def read_optional_list_str( |
|||
p: Annotated[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: Annotated[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: Annotated[Optional[List[str]], Query(alias="p_alias")] = None, |
|||
): |
|||
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: Annotated[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: Annotated[Optional[List[str]], Query(validation_alias="p_val_alias")] = None, |
|||
): |
|||
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: Annotated[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: Annotated[ |
|||
Optional[List[str]], Query(alias="p_alias", validation_alias="p_val_alias") |
|||
] = None, |
|||
): |
|||
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: Annotated[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,375 @@ |
|||
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 typing_extensions import Annotated |
|||
|
|||
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: Annotated[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: Annotated[Optional[str], Query(alias="p_alias")] = None, |
|||
): |
|||
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: Annotated[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: Annotated[Optional[str], Query(validation_alias="p_val_alias")] = None, |
|||
): |
|||
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: Annotated[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: Annotated[ |
|||
Optional[str], Query(alias="p_alias", validation_alias="p_val_alias") |
|||
] = None, |
|||
): |
|||
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: Annotated[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,495 @@ |
|||
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 typing_extensions import Annotated |
|||
|
|||
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: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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: Annotated[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