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