Skip to content

Commit 2edb5e5

Browse files
authored
Merge pull request #50 from Ge0rg3/dev/smt5541/typing_list_deprecated
Add support for standard list type hints
2 parents 1db7e5a + e425b8c commit 2edb5e5

File tree

8 files changed

+171
-31
lines changed

8 files changed

+171
-31
lines changed

.github/workflows/python-test.yml

+8-4
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,16 @@ jobs:
1717

1818
runs-on: ubuntu-latest
1919

20+
strategy:
21+
matrix:
22+
python-version: ["3.9", "3.10", "3.11", "3.12"]
23+
2024
steps:
21-
- uses: actions/checkout@v3
22-
- name: Set up Python 3
23-
uses: actions/setup-python@v3
25+
- uses: actions/checkout@v4
26+
- name: Set up Python ${{ matrix.python-version }}
27+
uses: actions/setup-python@v5
2428
with:
25-
python-version: "3.x"
29+
python-version: ${{ matrix.python-version }}
2630
- name: Install dependencies
2731
run: |
2832
python -m pip install --upgrade pip

README.md

+19-19
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
## Usage Example
1111
```py
1212
from flask import Flask
13-
from typing import List, Optional
13+
from typing import Optional
1414
from flask_parameter_validation import ValidateParameters, Route, Json, Query
1515
from datetime import datetime
1616

@@ -22,7 +22,7 @@ def hello(
2222
id: int = Route(),
2323
username: str = Json(min_str_length=5, blacklist="<>"),
2424
age: int = Json(min_int=18, max_int=99),
25-
nicknames: List[str] = Json(),
25+
nicknames: list[str] = Json(),
2626
date_of_birth: datetime = Json(),
2727
password_expiry: Optional[int] = Json(5),
2828
is_admin: bool = Query(False),
@@ -38,7 +38,7 @@ if __name__ == "__main__":
3838
## Usage
3939
To validate parameters with flask-parameter-validation, two conditions must be met.
4040
1. The `@ValidateParameters()` decorator must be applied to the function
41-
2. Type hints ([supported types](#type-hints-and-accepted-input-types)) and a default of a subclass of `Parameter` must be supplied per parameter flask-parameter-validation parameter
41+
2. Type hints ([supported types](#type-hints-and-accepted-input-types)) and a default of a subclass of `Parameter` must be supplied per parameter
4242

4343

4444
### Enable and customize Validation for a Route with the @ValidateParameters decorator
@@ -108,20 +108,20 @@ Note: "**POST Methods**" refers to the HTTP methods that send data in the reques
108108
#### Type Hints and Accepted Input Types
109109
Type Hints allow for inline specification of the input type of a parameter. Some types are only available to certain `Parameter` subclasses.
110110

111-
| Type Hint / Expected Python Type | Notes | `Route` | `Form` | `Json` | `Query` | `File` |
112-
|------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------|---------|--------|--------|---------|--------|
113-
| `str` | | Y | Y | Y | Y | N |
114-
| `int` | | Y | Y | Y | Y | N |
115-
| `bool` | | Y | Y | Y | Y | N |
116-
| `float` | | Y | Y | Y | Y | N |
117-
| `typing.List` (must not be `list`) | For `Query` and `Form` inputs, users can pass via either `value=1&value=2&value=3`, or `value=1,2,3`, both will be transformed to a `list`. | N | Y | Y | Y | N |
118-
| `typing.Union` | Cannot be used inside of `typing.List` | Y | Y | Y | Y | N |
119-
| `typing.Optional` | | Y | Y | Y | Y | Y |
120-
| `datetime.datetime` | Received as a `str` in ISO-8601 date-time format | Y | Y | Y | Y | N |
121-
| `datetime.date` | Received as a `str` in ISO-8601 full-date format | Y | Y | Y | Y | N |
122-
| `datetime.time` | Received as a `str` in ISO-8601 partial-time format | Y | Y | Y | Y | N |
123-
| `dict` | For `Query` and `Form` inputs, users should pass the stringified JSON | N | Y | Y | Y | N |
124-
| `FileStorage` | | N | N | N | N | Y |
111+
| Type Hint / Expected Python Type | Notes | `Route` | `Form` | `Json` | `Query` | `File` |
112+
|------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|--------|--------|---------|--------|
113+
| `str` | | Y | Y | Y | Y | N |
114+
| `int` | | Y | Y | Y | Y | N |
115+
| `bool` | | Y | Y | Y | Y | N |
116+
| `float` | | Y | Y | Y | Y | N |
117+
| `list`/`typing.List` (`typing.List` is [deprecated](https://docs.python.org/3/library/typing.html#typing.List)) | For `Query` and `Form` inputs, users can pass via either `value=1&value=2&value=3`, or `value=1,2,3`, both will be transformed to a `list` | N | Y | Y | Y | N |
118+
| `typing.Union` | Cannot be used inside of `typing.List` | Y | Y | Y | Y | N |
119+
| `typing.Optional` | | Y | Y | Y | Y | Y |
120+
| `datetime.datetime` | Received as a `str` in ISO-8601 date-time format | Y | Y | Y | Y | N |
121+
| `datetime.date` | Received as a `str` in ISO-8601 full-date format | Y | Y | Y | Y | N |
122+
| `datetime.time` | Received as a `str` in ISO-8601 partial-time format | Y | Y | Y | Y | N |
123+
| `dict` | For `Query` and `Form` inputs, users should pass the stringified JSON | N | Y | Y | Y | N |
124+
| `FileStorage` | | N | N | N | N | Y |
125125

126126
These can be used in tandem to describe a parameter to validate: `parameter_name: type_hint = ParameterSubclass()`
127127
- `parameter_name`: The field name itself, such as username
@@ -136,8 +136,8 @@ Validation beyond type-checking can be done by passing arguments into the constr
136136
| `default` | any | All | Specifies the default value for the field, makes non-Optional fields not required |
137137
| `min_str_length` | `int` | `str` | Specifies the minimum character length for a string input |
138138
| `max_str_length` | `int` | `str` | Specifies the maximum character length for a string input |
139-
| `min_list_length` | `int` | `typing.List` | Specifies the minimum number of elements in a list |
140-
| `max_list_length` | `int` | `typing.List` | Specifies the maximum number of elements in a list |
139+
| `min_list_length` | `int` | `list` | Specifies the minimum number of elements in a list |
140+
| `max_list_length` | `int` | `list` | Specifies the maximum number of elements in a list |
141141
| `min_int` | `int` | `int` | Specifies the minimum number for an integer input |
142142
| `max_int` | `int` | `int` | Specifies the maximum number for an integer input |
143143
| `whitelist` | `str` | `str` | A string containing allowed characters for the value |

flask_parameter_validation/parameter_validation.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
fn_list = dict()
1515

16+
list_type_hints = ["typing.List", "typing.Optional[typing.List", "list", "typing.Optional[list"]
1617

1718
class ValidateParameters:
1819
@classmethod
@@ -65,8 +66,7 @@ def nested_func_helper(**kwargs):
6566
# Step 3 - Extract list of parameters expected to be lists (otherwise all values are converted to lists)
6667
expected_list_params = []
6768
for name, param in expected_inputs.items():
68-
if str(param.annotation).startswith("typing.List") or str(param.annotation).startswith(
69-
"typing.Optional[typing.List"):
69+
if True in [str(param.annotation).startswith(list_hint) for list_hint in list_type_hints]:
7070
expected_list_params.append(param.default.alias or name)
7171

7272
# Step 4 - Convert request inputs to dicts
@@ -209,7 +209,7 @@ def validate(self, expected_input, all_request_inputs):
209209
user_inputs = [user_input]
210210
# If typing.List in union and user supplied valid list, convert remaining check only for list
211211
for exp_type in expected_input_types:
212-
if str(exp_type).startswith("typing.List"):
212+
if any(str(exp_type).startswith(list_hint) for list_hint in list_type_hints):
213213
if type(user_input) is list:
214214
# Only convert if validation passes
215215
if hasattr(exp_type, "__args__"):
@@ -219,7 +219,7 @@ def validate(self, expected_input, all_request_inputs):
219219
expected_input_type_str = str(exp_type)
220220
user_inputs = user_input
221221
# If list, expand inner typing items. Otherwise, convert to list to match anyway.
222-
elif expected_input_type_str.startswith("typing.List"):
222+
elif any(expected_input_type_str.startswith(list_hint) for list_hint in list_type_hints):
223223
expected_input_types = expected_input_type.__args__
224224
if type(user_input) is list:
225225
user_inputs = user_input
@@ -244,15 +244,15 @@ def validate(self, expected_input, all_request_inputs):
244244
)
245245

246246
# Validate that if lists are required, lists are given
247-
if expected_input_type_str.startswith("typing.List"):
247+
if any(expected_input_type_str.startswith(list_hint) for list_hint in list_type_hints):
248248
if type(user_input) is not list:
249249
validation_success = False
250250

251251
# Error if types don't match
252252
if not validation_success:
253253
if hasattr(
254254
original_expected_input_type, "__name__"
255-
) and not original_expected_input_type_str.startswith("typing."):
255+
) and not (original_expected_input_type_str.startswith("typing.") or original_expected_input_type_str.startswith("list")):
256256
type_name = original_expected_input_type.__name__
257257
else:
258258
type_name = original_expected_input_type_str
@@ -272,6 +272,6 @@ def validate(self, expected_input, all_request_inputs):
272272
raise ValidationError(str(e), expected_name, expected_input_type)
273273

274274
# Return input back to parent function
275-
if expected_input_type_str.startswith("typing.List"):
275+
if any(expected_input_type_str.startswith(list_hint) for list_hint in list_type_hints):
276276
return user_inputs
277277
return user_inputs[0]

flask_parameter_validation/test/test_form_params.py

+42
Original file line numberDiff line numberDiff line change
@@ -903,3 +903,45 @@ def test_max_list_length(client):
903903
# Test that above length yields error
904904
r = client.post(url, data={"v": ["the", "longest", "of", "lists"]})
905905
assert "error" in r.json
906+
907+
908+
def test_non_typing_list_str(client):
909+
url = "/form/list/non_typing"
910+
# Test that present single str input yields [input value]
911+
r = client.post(url, data={"v": "w"})
912+
assert "v" in r.json
913+
assert type(r.json["v"]) is list
914+
assert len(r.json["v"]) == 1
915+
assert type(r.json["v"][0]) is str
916+
assert r.json["v"][0] == "w"
917+
# Test that present CSV str input yields [input values]
918+
v = ["x", "y"]
919+
r = client.post(url, data={"v": v})
920+
assert "v" in r.json
921+
assert type(r.json["v"]) is list
922+
assert len(r.json["v"]) == 2
923+
list_assertion_helper(2, str, v, r.json["v"])
924+
# Test that missing input yields error
925+
r = client.post(url)
926+
assert "error" in r.json
927+
928+
def test_non_typing_optional_list_str(client):
929+
url = "/form/list/optional_non_typing"
930+
# Test that missing input yields None
931+
r = client.post(url)
932+
assert "v" in r.json
933+
assert r.json["v"] is None
934+
# Test that present str input yields [input value]
935+
r = client.post(url, data={"v": "test"})
936+
assert "v" in r.json
937+
assert type(r.json["v"]) is list
938+
assert len(r.json["v"]) == 1
939+
assert type(r.json["v"][0]) is str
940+
assert r.json["v"][0] == "test"
941+
# Test that present CSV str input yields [input values]
942+
v = ["two", "tests"]
943+
r = client.post(url, data={"v": v})
944+
assert "v" in r.json
945+
assert type(r.json["v"]) is list
946+
assert len(r.json["v"]) == 2
947+
list_assertion_helper(2, str, v, r.json["v"])

flask_parameter_validation/test/test_json_params.py

+30-1
Original file line numberDiff line numberDiff line change
@@ -1034,4 +1034,33 @@ def test_dict_json_schema(client):
10341034
"last_name": "Doe"
10351035
}
10361036
r = client.post(url, json={"v": v})
1037-
assert "error" in r.json
1037+
assert "error" in r.json
1038+
1039+
1040+
def test_non_typing_list_str(client):
1041+
url = "/json/list/non_typing"
1042+
# Test that present list[str] input yields [input values]
1043+
v = ["x", "y"]
1044+
r = client.post(url, json={"v": v})
1045+
assert "v" in r.json
1046+
assert type(r.json["v"]) is list
1047+
assert len(r.json["v"]) == 2
1048+
list_assertion_helper(2, str, v, r.json["v"])
1049+
# Test that missing input yields error
1050+
r = client.post(url)
1051+
assert "error" in r.json
1052+
1053+
1054+
def test_non_typing_optional_list_str(client):
1055+
url = "/json/list/optional_non_typing"
1056+
# Test that missing input yields None
1057+
r = client.post(url)
1058+
assert "v" in r.json
1059+
assert r.json["v"] is None
1060+
# Test that present list[str] input yields [input values]
1061+
v = ["two", "tests"]
1062+
r = client.post(url, json={"v": v})
1063+
assert "v" in r.json
1064+
assert type(r.json["v"]) is list
1065+
assert len(r.json["v"]) == 2
1066+
list_assertion_helper(2, str, v, r.json["v"])

0 commit comments

Comments
 (0)