mirror of
https://github.com/fastapi/fastapi.git
synced 2026-11-03 06:34:36 -05:00
🐛 Treat empty scalar values as missing in form models (#16529)
This commit is contained in:
1 parent
e5fc7ef524
commit
f214cd3462
3 files changed
+373
-115
No files matched your search
@@ -7,6 +7,10 @@ hide:
|
||||
|
||||
## Latest Changes
|
||||
|
||||
Browsers submit empty text inputs as empty strings (`""`). FastAPI now treats empty scalar form values as missing in form models, matching individual `Form` parameters. Required fields return a validation error. Fields with defaults or default factories use them and remain unset in `model_dump(exclude_unset=True)`.
|
||||
|
||||
Empty strings inside lists are preserved. Omitted boolean form fields still become `False`. JSON requests and direct Pydantic validation are unchanged.
|
||||
|
||||
## 0.148.0 (2026-10-19)
|
||||
|
||||
Defaults for omitted `Query`, `Header`, `Cookie`, and `Form` parameters and model fields are no longer treated as user input. Defaults are not validated unless `validate_default=True` is enabled. If you rely on validators running for omitted values, enable default validation with `Field(validate_default=True)` for model fields or, for example, `Query(validate_default=True)` for individual parameters. `Body(validate_default=True)` is now honored too.
|
||||
|
||||
@@ -69,7 +69,7 @@ from fastapi.logger import logger
|
||||
from fastapi.security.oauth2 import SecurityScopes
|
||||
from fastapi.types import DependencyCacheKey
|
||||
from fastapi.utils import create_model_field, get_path_param_names
|
||||
from pydantic import AliasChoices, BaseModel, Json
|
||||
from pydantic import AliasChoices, AliasPath, BaseModel, Json
|
||||
from pydantic.fields import FieldInfo
|
||||
from starlette.background import BackgroundTasks as StarletteBackgroundTasks
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
@@ -762,7 +762,7 @@ def _get_multidict_value(
|
||||
if (
|
||||
value is None
|
||||
or (
|
||||
isinstance(field.field_info, params.Form)
|
||||
isinstance(values, FormData)
|
||||
and isinstance(value, str) # For type checks
|
||||
and value == ""
|
||||
)
|
||||
@@ -916,65 +916,69 @@ async def _extract_form_body(
|
||||
field_aliases: set[str] = set()
|
||||
|
||||
for field in body_fields:
|
||||
alias = get_validation_alias(field)
|
||||
aliases = [get_validation_alias(field)]
|
||||
validation_alias = field.field_info.validation_alias
|
||||
# Interpret missing boolean form fields as unchecked HTML checkboxes.
|
||||
infer_false_if_missing = field.field_info.annotation in (
|
||||
bool,
|
||||
bool | None,
|
||||
) and (
|
||||
validation_alias is None
|
||||
or isinstance(validation_alias, str)
|
||||
or (
|
||||
isinstance(validation_alias, AliasChoices)
|
||||
and all(isinstance(choice, str) for choice in validation_alias.choices)
|
||||
has_alias_path = isinstance(validation_alias, AliasPath) or (
|
||||
isinstance(validation_alias, AliasChoices)
|
||||
and any(
|
||||
isinstance(choice, AliasPath) for choice in validation_alias.choices
|
||||
)
|
||||
)
|
||||
if infer_false_if_missing:
|
||||
if not has_alias_path:
|
||||
if isinstance(validation_alias, AliasChoices):
|
||||
aliases = cast(list[str], validation_alias.choices)
|
||||
alias = next(
|
||||
(name for name in aliases if name in received_body), aliases[0]
|
||||
)
|
||||
aliases = list(cast(list[str], validation_alias.choices))
|
||||
config = field.config or {}
|
||||
if config.get(
|
||||
"validate_by_name", config.get("populate_by_name", False)
|
||||
) and (
|
||||
alias not in received_body or config.get("validate_by_alias") is False
|
||||
):
|
||||
alias = field.name
|
||||
field_aliases.add(alias)
|
||||
if infer_false_if_missing and alias not in received_body:
|
||||
value: Any = False
|
||||
else:
|
||||
value = _get_multidict_value(field, received_body, alias=alias)
|
||||
field_info = field.field_info
|
||||
if (
|
||||
isinstance(field_info, params.File)
|
||||
and is_bytes_or_nonable_bytes_annotation(field.field_info.annotation)
|
||||
and isinstance(value, UploadFile)
|
||||
):
|
||||
value = await value.read()
|
||||
elif (
|
||||
is_bytes_sequence_annotation(field.field_info.annotation)
|
||||
and isinstance(field_info, params.File)
|
||||
and value_is_sequence(value)
|
||||
):
|
||||
# For types
|
||||
assert isinstance(value, sequence_types)
|
||||
results: list[bytes | str] = []
|
||||
for sub_value in value:
|
||||
if isinstance(sub_value, UploadFile):
|
||||
results.append(await sub_value.read())
|
||||
else:
|
||||
results.append(sub_value)
|
||||
value = serialize_sequence_value(field=field, value=results)
|
||||
if field.config is None:
|
||||
# Individual parameters are later looked up by their declared alias.
|
||||
alias = get_validation_alias(field)
|
||||
if config.get("validate_by_name", config.get("populate_by_name", False)):
|
||||
if config.get("validate_by_alias") is False:
|
||||
aliases = [field.name]
|
||||
elif field.name not in aliases:
|
||||
aliases.append(field.name)
|
||||
# Interpret missing boolean form fields as unchecked HTML checkboxes.
|
||||
infer_false_if_missing = (
|
||||
not has_alias_path
|
||||
and field.field_info.annotation in (bool, bool | None)
|
||||
and not any(alias in received_body for alias in aliases)
|
||||
)
|
||||
if field.config is None or infer_false_if_missing:
|
||||
selected_alias = aliases[0]
|
||||
for alias in aliases:
|
||||
if _get_multidict_value(field, received_body, alias=alias) is not None:
|
||||
selected_alias = alias
|
||||
break
|
||||
aliases = [selected_alias]
|
||||
for alias in aliases:
|
||||
field_aliases.add(alias)
|
||||
if value is not None:
|
||||
values[alias] = value
|
||||
if infer_false_if_missing:
|
||||
value: Any = False
|
||||
else:
|
||||
value = _get_multidict_value(field, received_body, alias=alias)
|
||||
field_info = field.field_info
|
||||
if (
|
||||
isinstance(field_info, params.File)
|
||||
and is_bytes_or_nonable_bytes_annotation(field.field_info.annotation)
|
||||
and isinstance(value, UploadFile)
|
||||
):
|
||||
value = await value.read()
|
||||
elif (
|
||||
is_bytes_sequence_annotation(field.field_info.annotation)
|
||||
and isinstance(field_info, params.File)
|
||||
and value_is_sequence(value)
|
||||
):
|
||||
# For types
|
||||
assert isinstance(value, sequence_types)
|
||||
results: list[bytes | str] = []
|
||||
for sub_value in value:
|
||||
if isinstance(sub_value, UploadFile):
|
||||
results.append(await sub_value.read())
|
||||
else:
|
||||
results.append(sub_value)
|
||||
value = serialize_sequence_value(field=field, value=results)
|
||||
if field.config is None:
|
||||
# Individual parameters are later looked up by their declared alias.
|
||||
alias = get_validation_alias(field)
|
||||
field_aliases.add(alias)
|
||||
if value is not None:
|
||||
values[alias] = value
|
||||
for key in received_body.keys():
|
||||
if key not in field_aliases:
|
||||
param_values = received_body.getlist(key)
|
||||
|
||||
@@ -16,19 +16,7 @@ from pydantic import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/parameter",
|
||||
pytest.param(
|
||||
"/model",
|
||||
marks=pytest.mark.xfail(
|
||||
reason="Empty scalar form strings are not treated as missing in models",
|
||||
raises=AssertionError,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("path", ["/parameter", "/model"])
|
||||
@pytest.mark.parametrize("default", [None, "default"])
|
||||
@pytest.mark.parametrize("encoding", ["urlencoded", "multipart"])
|
||||
def test_empty_string_uses_default(path, default, encoding):
|
||||
@@ -60,19 +48,7 @@ def test_empty_string_uses_default(path, default, encoding):
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/parameter",
|
||||
pytest.param(
|
||||
"/model",
|
||||
marks=pytest.mark.xfail(
|
||||
reason="Empty scalar form strings are not treated as missing in models",
|
||||
raises=AssertionError,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("path", ["/parameter", "/model"])
|
||||
@pytest.mark.parametrize("default", [None, 10])
|
||||
@pytest.mark.parametrize("encoding", ["urlencoded", "multipart"])
|
||||
def test_empty_integer_uses_default(path, default, encoding):
|
||||
@@ -104,27 +80,17 @@ def test_empty_integer_uses_default(path, default, encoding):
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/parameter",
|
||||
pytest.param(
|
||||
"/model",
|
||||
marks=pytest.mark.xfail(
|
||||
reason="Empty required scalar form strings are not treated as missing in models",
|
||||
raises=AssertionError,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_empty_required_string_is_missing(path):
|
||||
@pytest.mark.parametrize("path", ["/parameter", "/model"])
|
||||
@pytest.mark.parametrize("encoding", ["urlencoded", "multipart"])
|
||||
@pytest.mark.parametrize("annotation", [str, str | None])
|
||||
def test_empty_required_string_is_missing(path, encoding, annotation):
|
||||
app = FastAPI()
|
||||
|
||||
class Model(BaseModel):
|
||||
value: str | None
|
||||
value: annotation
|
||||
|
||||
@app.post("/parameter")
|
||||
def read_parameter(value: Annotated[str | None, Form()]):
|
||||
def read_parameter(value: Annotated[annotation, Form()]):
|
||||
return {"value": value, "fields_set": None}
|
||||
|
||||
@app.post("/model")
|
||||
@@ -135,34 +101,27 @@ def test_empty_required_string_is_missing(path):
|
||||
supplied = client.post(path, data={"value": "hello"})
|
||||
assert supplied.status_code == 200
|
||||
assert supplied.json()["value"] == "hello"
|
||||
response = client.post(path, data={"value": ""})
|
||||
if encoding == "urlencoded":
|
||||
response = client.post(path, data={"value": ""})
|
||||
else:
|
||||
response = client.post(path, files={"value": (None, "")})
|
||||
assert response.status_code == 422
|
||||
assert [(error["type"], error["loc"]) for error in response.json()["detail"]] == [
|
||||
("missing", ["body", "value"]),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path",
|
||||
[
|
||||
"/parameter",
|
||||
pytest.param(
|
||||
"/model",
|
||||
marks=pytest.mark.xfail(
|
||||
reason="Empty required scalar form strings are not treated as missing in models",
|
||||
raises=AssertionError,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_empty_required_integer_is_missing(path):
|
||||
@pytest.mark.parametrize("path", ["/parameter", "/model"])
|
||||
@pytest.mark.parametrize("encoding", ["urlencoded", "multipart"])
|
||||
@pytest.mark.parametrize("annotation", [int, int | None])
|
||||
def test_empty_required_integer_is_missing(path, encoding, annotation):
|
||||
app = FastAPI()
|
||||
|
||||
class Model(BaseModel):
|
||||
value: int | None
|
||||
value: annotation
|
||||
|
||||
@app.post("/parameter")
|
||||
def read_parameter(value: Annotated[int | None, Form()]):
|
||||
def read_parameter(value: Annotated[annotation, Form()]):
|
||||
return {"value": value, "fields_set": None}
|
||||
|
||||
@app.post("/model")
|
||||
@@ -173,13 +132,303 @@ def test_empty_required_integer_is_missing(path):
|
||||
supplied = client.post(path, data={"value": "1"})
|
||||
assert supplied.status_code == 200
|
||||
assert supplied.json()["value"] == 1
|
||||
response = client.post(path, data={"value": ""})
|
||||
if encoding == "urlencoded":
|
||||
response = client.post(path, data={"value": ""})
|
||||
else:
|
||||
response = client.post(path, files={"value": (None, "")})
|
||||
assert response.status_code == 422
|
||||
assert [(error["type"], error["loc"]) for error in response.json()["detail"]] == [
|
||||
("missing", ["body", "value"]),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("encoding", ["urlencoded", "multipart"])
|
||||
def test_empty_form_model_value_uses_default_factory(encoding):
|
||||
app = FastAPI()
|
||||
default_factory = Mock(return_value=10)
|
||||
|
||||
class Model(BaseModel):
|
||||
value: Annotated[int, Field(default_factory=default_factory)]
|
||||
|
||||
@app.post("/")
|
||||
def read_model(params: Annotated[Model, Form()]):
|
||||
return {
|
||||
"values": params.model_dump(),
|
||||
"supplied": params.model_dump(exclude_unset=True),
|
||||
}
|
||||
|
||||
default_factory.assert_not_called()
|
||||
client = TestClient(app)
|
||||
if encoding == "urlencoded":
|
||||
response = client.post("/", data={"value": ""})
|
||||
else:
|
||||
response = client.post("/", files={"value": (None, "")})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"values": {"value": 10}, "supplied": {}}
|
||||
default_factory.assert_called_once_with()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("validate_default,expected", [(False, 10), (True, 20)])
|
||||
def test_empty_form_model_value_respects_default_validation(validate_default, expected):
|
||||
app = FastAPI()
|
||||
|
||||
class Model(BaseModel):
|
||||
value: Annotated[
|
||||
int,
|
||||
AfterValidator(lambda value: value * 2),
|
||||
Field(validate_default=validate_default),
|
||||
] = 10
|
||||
|
||||
@app.post("/")
|
||||
def read_model(params: Annotated[Model, Form()]):
|
||||
return {
|
||||
"values": params.model_dump(),
|
||||
"supplied": params.model_dump(exclude_unset=True),
|
||||
}
|
||||
|
||||
client = TestClient(app)
|
||||
response = client.post("/", data={"value": ""})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"values": {"value": expected}, "supplied": {}}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field",
|
||||
[
|
||||
Field(alias="input"),
|
||||
Field(alias="legacy", validation_alias="input"),
|
||||
Field(validation_alias=AliasChoices("first", "input")),
|
||||
],
|
||||
)
|
||||
def test_empty_form_model_value_uses_alias(field):
|
||||
app = FastAPI()
|
||||
|
||||
class Model(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
value: Annotated[int, field] = 10
|
||||
|
||||
@app.post("/")
|
||||
def read_model(params: Annotated[Model, Form()]):
|
||||
return {
|
||||
"values": params.model_dump(),
|
||||
"supplied": params.model_dump(exclude_unset=True),
|
||||
}
|
||||
|
||||
client = TestClient(app)
|
||||
response = client.post("/", data={"input": "5"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"values": {"value": 5}, "supplied": {"value": 5}}
|
||||
response = client.post("/", data={"input": ""})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"values": {"value": 10}, "supplied": {}}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
ConfigDict(populate_by_name=True, extra="forbid"),
|
||||
pytest.param(
|
||||
ConfigDict(validate_by_name=True, validate_by_alias=False, extra="forbid"),
|
||||
marks=pytest.mark.skipif(
|
||||
PYDANTIC_VERSION_MINOR_TUPLE < (2, 11),
|
||||
reason="validate_by_name and validate_by_alias require Pydantic 2.11",
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_empty_form_model_value_accepts_field_name(config):
|
||||
app = FastAPI()
|
||||
|
||||
class Model(BaseModel):
|
||||
model_config = config
|
||||
value: Annotated[int, Field(alias="input")] = 10
|
||||
|
||||
@app.post("/")
|
||||
def read_model(params: Annotated[Model, Form()]):
|
||||
return {
|
||||
"values": params.model_dump(),
|
||||
"supplied": params.model_dump(exclude_unset=True),
|
||||
}
|
||||
|
||||
client = TestClient(app)
|
||||
response = client.post("/", data={"value": "5"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"values": {"value": 5}, "supplied": {"value": 5}}
|
||||
response = client.post("/", data={"value": ""})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"values": {"value": 10}, "supplied": {}}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", ["/parameter", "/model"])
|
||||
@pytest.mark.parametrize(
|
||||
"data,expected",
|
||||
[
|
||||
({"first": "", "second": ""}, 10),
|
||||
({"first": "", "second": "5"}, 5),
|
||||
({"first": "5", "second": ""}, 5),
|
||||
({"first": "5", "second": "6"}, 5),
|
||||
],
|
||||
)
|
||||
def test_empty_form_values_with_alias_choices(path, data, expected):
|
||||
app = FastAPI()
|
||||
|
||||
class Model(BaseModel):
|
||||
value: Annotated[
|
||||
int, Field(validation_alias=AliasChoices("first", "second"))
|
||||
] = 10
|
||||
|
||||
@app.post("/parameter")
|
||||
def read_parameter(
|
||||
value: Annotated[
|
||||
int, Form(validation_alias=AliasChoices("first", "second"))
|
||||
] = 10,
|
||||
):
|
||||
return {"value": value}
|
||||
|
||||
@app.post("/model")
|
||||
def read_model(params: Annotated[Model, Form()]):
|
||||
return params.model_dump()
|
||||
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data=data)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"value": expected}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"path,error_alias", [("/parameter", "value"), ("/model", "first")]
|
||||
)
|
||||
def test_required_form_alias_choices_preserve_validation_precedence(path, error_alias):
|
||||
app = FastAPI()
|
||||
|
||||
class Model(BaseModel):
|
||||
value: Annotated[int, Field(validation_alias=AliasChoices("first", "second"))]
|
||||
|
||||
@app.post("/parameter")
|
||||
def read_parameter(
|
||||
value: Annotated[int, Form(validation_alias=AliasChoices("first", "second"))],
|
||||
):
|
||||
return {"value": value}
|
||||
|
||||
@app.post("/model")
|
||||
def read_model(params: Annotated[Model, Form()]):
|
||||
return params.model_dump()
|
||||
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data={"first": "", "second": "5"})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"value": 5}
|
||||
|
||||
response = client.post(path, data={"first": "invalid", "second": "5"})
|
||||
assert response.status_code == 422
|
||||
assert [(error["type"], error["loc"]) for error in response.json()["detail"]] == [
|
||||
("int_parsing", ["body", error_alias]),
|
||||
]
|
||||
|
||||
response = client.post(path, data={"first": "", "second": ""})
|
||||
assert response.status_code == 422
|
||||
assert [(error["type"], error["loc"]) for error in response.json()["detail"]] == [
|
||||
("missing", ["body", error_alias]),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"data,expected,supplied",
|
||||
[
|
||||
({"first": "", "value": ""}, 10, {}),
|
||||
({"first": "", "value": "5"}, 5, {"value": 5}),
|
||||
],
|
||||
)
|
||||
def test_empty_form_model_alias_falls_back_to_field_name(data, expected, supplied):
|
||||
app = FastAPI()
|
||||
|
||||
class Model(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
value: Annotated[
|
||||
int, Field(validation_alias=AliasChoices("first", "second"))
|
||||
] = 10
|
||||
|
||||
@app.post("/")
|
||||
def read_model(params: Annotated[Model, Form()]):
|
||||
return {
|
||||
"value": params.value,
|
||||
"supplied": params.model_dump(exclude_unset=True),
|
||||
}
|
||||
|
||||
client = TestClient(app)
|
||||
response = client.post("/", data=data)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"value": expected, "supplied": supplied}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", ["/parameter", "/model"])
|
||||
@pytest.mark.parametrize(
|
||||
"data,expected",
|
||||
[
|
||||
({"second": [""]}, [""]),
|
||||
({"second": ["", "hello"]}, ["", "hello"]),
|
||||
({"first": [""], "second": ["hello"]}, [""]),
|
||||
],
|
||||
)
|
||||
def test_form_list_alias_choices_preserve_empty_items(path, data, expected):
|
||||
app = FastAPI()
|
||||
|
||||
class Model(BaseModel):
|
||||
value: Annotated[
|
||||
list[str], Field(validation_alias=AliasChoices("first", "second"))
|
||||
]
|
||||
|
||||
@app.post("/parameter")
|
||||
def read_parameter(
|
||||
value: Annotated[
|
||||
list[str], Form(validation_alias=AliasChoices("first", "second"))
|
||||
],
|
||||
):
|
||||
return {"value": value}
|
||||
|
||||
@app.post("/model")
|
||||
def read_model(params: Annotated[Model, Form()]):
|
||||
return params.model_dump(exclude_unset=True)
|
||||
|
||||
client = TestClient(app)
|
||||
response = client.post(path, data=data)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"value": expected}
|
||||
|
||||
|
||||
def test_empty_form_model_value_preserves_extra_and_json_strings():
|
||||
app = FastAPI()
|
||||
|
||||
class Model(BaseModel):
|
||||
model_config = ConfigDict(extra="allow")
|
||||
value: str = "default"
|
||||
|
||||
@app.post("/form")
|
||||
def read_form(params: Annotated[Model, Form()]):
|
||||
return {
|
||||
"values": params.model_dump(),
|
||||
"supplied": params.model_dump(exclude_unset=True),
|
||||
}
|
||||
|
||||
@app.post("/json")
|
||||
def read_json(params: Model):
|
||||
return params.model_dump(exclude_unset=True)
|
||||
|
||||
client = TestClient(app)
|
||||
response = client.post("/form", data={"value": "", "extra": ""})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"values": {"value": "default", "extra": ""},
|
||||
"supplied": {"extra": ""},
|
||||
}
|
||||
response = client.post("/json", json={"value": "", "extra": ""})
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"value": "", "extra": ""}
|
||||
assert Model.model_validate({"value": ""}).model_dump(exclude_unset=True) == {
|
||||
"value": ""
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path", ["/parameter", "/model"])
|
||||
@pytest.mark.parametrize("values", [[""], ["", "hello"]])
|
||||
def test_empty_required_string_list_item_is_preserved(path, values):
|
||||
@@ -538,6 +787,7 @@ def test_checkbox_model_accepts_field_name(config, data, expected, validation_al
|
||||
({"enabled": "on"}, True),
|
||||
({"checkbox": "on"}, True),
|
||||
({"checkbox": ["off", "on"]}, True),
|
||||
({"enabled": "", "checkbox": "on"}, True),
|
||||
({}, False),
|
||||
],
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user