mirror of https://github.com/tiangolo/fastapi.git
add failing tests for empty input values to get a CI run baseline for them
This commit is contained in:
parent
15eb6782dc
commit
49f6b8397d
|
|
@ -49,6 +49,7 @@ class StandardModel(Parent):
|
|||
default_false: bool = False
|
||||
default_none: Optional[bool] = None
|
||||
default_zero: int = 0
|
||||
default_str: str = "foo"
|
||||
true_if_unset: Optional[bool] = None
|
||||
|
||||
|
||||
|
|
@ -57,6 +58,7 @@ class FieldModel(Parent):
|
|||
default_false: bool = Field(default=False)
|
||||
default_none: Optional[bool] = Field(default=None)
|
||||
default_zero: int = Field(default=0)
|
||||
default_str: str = Field(default="foo")
|
||||
true_if_unset: Optional[bool] = Field(default=None)
|
||||
|
||||
|
||||
|
|
@ -67,6 +69,7 @@ if PYDANTIC_V2:
|
|||
default_false: Annotated[bool, Field(default=False)]
|
||||
default_none: Annotated[Optional[bool], Field(default=None)]
|
||||
default_zero: Annotated[int, Field(default=0)]
|
||||
default_str: Annotated[str, Field(default="foo")]
|
||||
true_if_unset: Annotated[Optional[bool], Field(default=None)]
|
||||
|
||||
class AnnotatedFormModel(Parent):
|
||||
|
|
@ -74,14 +77,23 @@ if PYDANTIC_V2:
|
|||
default_false: Annotated[bool, Form(default=False)]
|
||||
default_none: Annotated[Optional[bool], Form(default=None)]
|
||||
default_zero: Annotated[int, Form(default=0)]
|
||||
default_str: Annotated[str, Form(default="foo")]
|
||||
true_if_unset: Annotated[Optional[bool], Form(default=None)]
|
||||
|
||||
|
||||
class SimpleForm(BaseModel):
|
||||
"""https://github.com/fastapi/fastapi/pull/13464#issuecomment-2708378172"""
|
||||
|
||||
foo: Annotated[str, Form(default="bar")]
|
||||
alias_with: Annotated[str, Form(alias="with", default="nothing")]
|
||||
|
||||
|
||||
class ResponseModel(BaseModel):
|
||||
fields_set: list = Field(default_factory=list)
|
||||
dumped_fields_no_exclude: dict = Field(default_factory=dict)
|
||||
dumped_fields_exclude_default: dict = Field(default_factory=dict)
|
||||
dumped_fields_exclude_unset: dict = Field(default_factory=dict)
|
||||
dumped_fields_no_meta: dict = Field(default_factory=dict)
|
||||
init_input: dict
|
||||
|
||||
@classmethod
|
||||
|
|
@ -93,6 +105,9 @@ class ResponseModel(BaseModel):
|
|||
dumped_fields_no_exclude=value.model_dump(),
|
||||
dumped_fields_exclude_default=value.model_dump(exclude_defaults=True),
|
||||
dumped_fields_exclude_unset=value.model_dump(exclude_unset=True),
|
||||
dumped_fields_no_meta=value.model_dump(
|
||||
exclude={"init_input", "fields_set"}
|
||||
),
|
||||
)
|
||||
else:
|
||||
return ResponseModel(
|
||||
|
|
@ -101,6 +116,7 @@ class ResponseModel(BaseModel):
|
|||
dumped_fields_no_exclude=value.dict(),
|
||||
dumped_fields_exclude_default=value.dict(exclude_defaults=True),
|
||||
dumped_fields_exclude_unset=value.dict(exclude_unset=True),
|
||||
dumped_fields_no_meta=value.dict(exclude={"init_input", "fields_set"}),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -132,6 +148,36 @@ if PYDANTIC_V2:
|
|||
return ResponseModel.from_value(value)
|
||||
|
||||
|
||||
@app.post("/form/inlined")
|
||||
async def form_inlined(
|
||||
default_true: Annotated[bool, Form()] = True,
|
||||
default_false: Annotated[bool, Form()] = False,
|
||||
default_none: Annotated[Optional[bool], Form()] = None,
|
||||
default_zero: Annotated[int, Form()] = 0,
|
||||
default_str: Annotated[str, Form()] = "foo",
|
||||
true_if_unset: Annotated[Optional[bool], Form()] = None,
|
||||
):
|
||||
"""
|
||||
Rather than using a model, inline the fields in the endpoint.
|
||||
|
||||
This doesn't use the `ResponseModel` pattern, since that is just to
|
||||
test the instantiation behavior prior to the endpoint function.
|
||||
Since we are receiving the values of the fields here (and thus,
|
||||
having the defaults is correct behavior), we just return the values.
|
||||
"""
|
||||
if true_if_unset is None:
|
||||
true_if_unset = True
|
||||
|
||||
return {
|
||||
"default_true": default_true,
|
||||
"default_false": default_false,
|
||||
"default_none": default_none,
|
||||
"default_zero": default_zero,
|
||||
"default_str": default_str,
|
||||
"true_if_unset": true_if_unset,
|
||||
}
|
||||
|
||||
|
||||
@app.post("/json/standard")
|
||||
async def json_standard(value: StandardModel) -> ResponseModel:
|
||||
return ResponseModel.from_value(value)
|
||||
|
|
@ -153,6 +199,12 @@ if PYDANTIC_V2:
|
|||
return ResponseModel.from_value(value)
|
||||
|
||||
|
||||
@app.post("/simple-form")
|
||||
def form_endpoint(model: Annotated[SimpleForm, Form()]) -> dict:
|
||||
"""https://github.com/fastapi/fastapi/pull/13464#issuecomment-2708378172"""
|
||||
return model.model_dump()
|
||||
|
||||
|
||||
if PYDANTIC_V2:
|
||||
MODEL_TYPES = {
|
||||
"standard": StandardModel,
|
||||
|
|
@ -176,7 +228,7 @@ def client() -> TestClient:
|
|||
|
||||
@pytest.mark.parametrize("encoding", ENCODINGS)
|
||||
@pytest.mark.parametrize("model_type", MODEL_TYPES.keys())
|
||||
def test_no_prefill_defaults_all_unset(encoding, model_type, client, monkeypatch):
|
||||
def test_no_prefill_defaults_all_unset(encoding, model_type, client):
|
||||
"""
|
||||
When the model is instantiated by the server, it should not have its defaults prefilled
|
||||
"""
|
||||
|
|
@ -196,7 +248,7 @@ def test_no_prefill_defaults_all_unset(encoding, model_type, client, monkeypatch
|
|||
|
||||
@pytest.mark.parametrize("encoding", ENCODINGS)
|
||||
@pytest.mark.parametrize("model_type", MODEL_TYPES.keys())
|
||||
def test_no_prefill_defaults_partially_set(encoding, model_type, client, monkeypatch):
|
||||
def test_no_prefill_defaults_partially_set(encoding, model_type, client):
|
||||
"""
|
||||
When the model is instantiated by the server, it should not have its defaults prefilled,
|
||||
and pydantic should be able to differentiate between unset and default values when some are passed
|
||||
|
|
@ -230,3 +282,44 @@ def test_no_prefill_defaults_partially_set(encoding, model_type, client, monkeyp
|
|||
assert response_model.dumped_fields_no_exclude["true_if_unset"] is False
|
||||
assert "default_zero" not in dumped_exclude_default
|
||||
assert "default_zero" not in response_model.dumped_fields_exclude_default
|
||||
|
||||
|
||||
def test_casted_empty_defaults(client: TestClient):
|
||||
"""https://github.com/fastapi/fastapi/pull/13464#issuecomment-2708378172"""
|
||||
form_content = {"foo": "", "with": ""}
|
||||
response = client.post("/simple-form", data=form_content)
|
||||
response_content = response.json()
|
||||
assert response_content["foo"] == "bar" # Expected :'bar' -> Actual :''
|
||||
assert response_content["alias_with"] == "nothing" # ok
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_type", list(MODEL_TYPES.keys()) + ["inlined"])
|
||||
def test_empty_string_inputs(model_type, client):
|
||||
"""
|
||||
Form inputs with no input are empty strings,
|
||||
these should be treated as being unset.
|
||||
"""
|
||||
data = {
|
||||
"default_true": "",
|
||||
"default_false": "",
|
||||
"default_none": "",
|
||||
"default_str": "",
|
||||
"default_zero": "",
|
||||
"true_if_unset": "",
|
||||
}
|
||||
response = client.post(f"/form/{model_type}", data=data)
|
||||
assert response.status_code == 200
|
||||
if model_type != "inlined":
|
||||
response_model = ResponseModel(**response.json())
|
||||
assert set(response_model.fields_set) == {"true_if_unset", "init_input"}
|
||||
response_data = response_model.dumped_fields_no_meta
|
||||
else:
|
||||
response_data = response.json()
|
||||
assert response_data == {
|
||||
"default_true": True,
|
||||
"default_false": False,
|
||||
"default_none": None,
|
||||
"default_zero": 0,
|
||||
"default_str": "foo",
|
||||
"true_if_unset": True,
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue