Skip to content

Commit 0280a98

Browse files
committed
Implement #44: Validate subclasses of StrEnum and IntEnum
1 parent abcb480 commit 0280a98

File tree

10 files changed

+562
-53
lines changed

10 files changed

+562
-53
lines changed

README.md

+35-34
Large diffs are not rendered by default.

flask_parameter_validation/parameter_types/parameter.py

+24-18
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
"""
55
import re
66
from datetime import date, datetime, time
7+
from enum import Enum, StrEnum, IntEnum
8+
79
import dateutil.parser as parser
810
import jsonschema
911
from jsonschema.exceptions import ValidationError as JSONSchemaValidationError
@@ -13,22 +15,23 @@ class Parameter:
1315

1416
# Parameter initialisation
1517
def __init__(
16-
self,
17-
default=None, # any: default parameter value
18-
min_str_length=None, # int: min parameter length
19-
max_str_length=None, # int: max parameter length
20-
min_list_length=None, # int: min number of items in list
21-
max_list_length=None, # int: max number of items in list
22-
min_int=None, # int: min number (if val is int)
23-
max_int=None, # int: max number (if val is int)
24-
whitelist=None, # str: character whitelist
25-
blacklist=None, # str: character blacklist
26-
pattern=None, # str: regexp pattern
27-
func=None, # Callable -> Union[bool, tuple[bool, str]]: function performing a fully customized validation
28-
datetime_format=None, # str: datetime format string (https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes),
29-
comment=None, # str: comment for autogenerated documentation
30-
alias=None, # str: alias for parameter name
31-
json_schema=None, # dict: JSON Schema to check received dicts or lists against
18+
self,
19+
default=None, # any: default parameter value
20+
min_str_length=None, # int: min parameter length
21+
max_str_length=None, # int: max parameter length
22+
min_list_length=None, # int: min number of items in list
23+
max_list_length=None, # int: max number of items in list
24+
min_int=None, # int: min number (if val is int)
25+
max_int=None, # int: max number (if val is int)
26+
whitelist=None, # str: character whitelist
27+
blacklist=None, # str: character blacklist
28+
pattern=None, # str: regexp pattern
29+
func=None, # Callable -> Union[bool, tuple[bool, str]]: function performing a fully customized validation
30+
datetime_format=None,
31+
# str: datetime format string (https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes),
32+
comment=None, # str: comment for autogenerated documentation
33+
alias=None, # str: alias for parameter name
34+
json_schema=None, # dict: JSON Schema to check received dicts or lists against
3235
):
3336
self.default = default
3437
self.min_list_length = min_list_length
@@ -150,8 +153,6 @@ def validate(self, value):
150153
if self.func is not None and not original_value_type_list:
151154
self.func_helper(value)
152155

153-
154-
155156
return True
156157

157158
def convert(self, value, allowed_types):
@@ -183,4 +184,9 @@ def convert(self, value, allowed_types):
183184
return date.fromisoformat(str(value))
184185
except ValueError:
185186
raise ValueError("date format does not match ISO 8601")
187+
elif len(allowed_types) == 1 and (issubclass(allowed_types[0], StrEnum) or issubclass(allowed_types[0], IntEnum)):
188+
if issubclass(allowed_types[0], IntEnum):
189+
value = int(value)
190+
returning = allowed_types[0](value)
191+
return returning
186192
return value

flask_parameter_validation/test/conftest.py

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ def app():
1111

1212
@pytest.fixture()
1313
def client(app):
14+
print(app.url_map)
1415
return app.test_client()
1516

1617

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from enum import StrEnum, IntEnum
2+
3+
4+
class Fruits(StrEnum):
5+
APPLE = "apple"
6+
ORANGE = "orange"
7+
8+
9+
class Binary(IntEnum):
10+
ZERO = 0
11+
ONE = 1

flask_parameter_validation/test/test_form_params.py

+122
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import datetime
33
from typing import Type, List, Optional
44

5+
from flask_parameter_validation.test.enums import Fruits, Binary
6+
57

68
def list_assertion_helper(length: int, list_children_type: Type, expected_list: List, tested_list,
79
expected_call: Optional[str] = None):
@@ -903,3 +905,123 @@ def test_max_list_length(client):
903905
# Test that above length yields error
904906
r = client.post(url, data={"v": ["the", "longest", "of", "lists"]})
905907
assert "error" in r.json
908+
909+
910+
# Enum validation
911+
def test_required_str_enum(client):
912+
url = "/form/str_enum/required"
913+
# Test that present str_enum input yields input value
914+
r = client.post(url, data={"v": Fruits.APPLE.value})
915+
assert "v" in r.json
916+
print(r.json["v"])
917+
assert r.json["v"] == Fruits.APPLE.value
918+
# Test that missing input yields error
919+
r = client.post(url)
920+
assert "error" in r.json
921+
# Test that present non-str_enum input yields error
922+
r = client.post(url, data={"v": "a"})
923+
assert "error" in r.json
924+
925+
926+
def test_optional_str_enum(client):
927+
url = "/form/str_enum/optional"
928+
# Test that missing input yields None
929+
r = client.post(url)
930+
assert "v" in r.json
931+
assert r.json["v"] is None
932+
# Test that present str_enum input yields input value
933+
r = client.post(url, data={"v": Fruits.ORANGE.value})
934+
assert "v" in r.json
935+
assert r.json["v"] == Fruits.ORANGE.value
936+
# Test that present non-str_enum input yields error
937+
r = client.post(url, data={"v": "v"})
938+
assert "error" in r.json
939+
940+
941+
def test_str_enum_default(client):
942+
url = "/form/str_enum/default"
943+
# Test that missing input for required and optional yields default values
944+
r = client.post(url)
945+
assert "n_opt" in r.json
946+
assert r.json["n_opt"] == Fruits.APPLE.value
947+
assert "opt" in r.json
948+
assert r.json["opt"] == Fruits.ORANGE.value
949+
# Test that present str_enum input for required and optional yields input values
950+
r = client.post(url, data={"opt": Fruits.ORANGE.value, "n_opt": Fruits.APPLE.value})
951+
assert "opt" in r.json
952+
assert r.json["opt"] == Fruits.ORANGE.value
953+
assert "n_opt" in r.json
954+
assert r.json["n_opt"] == Fruits.APPLE.value
955+
# Test that present non-str_enum input for required yields error
956+
r = client.post(url, data={"opt": "a", "n_opt": "b"})
957+
assert "error" in r.json
958+
959+
960+
def test_str_enum_func(client):
961+
url = "/form/str_enum/func"
962+
# Test that input passing func yields input
963+
r = client.post(url, data={"v": Fruits.ORANGE.value})
964+
assert "v" in r.json
965+
assert r.json["v"] == Fruits.ORANGE.value
966+
# Test that input failing func yields error
967+
r = client.post(url, data={"v": Fruits.APPLE.value})
968+
assert "error" in r.json
969+
970+
971+
def test_required_int_enum(client):
972+
url = "/form/int_enum/required"
973+
# Test that present int_enum input yields input value
974+
r = client.post(url, data={"v": Binary.ONE.value})
975+
assert "v" in r.json
976+
assert r.json["v"] == Binary.ONE.value
977+
# Test that missing input yields error
978+
r = client.post(url)
979+
assert "error" in r.json
980+
# Test that present non-int_enum input yields error
981+
r = client.post(url, data={"v": 8})
982+
assert "error" in r.json
983+
984+
985+
def test_optional_int_enum(client):
986+
url = "/form/int_enum/optional"
987+
# Test that missing input yields None
988+
r = client.post(url)
989+
assert "v" in r.json
990+
assert r.json["v"] is None
991+
# Test that present int_enum input yields input value
992+
r = client.post(url, data={"v": Binary.ZERO.value})
993+
assert "v" in r.json
994+
assert r.json["v"] == Binary.ZERO.value
995+
# Test that present non-int_enum input yields error
996+
r = client.post(url, data={"v": 8})
997+
assert "error" in r.json
998+
999+
1000+
def test_int_enum_default(client):
1001+
url = "/form/int_enum/default"
1002+
# Test that missing input for required and optional yields default values
1003+
r = client.post(url)
1004+
assert "n_opt" in r.json
1005+
assert r.json["n_opt"] == Binary.ZERO.value
1006+
assert "opt" in r.json
1007+
assert r.json["opt"] == Binary.ONE.value
1008+
# Test that present int_enum input for required and optional yields input values
1009+
r = client.post(url, data={"opt": Binary.ONE.value, "n_opt": Binary.ZERO.value})
1010+
assert "opt" in r.json
1011+
assert r.json["opt"] == Binary.ONE.value
1012+
assert "n_opt" in r.json
1013+
assert r.json["n_opt"] == Binary.ZERO.value
1014+
# Test that present non-int_enum input for required yields error
1015+
r = client.post(url, data={"opt": "a", "n_opt": 9})
1016+
assert "error" in r.json
1017+
1018+
1019+
def test_int_enum_func(client):
1020+
url = "/form/int_enum/func"
1021+
# Test that input passing func yields input
1022+
r = client.post(url, data={"v": Binary.ZERO.value})
1023+
assert "v" in r.json
1024+
assert r.json["v"] == Binary.ZERO.value
1025+
# Test that input failing func yields error
1026+
r = client.post(url, data={"v": Binary.ONE.value})
1027+
assert "error" in r.json

flask_parameter_validation/test/test_json_params.py

+122-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import datetime
33
from typing import Type, List, Optional
44

5+
from flask_parameter_validation.test.enums import Binary, Fruits
6+
57

68
def list_assertion_helper(length: int, list_children_type: Type, expected_list: List, tested_list,
79
expected_call: Optional[str] = None):
@@ -1034,4 +1036,123 @@ def test_dict_json_schema(client):
10341036
"last_name": "Doe"
10351037
}
10361038
r = client.post(url, json={"v": v})
1037-
assert "error" in r.json
1039+
assert "error" in r.json
1040+
1041+
1042+
# Enum validation
1043+
def test_required_str_enum(client):
1044+
url = "/json/str_enum/required"
1045+
# Test that present str_enum input yields input value
1046+
r = client.post(url, json={"v": Fruits.APPLE.value})
1047+
assert "v" in r.json
1048+
assert r.json["v"] == Fruits.APPLE.value
1049+
# Test that missing input yields error
1050+
r = client.post(url)
1051+
assert "error" in r.json
1052+
# Test that present non-str_enum input yields error
1053+
r = client.post(url, json={"v": "a"})
1054+
assert "error" in r.json
1055+
1056+
1057+
def test_optional_str_enum(client):
1058+
url = "/json/str_enum/optional"
1059+
# Test that missing input yields None
1060+
r = client.post(url)
1061+
assert "v" in r.json
1062+
assert r.json["v"] is None
1063+
# Test that present str_enum input yields input value
1064+
r = client.post(url, json={"v": Fruits.ORANGE.value})
1065+
assert "v" in r.json
1066+
assert r.json["v"] == Fruits.ORANGE.value
1067+
# Test that present non-str_enum input yields error
1068+
r = client.post(url, json={"v": "v"})
1069+
assert "error" in r.json
1070+
1071+
1072+
def test_str_enum_default(client):
1073+
url = "/json/str_enum/default"
1074+
# Test that missing input for required and optional yields default values
1075+
r = client.post(url)
1076+
assert "n_opt" in r.json
1077+
assert r.json["n_opt"] == Fruits.APPLE.value
1078+
assert "opt" in r.json
1079+
assert r.json["opt"] == Fruits.ORANGE.value
1080+
# Test that present str_enum input for required and optional yields input values
1081+
r = client.post(url, json={"opt": Fruits.ORANGE.value, "n_opt": Fruits.APPLE.value})
1082+
assert "opt" in r.json
1083+
assert r.json["opt"] == Fruits.ORANGE.value
1084+
assert "n_opt" in r.json
1085+
assert r.json["n_opt"] == Fruits.APPLE.value
1086+
# Test that present non-str_enum input for required yields error
1087+
r = client.post(url, json={"opt": "a", "n_opt": "b"})
1088+
assert "error" in r.json
1089+
1090+
1091+
def test_str_enum_func(client):
1092+
url = "/json/str_enum/func"
1093+
# Test that input passing func yields input
1094+
r = client.post(url, json={"v": Fruits.ORANGE.value})
1095+
assert "v" in r.json
1096+
assert r.json["v"] == Fruits.ORANGE.value
1097+
# Test that input failing func yields error
1098+
r = client.post(url, json={"v": Fruits.APPLE.value})
1099+
assert "error" in r.json
1100+
1101+
1102+
def test_required_int_enum(client):
1103+
url = "/json/int_enum/required"
1104+
# Test that present int_enum input yields input value
1105+
r = client.post(url, json={"v": Binary.ONE.value})
1106+
assert "v" in r.json
1107+
assert r.json["v"] == Binary.ONE.value
1108+
# Test that missing input yields error
1109+
r = client.post(url)
1110+
assert "error" in r.json
1111+
# Test that present non-int_enum input yields error
1112+
r = client.post(url, json={"v": 8})
1113+
assert "error" in r.json
1114+
1115+
1116+
def test_optional_int_enum(client):
1117+
url = "/json/int_enum/optional"
1118+
# Test that missing input yields None
1119+
r = client.post(url)
1120+
assert "v" in r.json
1121+
assert r.json["v"] is None
1122+
# Test that present int_enum input yields input value
1123+
r = client.post(url, json={"v": Binary.ZERO.value})
1124+
assert "v" in r.json
1125+
assert r.json["v"] == Binary.ZERO.value
1126+
# Test that present non-int_enum input yields error
1127+
r = client.post(url, json={"v": 8})
1128+
assert "error" in r.json
1129+
1130+
1131+
def test_int_enum_default(client):
1132+
url = "/json/int_enum/default"
1133+
# Test that missing input for required and optional yields default values
1134+
r = client.post(url)
1135+
assert "n_opt" in r.json
1136+
assert r.json["n_opt"] == Binary.ZERO.value
1137+
assert "opt" in r.json
1138+
assert r.json["opt"] == Binary.ONE.value
1139+
# Test that present int_enum input for required and optional yields input values
1140+
r = client.post(url, json={"opt": Binary.ONE.value, "n_opt": Binary.ZERO.value})
1141+
assert "opt" in r.json
1142+
assert r.json["opt"] == Binary.ONE.value
1143+
assert "n_opt" in r.json
1144+
assert r.json["n_opt"] == Binary.ZERO.value
1145+
# Test that present non-int_enum input for required yields error
1146+
r = client.post(url, json={"opt": "a", "n_opt": 9})
1147+
assert "error" in r.json
1148+
1149+
1150+
def test_int_enum_func(client):
1151+
url = "/json/int_enum/func"
1152+
# Test that input passing func yields input
1153+
r = client.post(url, json={"v": Binary.ZERO})
1154+
assert "v" in r.json
1155+
assert r.json["v"] == Binary.ZERO.value
1156+
# Test that input failing func yields error
1157+
r = client.post(url, json={"v": Binary.ONE.value})
1158+
assert "error" in r.json

0 commit comments

Comments
 (0)