From f790efb4949227c782545bb1cd3959ba018237d0 Mon Sep 17 00:00:00 2001 From: Willow Rimlinger Date: Thu, 13 Nov 2025 15:16:09 -0500 Subject: [PATCH 1/7] Type check TypedDicts --- .../parameter_validation.py | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/flask_parameter_validation/parameter_validation.py b/flask_parameter_validation/parameter_validation.py index 83ac50d..f6a31d5 100644 --- a/flask_parameter_validation/parameter_validation.py +++ b/flask_parameter_validation/parameter_validation.py @@ -6,7 +6,7 @@ import re import uuid from inspect import signature -from typing import Optional, Union, get_origin, get_args, Any +from typing import NotRequired, Optional, Required, Union, get_origin, get_args, Any, is_typeddict import flask from flask import request @@ -20,10 +20,13 @@ fn_list = dict() # from 3.10 onwards, Unions written X | Y have the type UnionType +# from 3.10 onwards, we get the is_typeddict function so we can support TypedDicts UNION_TYPES = [Union] +HANDLE_TYPEDDICTS = False if sys.version_info >= (3, 10): from types import UnionType UNION_TYPES = [Union, UnionType] + HANDLE_TYPEDDICTS = True class ValidateParameters: @classmethod @@ -217,6 +220,40 @@ def _generic_types_validation_helper(self, converted_list.append(sub_converted_input) return converted_list, True + # typeddict + elif HANDLE_TYPEDDICTS and is_typeddict(expected_input_type): + # check for a stringified dict (like from Query) + if type(user_input) is str: + try: + user_input = json.loads(user_input) + except ValueError: + return user_input, False + if type(user_input) is not dict: + return user_input, False + # check that we have all required keys + for key in expected_input_type.__required_keys__: + if key not in user_input: + return user_input, False + + # process + converted_dict = {} + # go through each user input key and make sure the value is the correct type + for key, value in user_input.items(): + annotations = inspect.get_annotations(expected_input_type) + if key not in annotations: + # we are strict in not allowing extra keys + # if you want extra keys, use NotRequired + return user_input, False + # get the Required and NotRequired decorators out of the way, if present + annotation_type = annotations[key] + if get_origin(annotation_type) is NotRequired or get_origin(annotation_type) is Required: + annotation_type = get_args(annotation_type)[0] + sub_converted_input, sub_success = self._generic_types_validation_helper(expected_name, annotation_type, value, source) + if not sub_success: + return user_input, False + converted_dict[key] = sub_converted_input + return converted_dict, True + # dict elif get_origin(expected_input_type) is dict or expected_input_type is dict: # check for a stringified dict (like from Query or Form) From a74c89fad2c821b1e539e61bfae0ad3018501b51 Mon Sep 17 00:00:00 2001 From: Willow Rimlinger Date: Tue, 18 Nov 2025 17:46:03 -0500 Subject: [PATCH 2/7] Add tests for typeddicts --- .../test/test_form_params.py | 253 ++++++++++++++++++ .../test/test_json_params.py | 253 ++++++++++++++++++ .../test/test_multi_source_params.py | 22 ++ .../test/test_query_params.py | 253 ++++++++++++++++++ .../multi_source_blueprint.py | 17 +- .../testing_blueprints/parameter_blueprint.py | 2 + .../testing_blueprints/typeddict_blueprint.py | 217 +++++++++++++++ 7 files changed, 1016 insertions(+), 1 deletion(-) create mode 100644 flask_parameter_validation/test/testing_blueprints/typeddict_blueprint.py diff --git a/flask_parameter_validation/test/test_form_params.py b/flask_parameter_validation/test/test_form_params.py index 4cf6a66..1033bd8 100644 --- a/flask_parameter_validation/test/test_form_params.py +++ b/flask_parameter_validation/test/test_form_params.py @@ -1741,4 +1741,257 @@ def test_dict_args_str_list_3_10_union(client): r = client.post(url, data={"v": json.dumps(d)}) assert "error" in r.json + def test_typeddict_normal(client): + url = "/form/typeddict/" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json + # Test that incorrect values yields error + d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json + + def test_typeddict_functional(client): + url = "/form/typeddict/functional" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json + # Test that incorrect values yields error + d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json + + def test_typeddict_optional(client): + url = "/form/typeddict/optional" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that no input yields input value + d = None + r = client.post(url, data={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json + # Test that empty dict yields error + d = {} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json + + def test_typeddict_union_optional(client): + url = "/form/typeddict/union_optional" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that no input yields input value + d = None + r = client.post(url, data={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json + # Test that empty dict yields error + d = {} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json + + def test_typeddict_default(client): + url = "/form/typeddict/default" + # Test that missing input for required and optional yields default values + r = client.post(url) + assert "n_opt" in r.json + assert r.json["n_opt"] == {"id": 1, "name": "Bob", "timestamp": datetime.datetime(2025, 11, 18, 0, 0).isoformat()} + assert "opt" in r.json + assert r.json["opt"] == {"id": 2, "name": "Billy", "timestamp": datetime.datetime(2025, 11, 18, 5, 30).isoformat()} + # Test that present TypedDict input for required and optional yields input values + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={ + "opt": json.dumps(d), + "n_opt": json.dumps(d), + }) + assert "opt" in r.json + assert r.json["opt"] == d + assert "n_opt" in r.json + assert r.json["n_opt"] == d + # Test that present non-TypedDict input for required yields error + r = client.post(url, data={"opt": {"id": 3}, "n_opt": "b"}) + assert "error" in r.json + + def test_typeddict_func(client): + url = "/form/typeddict/func" + # Test that correct input yields input value + d = {"id": 3, "name": "Bill", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that func failing input yields input value + d = {"id": 3, "name": "Billy Bob Joe", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json + + def test_typeddict_json_schema(client): + url = "/form/typeddict/json_schema" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json + # Test that incorrect values yields error + d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json + + def test_typeddict_not_required(client): + url = "/form/typeddict/not_required" + # Test that all keys yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing not requried key yields input value + d = {"name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing required keys yields error + d = {"name": "Merriweather"} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json + + def test_typeddict_required(client): + url = "/form/typeddict/required" + # Test that all keys yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing not requried key yields input value + d = {"name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing required keys yields error + d = {"name": "Merriweather"} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json + + def test_typeddict_complex(client): + url = "/form/typeddict/complex" + # Test that correct input yields input value + d = { + "name": "change da world", + "children": [ + { + "id": 4, + "name": "my final message. Goodb ye", + "timestamp": datetime.datetime.now().isoformat(), + } + ], + "left": { + "x": 3.4, + "y": 1.0, + "z": 99999.34455663 + }, + "right": { + "x": 3.2, + "y": 1.1, + "z": 999.3663 + }, + } + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that empty children list yields input value + d = { + "name": "change da world", + "children": [], + "left": { + "x": 3.4, + "y": 1.0, + "z": 99999.34455663 + }, + "right": { + "x": 3.2, + "y": 1.1, + "z": 999.3663 + }, + } + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that incorrect child TypedDict yields error + d = { + "name": "change da world", + "children": [ + { + "id": 4, + "name": 6, + "timestamp": datetime.datetime.now().isoformat(), + } + ], + "left": { + "x": 3.4, + "y": 1.0, + "z": 99999.34455663 + }, + "right": { + "x": 3.2, + "y": 1.1, + "z": 999.3663 + }, + } + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json + # Test that omitting NotRequired key in child yields input value + d = { + "name": "tags", + "children": [ + { + "id": 4, + "name": "ice my wrist", + "timestamp": datetime.datetime.now().isoformat(), + } + ], + "left": { + "x": 3.4, + "y": 1.0, + "z": 99999.34455663 + }, + "right": { + "x": 3.2, + "y": 1.1, + "z": 999.3663 + }, + } + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that incorrect values yields error + d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json diff --git a/flask_parameter_validation/test/test_json_params.py b/flask_parameter_validation/test/test_json_params.py index 0131035..d92531b 100644 --- a/flask_parameter_validation/test/test_json_params.py +++ b/flask_parameter_validation/test/test_json_params.py @@ -1985,4 +1985,257 @@ def test_dict_args_str_list_3_10_union(client): r = client.post(url, json={"v": d}) assert "error" in r.json + def test_typeddict_normal(client): + url = "/json/typeddict/" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.post(url, json={"v": d}) + assert "error" in r.json + # Test that incorrect values yields error + d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "error" in r.json + + def test_typeddict_functional(client): + url = "/json/typeddict/functional" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.post(url, json={"v": d}) + assert "error" in r.json + # Test that incorrect values yields error + d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "error" in r.json + + def test_typeddict_optional(client): + url = "/json/typeddict/optional" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that no input yields input value + d = None + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.post(url, json={"v": d}) + assert "error" in r.json + # Test that empty dict yields error + d = {} + r = client.post(url, json={"v": d}) + assert "error" in r.json + + def test_typeddict_union_optional(client): + url = "/json/typeddict/union_optional" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that no input yields input value + d = None + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.post(url, json={"v": d}) + assert "error" in r.json + # Test that empty dict yields error + d = {} + r = client.post(url, json={"v": d}) + assert "error" in r.json + + def test_typeddict_default(client): + url = "/json/typeddict/default" + # Test that missing input for required and optional yields default values + r = client.post(url) + assert "n_opt" in r.json + assert r.json["n_opt"] == {"id": 1, "name": "Bob", "timestamp": datetime.datetime(2025, 11, 18, 0, 0).isoformat()} + assert "opt" in r.json + assert r.json["opt"] == {"id": 2, "name": "Billy", "timestamp": datetime.datetime(2025, 11, 18, 5, 30).isoformat()} + # Test that present TypedDict input for required and optional yields input values + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={ + "opt": d, + "n_opt": d, + }) + assert "opt" in r.json + assert r.json["opt"] == d + assert "n_opt" in r.json + assert r.json["n_opt"] == d + # Test that present non-TypedDict input for required yields error + r = client.post(url, json={"opt": {"id": 3}, "n_opt": "b"}) + assert "error" in r.json + + def test_typeddict_func(client): + url = "/json/typeddict/func" + # Test that correct input yields input value + d = {"id": 3, "name": "Bill", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that func failing input yields input value + d = {"id": 3, "name": "Billy Bob Joe", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "error" in r.json + + def test_typeddict_json_schema(client): + url = "/json/typeddict/json_schema" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.post(url, json={"v": d}) + assert "error" in r.json + # Test that incorrect values yields error + d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "error" in r.json + + def test_typeddict_not_required(client): + url = "/json/typeddict/not_required" + # Test that all keys yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing not requried key yields input value + d = {"name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing required keys yields error + d = {"name": "Merriweather"} + r = client.post(url, json={"v": d}) + assert "error" in r.json + + def test_typeddict_required(client): + url = "/json/typeddict/required" + # Test that all keys yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing not requried key yields input value + d = {"name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing required keys yields error + d = {"name": "Merriweather"} + r = client.post(url, json={"v": d}) + assert "error" in r.json + + def test_typeddict_complex(client): + url = "/json/typeddict/complex" + # Test that correct input yields input value + d = { + "name": "change da world", + "children": [ + { + "id": 4, + "name": "my final message. Goodb ye", + "timestamp": datetime.datetime.now().isoformat(), + } + ], + "left": { + "x": 3.4, + "y": 1.0, + "z": 99999.34455663 + }, + "right": { + "x": 3.2, + "y": 1.1, + "z": 999.3663 + }, + } + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that empty children list yields input value + d = { + "name": "change da world", + "children": [], + "left": { + "x": 3.4, + "y": 1.0, + "z": 99999.34455663 + }, + "right": { + "x": 3.2, + "y": 1.1, + "z": 999.3663 + }, + } + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that incorrect child TypedDict yields error + d = { + "name": "change da world", + "children": [ + { + "id": 4, + "name": 6, + "timestamp": datetime.datetime.now().isoformat(), + } + ], + "left": { + "x": 3.4, + "y": 1.0, + "z": 99999.34455663 + }, + "right": { + "x": 3.2, + "y": 1.1, + "z": 999.3663 + }, + } + r = client.post(url, json={"v": d}) + assert "error" in r.json + # Test that omitting NotRequired key in child yields input value + d = { + "name": "tags", + "children": [ + { + "id": 4, + "name": "ice my wrist", + "timestamp": datetime.datetime.now().isoformat(), + } + ], + "left": { + "x": 3.4, + "y": 1.0, + "z": 99999.34455663 + }, + "right": { + "x": 3.2, + "y": 1.1, + "z": 999.3663 + }, + } + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that incorrect values yields error + d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "error" in r.json diff --git a/flask_parameter_validation/test/test_multi_source_params.py b/flask_parameter_validation/test/test_multi_source_params.py index bc4ff41..adfcf37 100644 --- a/flask_parameter_validation/test/test_multi_source_params.py +++ b/flask_parameter_validation/test/test_multi_source_params.py @@ -769,3 +769,25 @@ def test_multi_source_dict_args_str_list_3_10_union(client, source_a, source_b): r = client.get(url) assert "error" in r.json + @pytest.mark.parametrize(*common_parameters) + def test_multi_source_typeddict(client, source_a, source_b): + if source_a == source_b or "route" in [source_a, source_b]: # Duplicate sources shouldn't be something someone does, so we won't test for it, Route does not support parameters of type 'dict' + return + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + url = f"/ms_{source_a}_{source_b}/typeddict/" + for source in [source_a, source_b]: + # Test that present input yields input value + r = None + if source == "query": + r = client.get(url, query_string={"v": json.dumps(d)}) + elif source == "form": + r = client.get(url, data={"v": json.dumps(d)}) + elif source == "json": + r = client.get(url, json={"v": d}) + assert r is not None + assert "v" in r.json + assert r.json["v"] == d + # Test that missing input yields error + r = client.get(url) + assert "error" in r.json + diff --git a/flask_parameter_validation/test/test_query_params.py b/flask_parameter_validation/test/test_query_params.py index 7c0b3eb..c774be5 100644 --- a/flask_parameter_validation/test/test_query_params.py +++ b/flask_parameter_validation/test/test_query_params.py @@ -2850,4 +2850,257 @@ def test_dict_args_str_list_3_10_union(client): r = client.get(url, query_string={"v": json.dumps(d)}) assert "error" in r.json + def test_typeddict_normal(client): + url = "/query/typeddict/" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json + # Test that incorrect values yields error + d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json + + def test_typeddict_functional(client): + url = "/query/typeddict/functional" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json + # Test that incorrect values yields error + d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json + + def test_typeddict_optional(client): + url = "/query/typeddict/optional" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that no input yields input value + d = None + r = client.get(url, query_string={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json + # Test that empty dict yields error + d = {} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json + + def test_typeddict_union_optional(client): + url = "/query/typeddict/union_optional" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that no input yields input value + d = None + r = client.get(url, query_string={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json + # Test that empty dict yields error + d = {} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json + + def test_typeddict_default(client): + url = "/query/typeddict/default" + # Test that missing input for required and optional yields default values + r = client.get(url) + assert "n_opt" in r.json + assert r.json["n_opt"] == {"id": 1, "name": "Bob", "timestamp": datetime.datetime(2025, 11, 18, 0, 0).isoformat()} + assert "opt" in r.json + assert r.json["opt"] == {"id": 2, "name": "Billy", "timestamp": datetime.datetime(2025, 11, 18, 5, 30).isoformat()} + # Test that present TypedDict input for required and optional yields input values + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={ + "opt": json.dumps(d), + "n_opt": json.dumps(d), + }) + assert "opt" in r.json + assert r.json["opt"] == d + assert "n_opt" in r.json + assert r.json["n_opt"] == d + # Test that present non-TypedDict input for required yields error + r = client.get(url, query_string={"opt": {"id": 3}, "n_opt": "b"}) + assert "error" in r.json + + def test_typeddict_func(client): + url = "/query/typeddict/func" + # Test that correct input yields input value + d = {"id": 3, "name": "Bill", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that func failing input yields input value + d = {"id": 3, "name": "Billy Bob Joe", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json + + def test_typeddict_json_schema(client): + url = "/query/typeddict/json_schema" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json + # Test that incorrect values yields error + d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json + + def test_typeddict_not_required(client): + url = "/query/typeddict/not_required" + # Test that all keys yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing not requried key yields input value + d = {"name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing required keys yields error + d = {"name": "Merriweather"} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json + + def test_typeddict_required(client): + url = "/query/typeddict/required" + # Test that all keys yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing not requried key yields input value + d = {"name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing required keys yields error + d = {"name": "Merriweather"} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json + + def test_typeddict_complex(client): + url = "/query/typeddict/complex" + # Test that correct input yields input value + d = { + "name": "change da world", + "children": [ + { + "id": 4, + "name": "my final message. Goodb ye", + "timestamp": datetime.datetime.now().isoformat(), + } + ], + "left": { + "x": 3.4, + "y": 1.0, + "z": 99999.34455663 + }, + "right": { + "x": 3.2, + "y": 1.1, + "z": 999.3663 + }, + } + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that empty children list yields input value + d = { + "name": "change da world", + "children": [], + "left": { + "x": 3.4, + "y": 1.0, + "z": 99999.34455663 + }, + "right": { + "x": 3.2, + "y": 1.1, + "z": 999.3663 + }, + } + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that incorrect child TypedDict yields error + d = { + "name": "change da world", + "children": [ + { + "id": 4, + "name": 6, + "timestamp": datetime.datetime.now().isoformat(), + } + ], + "left": { + "x": 3.4, + "y": 1.0, + "z": 99999.34455663 + }, + "right": { + "x": 3.2, + "y": 1.1, + "z": 999.3663 + }, + } + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json + # Test that omitting NotRequired key in child yields input value + d = { + "name": "tags", + "children": [ + { + "id": 4, + "name": "ice my wrist", + "timestamp": datetime.datetime.now().isoformat(), + } + ], + "left": { + "x": 3.4, + "y": 1.0, + "z": 99999.34455663 + }, + "right": { + "x": 3.2, + "y": 1.1, + "z": 999.3663 + }, + } + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that incorrect values yields error + d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json diff --git a/flask_parameter_validation/test/testing_blueprints/multi_source_blueprint.py b/flask_parameter_validation/test/testing_blueprints/multi_source_blueprint.py index d57dfc4..ee2c6ad 100644 --- a/flask_parameter_validation/test/testing_blueprints/multi_source_blueprint.py +++ b/flask_parameter_validation/test/testing_blueprints/multi_source_blueprint.py @@ -1,7 +1,7 @@ import sys import datetime import uuid -from typing import Optional, List, Union +from typing import Optional, List, Union, TypedDict, NotRequired, Required from flask import Blueprint, jsonify @@ -238,5 +238,20 @@ def multi_source_dict_str_list_3_10_union(v: dict[str, Union[list[int], bool]] = assert type(ele) is int return jsonify({"v": v}) + class Simple(TypedDict): + id: int + name: str + timestamp: datetime.datetime + + @param_bp.route("/typeddict/", methods=["GET", "POST"]) + @ValidateParameters() + def multi_source_typeddict_normal(v: Simple = MultiSource(sources[0], sources[1], list_disable_query_csv=True)): + assert type(v) is dict + assert "id" in v and "name" in v and "timestamp" in v + assert type(v["id"]) is int + assert type(v["name"]) is str + assert type(v["timestamp"]) is datetime.datetime + v["timestamp"] = v["timestamp"].isoformat() + return jsonify({"v": v}) return param_bp diff --git a/flask_parameter_validation/test/testing_blueprints/parameter_blueprint.py b/flask_parameter_validation/test/testing_blueprints/parameter_blueprint.py index 9a55876..d9fc013 100644 --- a/flask_parameter_validation/test/testing_blueprints/parameter_blueprint.py +++ b/flask_parameter_validation/test/testing_blueprints/parameter_blueprint.py @@ -14,6 +14,7 @@ from flask_parameter_validation.test.testing_blueprints.time_blueprint import get_time_blueprint from flask_parameter_validation.test.testing_blueprints.union_blueprint import get_union_blueprint from flask_parameter_validation.test.testing_blueprints.uuid_blueprint import get_uuid_blueprint +from flask_parameter_validation.test.testing_blueprints.typeddict_blueprint import get_typeddict_blueprint def get_parameter_blueprint(ParamType: type[Parameter], bp_name: str, param_name: str, http_verb: str) -> Blueprint: @@ -33,4 +34,5 @@ def get_parameter_blueprint(ParamType: type[Parameter], bp_name: str, param_name param_bp.register_blueprint(get_enum_blueprint(ParamType, f"{bp_name}_str_enum", http_verb, Fruits, "str_enum")) param_bp.register_blueprint(get_enum_blueprint(ParamType, f"{bp_name}_int_enum", http_verb, Binary, "int_enum")) param_bp.register_blueprint(get_uuid_blueprint(ParamType, f"{bp_name}_uuid", http_verb)) + param_bp.register_blueprint(get_typeddict_blueprint(ParamType, f"{bp_name}_typeddict", http_verb)) return param_bp diff --git a/flask_parameter_validation/test/testing_blueprints/typeddict_blueprint.py b/flask_parameter_validation/test/testing_blueprints/typeddict_blueprint.py new file mode 100644 index 0000000..d82e43f --- /dev/null +++ b/flask_parameter_validation/test/testing_blueprints/typeddict_blueprint.py @@ -0,0 +1,217 @@ +import datetime +import sys +from typing import NotRequired, Optional, Required, TypedDict, is_typeddict + +from flask import Blueprint, jsonify + +from flask_parameter_validation import ValidateParameters +from flask_parameter_validation.parameter_types.parameter import Parameter + + +def get_typeddict_blueprint(ParamType: type[Parameter], bp_name: str, http_verb: str) -> Blueprint: + typeddict_bp = Blueprint(bp_name, __name__, url_prefix="/typeddict") + decorator = getattr(typeddict_bp, http_verb) + + if sys.version_info < (3, 10): + return typeddict_bp + + # TypedDict not currently supported by Route + # def path(base: str, route_additions: str) -> str: + # return base + (route_additions if ParamType is Route else "") + + class Simple(TypedDict): + id: int + name: str + timestamp: datetime.datetime + + @decorator("/") + @ValidateParameters() + def normal(v: Simple = ParamType(list_disable_query_csv=True)): + assert type(v) is dict + assert "id" in v and "name" in v and "timestamp" in v + assert type(v["id"]) is int + assert type(v["name"]) is str + assert type(v["timestamp"]) is datetime.datetime + v["timestamp"] = v["timestamp"].isoformat() + return jsonify({"v": v}) + + SimpleFunc = TypedDict("SimpleFunc", {"id": int, "name": str, "timestamp": datetime.datetime}) + + @decorator("/functional") + @ValidateParameters() + def functional(v: SimpleFunc = ParamType(list_disable_query_csv=True)): + assert type(v) is dict + assert "id" in v and "name" in v and "timestamp" in v + assert type(v["id"]) is int + assert type(v["name"]) is str + assert type(v["timestamp"]) is datetime.datetime + v["timestamp"] = v["timestamp"].isoformat() + return jsonify({"v": v}) + + @decorator("/optional") + @ValidateParameters() + def optional(v: Optional[Simple] = ParamType(list_disable_query_csv=True)): + if v is not None: + assert type(v) is dict + assert "id" in v and "name" in v and "timestamp" in v + assert type(v["id"]) is int + assert type(v["name"]) is str + assert type(v["timestamp"]) is datetime.datetime + v["timestamp"] = v["timestamp"].isoformat() + return jsonify({"v": v}) + + @decorator("/union_optional") + @ValidateParameters() + def union_optional(v: Simple | None = ParamType(list_disable_query_csv=True)): + if v is not None: + assert type(v) is dict + assert "id" in v and "name" in v and "timestamp" in v + assert type(v["id"]) is int + assert type(v["name"]) is str + assert type(v["timestamp"]) is datetime.datetime + v["timestamp"] = v["timestamp"].isoformat() + return jsonify({"v": v}) + + @decorator("/default") + @ValidateParameters() + def decorator_default( + n_opt: Simple = ParamType(default={"id": 1, "name": "Bob", "timestamp": datetime.datetime(2025, 11, 18, 0, 0)}, list_disable_query_csv=True), + opt: Optional[Simple] = ParamType(default={"id": 2, "name": "Billy", "timestamp": datetime.datetime(2025, 11, 18, 5, 30)}, list_disable_query_csv=True) + ): + assert type(n_opt) is dict + assert "id" in n_opt and "name" in n_opt and "timestamp" in n_opt + assert type(n_opt["id"]) is int + assert type(n_opt["name"]) is str + assert type(n_opt["timestamp"]) is datetime.datetime + if opt is not None: + assert type(opt) is dict + assert "id" in opt and "name" in opt and "timestamp" in opt + assert type(opt["id"]) is int + assert type(opt["name"]) is str + assert type(opt["timestamp"]) is datetime.datetime + opt["timestamp"] = opt["timestamp"].isoformat() + n_opt["timestamp"] = n_opt["timestamp"].isoformat() + return jsonify({ + "n_opt": n_opt, + "opt": opt + }) + + def is_name_short(v): + assert type(v) is dict + assert "name" in v + return len(v["name"]) <= 4 + + @decorator("/func") + @ValidateParameters() + def func(v: Simple = ParamType(func=is_name_short, list_disable_query_csv=True)): + assert type(v) is dict + assert "id" in v and "name" in v and "timestamp" in v + assert type(v["id"]) is int + assert type(v["name"]) is str + assert type(v["timestamp"]) is datetime.datetime + assert len(v["name"]) <= 4 + v["timestamp"] = v["timestamp"].isoformat() + return jsonify({"v": v}) + + json_schema = { + "type": "object", + "required": ["id", "name", "timestamp"], + "properties": { + "id": {"type": "integer"}, + "name": {"type": "string"}, + "last_name": {"type": "string"}, + } + } + + @decorator("/json_schema") + @ValidateParameters() + def json_schema(v: SimpleFunc = ParamType(json_schema=json_schema, list_disable_query_csv=True)): + assert type(v) is dict + assert "id" in v and "name" in v and "timestamp" in v + assert type(v["id"]) is int + assert type(v["name"]) is str + assert type(v["timestamp"]) is datetime.datetime + v["timestamp"] = v["timestamp"].isoformat() + return jsonify({"v": v}) + + class SimpleNotRequired(TypedDict): + id: NotRequired[int] + name: str + timestamp: datetime.datetime + + @decorator("/not_required") + @ValidateParameters() + def not_required(v: SimpleNotRequired = ParamType(list_disable_query_csv=True)): + assert type(v) is dict + assert "name" in v and "timestamp" in v + assert type(v["name"]) is str + assert type(v["timestamp"]) is datetime.datetime + if "id" in v: + assert type(v["id"]) is int + v["timestamp"] = v["timestamp"].isoformat() + return jsonify({"v": v}) + + class SimpleRequired(TypedDict, total=False): + id: int + name: Required[str] + timestamp: Required[datetime.datetime] + + @decorator("/required") + @ValidateParameters() + def required(v: SimpleRequired = ParamType(list_disable_query_csv=True)): + assert type(v) is dict + assert "name" in v and "timestamp" in v + assert type(v["name"]) is str + assert type(v["timestamp"]) is datetime.datetime + if "id" in v: + assert type(v["id"]) is int + v["timestamp"] = v["timestamp"].isoformat() + return jsonify({"v": v}) + + class Coord(TypedDict): + x: float + y: float + z: float + id: NotRequired[int] + + class Complex(TypedDict): + children: list[Simple] + left: Coord + right: Coord + name: str + + @decorator("/complex") + @ValidateParameters() + def complex(v: Complex = ParamType(list_disable_query_csv=True)): + assert type(v) is dict + assert "children" in v and "left" in v and "right" in v and "name" in v + assert type(v["name"]) is str + assert type(v["left"]) is dict + assert type(v["right"]) is dict + assert type(v["children"]) is list + new_children = [] + for ele in v["children"]: + assert type(v) is dict + assert "id" in ele and "name" in ele and "timestamp" in ele + assert type(ele["id"]) is int + assert type(ele["name"]) is str + assert type(ele["timestamp"]) is datetime.datetime + ele["timestamp"] = ele["timestamp"].isoformat() + new_children.append(ele) + v["children"] = new_children + assert "x" in v["left"] and "y" in v["left"] and "z" in v["left"] + assert type(v["left"]["x"]) is float + assert type(v["left"]["y"]) is float + assert type(v["left"]["z"]) is float + if "id" in v["left"]: + assert type(id) is int + assert "x" in v["right"] and "y" in v["right"] and "z" in v["right"] + assert type(v["right"]["x"]) is float + assert type(v["right"]["y"]) is float + assert type(v["right"]["z"]) is float + if "id" in v["right"]: + assert type(id) is int + return jsonify({"v": v}) + + return typeddict_bp + From a26dc50b2a57e119cce5e63e2780ca9841687085 Mon Sep 17 00:00:00 2001 From: Willow Rimlinger Date: Wed, 19 Nov 2025 17:19:21 -0500 Subject: [PATCH 3/7] Update README --- README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b12f6ff..aa78454 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ## Usage Example ```py from flask import Flask -from typing import Optional +from typing import Optional, TypedDict, NotRequired from flask_parameter_validation import ValidateParameters, Route, Json, Query from datetime import datetime from enum import Enum @@ -23,6 +23,11 @@ class UserType(str, Enum): # In Python 3.11 or later, subclass StrEnum from enu USER = "user" SERVICE = "service" +class SocialLink(TypedDict): + friendly_name: str + url: str + icon: NotRequired[str] + app = Flask(__name__) @app.route("/update/", methods=["POST"]) @@ -37,7 +42,8 @@ def hello( is_admin: bool = Query(False), user_type: UserType = Json(alias="type"), status: AccountStatus = Json(), - permissions: dict[str, str] = Query(list_disable_query_csv=True) + permissions: dict[str, str] = Query(list_disable_query_csv=True), + socials: list[SocialLink] = Json() ): return "Hello World!" @@ -131,7 +137,8 @@ Type Hints allow for inline specification of the input type of a parameter. Some | `datetime.datetime` | Received as a `str` in ISO-8601 date-time format | Y | Y | Y | Y | N | | `datetime.date` | Received as a `str` in ISO-8601 full-date format | Y | Y | Y | Y | N | | `datetime.time` | Received as a `str` in ISO-8601 partial-time format | Y | Y | Y | Y | N | -| `dict` | For `Query` and `Form` inputs, users should pass the stringified JSON. For `Query`, you likely will need to use `list_disable_query_csv=True`. | N | Y | Y | Y | N | +| `dict` | For `Query` and `Form` inputs, users should pass the stringified JSON. For `Query`, you likely will need to use `list_disable_query_csv=True`. | N | Y | Y | Y | N | +| `TypedDict` | For `Query` and `Form` inputs, users should pass the stringified JSON. For `Query`, you likely will need to use `list_disable_query_csv=True`. | N | Y | Y | Y | N | | `FileStorage` | | N | N | N | N | Y | | A subclass of `StrEnum` or `IntEnum`, or a subclass of `Enum` with `str` or `int` mixins prior to Python 3.11 | | Y | Y | Y | Y | N | | `uuid.UUID` | Received as a `str` with or without hyphens, case-insensitive | Y | Y | Y | Y | N | From 463e1ee74d0157f99ac4e7ef1036dde9ada5d17f Mon Sep 17 00:00:00 2001 From: Willow Rimlinger Date: Wed, 19 Nov 2025 17:40:38 -0500 Subject: [PATCH 4/7] Hide post-3.10 imports behind if statement --- flask_parameter_validation/parameter_validation.py | 4 +++- .../test/testing_blueprints/multi_source_blueprint.py | 4 +++- .../test/testing_blueprints/typeddict_blueprint.py | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/flask_parameter_validation/parameter_validation.py b/flask_parameter_validation/parameter_validation.py index f6a31d5..c7ead42 100644 --- a/flask_parameter_validation/parameter_validation.py +++ b/flask_parameter_validation/parameter_validation.py @@ -6,7 +6,9 @@ import re import uuid from inspect import signature -from typing import NotRequired, Optional, Required, Union, get_origin, get_args, Any, is_typeddict +from typing import Optional, Union, get_origin, get_args, Any +if sys.version_info >= (3, 10): + from typing import NotRequired, Required, is_typeddict import flask from flask import request diff --git a/flask_parameter_validation/test/testing_blueprints/multi_source_blueprint.py b/flask_parameter_validation/test/testing_blueprints/multi_source_blueprint.py index ee2c6ad..c34ed97 100644 --- a/flask_parameter_validation/test/testing_blueprints/multi_source_blueprint.py +++ b/flask_parameter_validation/test/testing_blueprints/multi_source_blueprint.py @@ -1,7 +1,9 @@ import sys import datetime import uuid -from typing import Optional, List, Union, TypedDict, NotRequired, Required +from typing import Optional, List, Union, TypedDict +if sys.version_info >= (3, 10): + from typing import NotRequired, Required from flask import Blueprint, jsonify diff --git a/flask_parameter_validation/test/testing_blueprints/typeddict_blueprint.py b/flask_parameter_validation/test/testing_blueprints/typeddict_blueprint.py index d82e43f..218ce37 100644 --- a/flask_parameter_validation/test/testing_blueprints/typeddict_blueprint.py +++ b/flask_parameter_validation/test/testing_blueprints/typeddict_blueprint.py @@ -1,6 +1,8 @@ import datetime import sys -from typing import NotRequired, Optional, Required, TypedDict, is_typeddict +from typing import Optional, TypedDict +if sys.version_info >= (3, 10): + from typing import NotRequired, Required, is_typeddict from flask import Blueprint, jsonify From 2360fab529fe97f4836def1ea49e1b1f4650d488 Mon Sep 17 00:00:00 2001 From: Willow Rimlinger Date: Wed, 19 Nov 2025 17:43:29 -0500 Subject: [PATCH 5/7] Put pytest in requirements.txt --- flask_parameter_validation/test/requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flask_parameter_validation/test/requirements.txt b/flask_parameter_validation/test/requirements.txt index b2bbede..3e574ab 100644 --- a/flask_parameter_validation/test/requirements.txt +++ b/flask_parameter_validation/test/requirements.txt @@ -1,3 +1,4 @@ Flask==3.0.2 ../../ -requests \ No newline at end of file +requests +pytest \ No newline at end of file From f3f6cc77489f9a5f7dc851fdcec7cfc9fa1d1915 Mon Sep 17 00:00:00 2001 From: Willow Rimlinger Date: Thu, 20 Nov 2025 10:44:00 -0500 Subject: [PATCH 6/7] Add TypedDict support for 3.9 using typing_extensions --- .../parameter_validation.py | 16 +- .../test/test_form_params.py | 449 +++++++++--------- .../test/test_json_params.py | 449 +++++++++--------- .../test/test_multi_source_params.py | 42 +- .../test/test_query_params.py | 449 +++++++++--------- .../multi_source_blueprint.py | 35 +- .../testing_blueprints/typeddict_blueprint.py | 35 +- 7 files changed, 741 insertions(+), 734 deletions(-) diff --git a/flask_parameter_validation/parameter_validation.py b/flask_parameter_validation/parameter_validation.py index c7ead42..69b82b8 100644 --- a/flask_parameter_validation/parameter_validation.py +++ b/flask_parameter_validation/parameter_validation.py @@ -6,9 +6,7 @@ import re import uuid from inspect import signature -from typing import Optional, Union, get_origin, get_args, Any -if sys.version_info >= (3, 10): - from typing import NotRequired, Required, is_typeddict +from typing import Optional, Union, get_origin, get_args, Any, get_type_hints import flask from flask import request @@ -22,13 +20,15 @@ fn_list = dict() # from 3.10 onwards, Unions written X | Y have the type UnionType -# from 3.10 onwards, we get the is_typeddict function so we can support TypedDicts UNION_TYPES = [Union] -HANDLE_TYPEDDICTS = False if sys.version_info >= (3, 10): from types import UnionType UNION_TYPES = [Union, UnionType] - HANDLE_TYPEDDICTS = True + +if sys.version_info >= (3, 11): + from typing import NotRequired, Required, is_typeddict +elif sys.version_info >= (3, 9): + from typing_extensions import NotRequired, Required, is_typeddict class ValidateParameters: @classmethod @@ -223,7 +223,7 @@ def _generic_types_validation_helper(self, return converted_list, True # typeddict - elif HANDLE_TYPEDDICTS and is_typeddict(expected_input_type): + elif is_typeddict(expected_input_type): # check for a stringified dict (like from Query) if type(user_input) is str: try: @@ -241,7 +241,7 @@ def _generic_types_validation_helper(self, converted_dict = {} # go through each user input key and make sure the value is the correct type for key, value in user_input.items(): - annotations = inspect.get_annotations(expected_input_type) + annotations = get_type_hints(expected_input_type) if key not in annotations: # we are strict in not allowing extra keys # if you want extra keys, use NotRequired diff --git a/flask_parameter_validation/test/test_form_params.py b/flask_parameter_validation/test/test_form_params.py index 1033bd8..28aa698 100644 --- a/flask_parameter_validation/test/test_form_params.py +++ b/flask_parameter_validation/test/test_form_params.py @@ -1741,59 +1741,60 @@ def test_dict_args_str_list_3_10_union(client): r = client.post(url, data={"v": json.dumps(d)}) assert "error" in r.json - def test_typeddict_normal(client): - url = "/form/typeddict/" - # Test that correct input yields input value - d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, data={"v": json.dumps(d)}) - assert "v" in r.json - assert r.json["v"] == d - # Test that missing keys yields error - d = {"id": 3} - r = client.post(url, data={"v": json.dumps(d)}) - assert "error" in r.json - # Test that incorrect values yields error - d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, data={"v": json.dumps(d)}) - assert "error" in r.json +def test_typeddict_normal(client): + url = "/form/typeddict/" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json + # Test that incorrect values yields error + d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json - def test_typeddict_functional(client): - url = "/form/typeddict/functional" - # Test that correct input yields input value - d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, data={"v": json.dumps(d)}) - assert "v" in r.json - assert r.json["v"] == d - # Test that missing keys yields error - d = {"id": 3} - r = client.post(url, data={"v": json.dumps(d)}) - assert "error" in r.json - # Test that incorrect values yields error - d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, data={"v": json.dumps(d)}) - assert "error" in r.json +def test_typeddict_functional(client): + url = "/form/typeddict/functional" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json + # Test that incorrect values yields error + d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json - def test_typeddict_optional(client): - url = "/form/typeddict/optional" - # Test that correct input yields input value - d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, data={"v": json.dumps(d)}) - assert "v" in r.json - assert r.json["v"] == d - # Test that no input yields input value - d = None - r = client.post(url, data={"v": d}) - assert "v" in r.json - assert r.json["v"] == d - # Test that missing keys yields error - d = {"id": 3} - r = client.post(url, data={"v": json.dumps(d)}) - assert "error" in r.json - # Test that empty dict yields error - d = {} - r = client.post(url, data={"v": json.dumps(d)}) - assert "error" in r.json +def test_typeddict_optional(client): + url = "/form/typeddict/optional" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that no input yields input value + d = None + r = client.post(url, data={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json + # Test that empty dict yields error + d = {} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json +if sys.version_info >= (3, 10): def test_typeddict_union_optional(client): url = "/form/typeddict/union_optional" # Test that correct input yields input value @@ -1815,183 +1816,183 @@ def test_typeddict_union_optional(client): r = client.post(url, data={"v": json.dumps(d)}) assert "error" in r.json - def test_typeddict_default(client): - url = "/form/typeddict/default" - # Test that missing input for required and optional yields default values - r = client.post(url) - assert "n_opt" in r.json - assert r.json["n_opt"] == {"id": 1, "name": "Bob", "timestamp": datetime.datetime(2025, 11, 18, 0, 0).isoformat()} - assert "opt" in r.json - assert r.json["opt"] == {"id": 2, "name": "Billy", "timestamp": datetime.datetime(2025, 11, 18, 5, 30).isoformat()} - # Test that present TypedDict input for required and optional yields input values - d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, data={ - "opt": json.dumps(d), - "n_opt": json.dumps(d), - }) - assert "opt" in r.json - assert r.json["opt"] == d - assert "n_opt" in r.json - assert r.json["n_opt"] == d - # Test that present non-TypedDict input for required yields error - r = client.post(url, data={"opt": {"id": 3}, "n_opt": "b"}) - assert "error" in r.json +def test_typeddict_default(client): + url = "/form/typeddict/default" + # Test that missing input for required and optional yields default values + r = client.post(url) + assert "n_opt" in r.json + assert r.json["n_opt"] == {"id": 1, "name": "Bob", "timestamp": datetime.datetime(2025, 11, 18, 0, 0).isoformat()} + assert "opt" in r.json + assert r.json["opt"] == {"id": 2, "name": "Billy", "timestamp": datetime.datetime(2025, 11, 18, 5, 30).isoformat()} + # Test that present TypedDict input for required and optional yields input values + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={ + "opt": json.dumps(d), + "n_opt": json.dumps(d), + }) + assert "opt" in r.json + assert r.json["opt"] == d + assert "n_opt" in r.json + assert r.json["n_opt"] == d + # Test that present non-TypedDict input for required yields error + r = client.post(url, data={"opt": {"id": 3}, "n_opt": "b"}) + assert "error" in r.json - def test_typeddict_func(client): - url = "/form/typeddict/func" - # Test that correct input yields input value - d = {"id": 3, "name": "Bill", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, data={"v": json.dumps(d)}) - assert "v" in r.json - assert r.json["v"] == d - # Test that func failing input yields input value - d = {"id": 3, "name": "Billy Bob Joe", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, data={"v": json.dumps(d)}) - assert "error" in r.json +def test_typeddict_func(client): + url = "/form/typeddict/func" + # Test that correct input yields input value + d = {"id": 3, "name": "Bill", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that func failing input yields input value + d = {"id": 3, "name": "Billy Bob Joe", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json - def test_typeddict_json_schema(client): - url = "/form/typeddict/json_schema" - # Test that correct input yields input value - d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, data={"v": json.dumps(d)}) - assert "v" in r.json - assert r.json["v"] == d - # Test that missing keys yields error - d = {"id": 3} - r = client.post(url, data={"v": json.dumps(d)}) - assert "error" in r.json - # Test that incorrect values yields error - d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, data={"v": json.dumps(d)}) - assert "error" in r.json +def test_typeddict_json_schema(client): + url = "/form/typeddict/json_schema" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json + # Test that incorrect values yields error + d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json - def test_typeddict_not_required(client): - url = "/form/typeddict/not_required" - # Test that all keys yields input value - d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, data={"v": json.dumps(d)}) - assert "v" in r.json - assert r.json["v"] == d - # Test that missing not requried key yields input value - d = {"name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, data={"v": json.dumps(d)}) - assert "v" in r.json - assert r.json["v"] == d - # Test that missing required keys yields error - d = {"name": "Merriweather"} - r = client.post(url, data={"v": json.dumps(d)}) - assert "error" in r.json +def test_typeddict_not_required(client): + url = "/form/typeddict/not_required" + # Test that all keys yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing not requried key yields input value + d = {"name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing required keys yields error + d = {"name": "Merriweather"} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json - def test_typeddict_required(client): - url = "/form/typeddict/required" - # Test that all keys yields input value - d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, data={"v": json.dumps(d)}) - assert "v" in r.json - assert r.json["v"] == d - # Test that missing not requried key yields input value - d = {"name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, data={"v": json.dumps(d)}) - assert "v" in r.json - assert r.json["v"] == d - # Test that missing required keys yields error - d = {"name": "Merriweather"} - r = client.post(url, data={"v": json.dumps(d)}) - assert "error" in r.json +def test_typeddict_required(client): + url = "/form/typeddict/required" + # Test that all keys yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing not requried key yields input value + d = {"name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing required keys yields error + d = {"name": "Merriweather"} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json - def test_typeddict_complex(client): - url = "/form/typeddict/complex" - # Test that correct input yields input value - d = { - "name": "change da world", - "children": [ - { - "id": 4, - "name": "my final message. Goodb ye", - "timestamp": datetime.datetime.now().isoformat(), - } - ], - "left": { - "x": 3.4, - "y": 1.0, - "z": 99999.34455663 - }, - "right": { - "x": 3.2, - "y": 1.1, - "z": 999.3663 - }, - } - r = client.post(url, data={"v": json.dumps(d)}) - assert "v" in r.json - assert r.json["v"] == d - # Test that empty children list yields input value - d = { - "name": "change da world", - "children": [], - "left": { - "x": 3.4, - "y": 1.0, - "z": 99999.34455663 - }, - "right": { - "x": 3.2, - "y": 1.1, - "z": 999.3663 - }, - } - r = client.post(url, data={"v": json.dumps(d)}) - assert "v" in r.json - assert r.json["v"] == d - # Test that incorrect child TypedDict yields error - d = { - "name": "change da world", - "children": [ - { - "id": 4, - "name": 6, - "timestamp": datetime.datetime.now().isoformat(), - } - ], - "left": { - "x": 3.4, - "y": 1.0, - "z": 99999.34455663 - }, - "right": { - "x": 3.2, - "y": 1.1, - "z": 999.3663 - }, - } - r = client.post(url, data={"v": json.dumps(d)}) - assert "error" in r.json - # Test that omitting NotRequired key in child yields input value - d = { - "name": "tags", - "children": [ - { - "id": 4, - "name": "ice my wrist", - "timestamp": datetime.datetime.now().isoformat(), - } - ], - "left": { - "x": 3.4, - "y": 1.0, - "z": 99999.34455663 - }, - "right": { - "x": 3.2, - "y": 1.1, - "z": 999.3663 - }, - } - r = client.post(url, data={"v": json.dumps(d)}) - assert "v" in r.json - assert r.json["v"] == d - # Test that incorrect values yields error - d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, data={"v": json.dumps(d)}) - assert "error" in r.json +def test_typeddict_complex(client): + url = "/form/typeddict/complex" + # Test that correct input yields input value + d = { + "name": "change da world", + "children": [ + { + "id": 4, + "name": "my final message. Goodb ye", + "timestamp": datetime.datetime.now().isoformat(), + } + ], + "left": { + "x": 3.4, + "y": 1.0, + "z": 99999.34455663 + }, + "right": { + "x": 3.2, + "y": 1.1, + "z": 999.3663 + }, + } + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that empty children list yields input value + d = { + "name": "change da world", + "children": [], + "left": { + "x": 3.4, + "y": 1.0, + "z": 99999.34455663 + }, + "right": { + "x": 3.2, + "y": 1.1, + "z": 999.3663 + }, + } + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that incorrect child TypedDict yields error + d = { + "name": "change da world", + "children": [ + { + "id": 4, + "name": 6, + "timestamp": datetime.datetime.now().isoformat(), + } + ], + "left": { + "x": 3.4, + "y": 1.0, + "z": 99999.34455663 + }, + "right": { + "x": 3.2, + "y": 1.1, + "z": 999.3663 + }, + } + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json + # Test that omitting NotRequired key in child yields input value + d = { + "name": "tags", + "children": [ + { + "id": 4, + "name": "ice my wrist", + "timestamp": datetime.datetime.now().isoformat(), + } + ], + "left": { + "x": 3.4, + "y": 1.0, + "z": 99999.34455663 + }, + "right": { + "x": 3.2, + "y": 1.1, + "z": 999.3663 + }, + } + r = client.post(url, data={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that incorrect values yields error + d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, data={"v": json.dumps(d)}) + assert "error" in r.json diff --git a/flask_parameter_validation/test/test_json_params.py b/flask_parameter_validation/test/test_json_params.py index d92531b..8b56576 100644 --- a/flask_parameter_validation/test/test_json_params.py +++ b/flask_parameter_validation/test/test_json_params.py @@ -1985,59 +1985,60 @@ def test_dict_args_str_list_3_10_union(client): r = client.post(url, json={"v": d}) assert "error" in r.json - def test_typeddict_normal(client): - url = "/json/typeddict/" - # Test that correct input yields input value - d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, json={"v": d}) - assert "v" in r.json - assert r.json["v"] == d - # Test that missing keys yields error - d = {"id": 3} - r = client.post(url, json={"v": d}) - assert "error" in r.json - # Test that incorrect values yields error - d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, json={"v": d}) - assert "error" in r.json +def test_typeddict_normal(client): + url = "/json/typeddict/" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.post(url, json={"v": d}) + assert "error" in r.json + # Test that incorrect values yields error + d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "error" in r.json - def test_typeddict_functional(client): - url = "/json/typeddict/functional" - # Test that correct input yields input value - d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, json={"v": d}) - assert "v" in r.json - assert r.json["v"] == d - # Test that missing keys yields error - d = {"id": 3} - r = client.post(url, json={"v": d}) - assert "error" in r.json - # Test that incorrect values yields error - d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, json={"v": d}) - assert "error" in r.json +def test_typeddict_functional(client): + url = "/json/typeddict/functional" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.post(url, json={"v": d}) + assert "error" in r.json + # Test that incorrect values yields error + d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "error" in r.json - def test_typeddict_optional(client): - url = "/json/typeddict/optional" - # Test that correct input yields input value - d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, json={"v": d}) - assert "v" in r.json - assert r.json["v"] == d - # Test that no input yields input value - d = None - r = client.post(url, json={"v": d}) - assert "v" in r.json - assert r.json["v"] == d - # Test that missing keys yields error - d = {"id": 3} - r = client.post(url, json={"v": d}) - assert "error" in r.json - # Test that empty dict yields error - d = {} - r = client.post(url, json={"v": d}) - assert "error" in r.json +def test_typeddict_optional(client): + url = "/json/typeddict/optional" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that no input yields input value + d = None + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.post(url, json={"v": d}) + assert "error" in r.json + # Test that empty dict yields error + d = {} + r = client.post(url, json={"v": d}) + assert "error" in r.json +if sys.version_info >= (3, 10): def test_typeddict_union_optional(client): url = "/json/typeddict/union_optional" # Test that correct input yields input value @@ -2059,183 +2060,183 @@ def test_typeddict_union_optional(client): r = client.post(url, json={"v": d}) assert "error" in r.json - def test_typeddict_default(client): - url = "/json/typeddict/default" - # Test that missing input for required and optional yields default values - r = client.post(url) - assert "n_opt" in r.json - assert r.json["n_opt"] == {"id": 1, "name": "Bob", "timestamp": datetime.datetime(2025, 11, 18, 0, 0).isoformat()} - assert "opt" in r.json - assert r.json["opt"] == {"id": 2, "name": "Billy", "timestamp": datetime.datetime(2025, 11, 18, 5, 30).isoformat()} - # Test that present TypedDict input for required and optional yields input values - d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, json={ - "opt": d, - "n_opt": d, - }) - assert "opt" in r.json - assert r.json["opt"] == d - assert "n_opt" in r.json - assert r.json["n_opt"] == d - # Test that present non-TypedDict input for required yields error - r = client.post(url, json={"opt": {"id": 3}, "n_opt": "b"}) - assert "error" in r.json +def test_typeddict_default(client): + url = "/json/typeddict/default" + # Test that missing input for required and optional yields default values + r = client.post(url) + assert "n_opt" in r.json + assert r.json["n_opt"] == {"id": 1, "name": "Bob", "timestamp": datetime.datetime(2025, 11, 18, 0, 0).isoformat()} + assert "opt" in r.json + assert r.json["opt"] == {"id": 2, "name": "Billy", "timestamp": datetime.datetime(2025, 11, 18, 5, 30).isoformat()} + # Test that present TypedDict input for required and optional yields input values + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={ + "opt": d, + "n_opt": d, + }) + assert "opt" in r.json + assert r.json["opt"] == d + assert "n_opt" in r.json + assert r.json["n_opt"] == d + # Test that present non-TypedDict input for required yields error + r = client.post(url, json={"opt": {"id": 3}, "n_opt": "b"}) + assert "error" in r.json - def test_typeddict_func(client): - url = "/json/typeddict/func" - # Test that correct input yields input value - d = {"id": 3, "name": "Bill", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, json={"v": d}) - assert "v" in r.json - assert r.json["v"] == d - # Test that func failing input yields input value - d = {"id": 3, "name": "Billy Bob Joe", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, json={"v": d}) - assert "error" in r.json +def test_typeddict_func(client): + url = "/json/typeddict/func" + # Test that correct input yields input value + d = {"id": 3, "name": "Bill", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that func failing input yields input value + d = {"id": 3, "name": "Billy Bob Joe", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "error" in r.json - def test_typeddict_json_schema(client): - url = "/json/typeddict/json_schema" - # Test that correct input yields input value - d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, json={"v": d}) - assert "v" in r.json - assert r.json["v"] == d - # Test that missing keys yields error - d = {"id": 3} - r = client.post(url, json={"v": d}) - assert "error" in r.json - # Test that incorrect values yields error - d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, json={"v": d}) - assert "error" in r.json +def test_typeddict_json_schema(client): + url = "/json/typeddict/json_schema" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.post(url, json={"v": d}) + assert "error" in r.json + # Test that incorrect values yields error + d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "error" in r.json - def test_typeddict_not_required(client): - url = "/json/typeddict/not_required" - # Test that all keys yields input value - d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, json={"v": d}) - assert "v" in r.json - assert r.json["v"] == d - # Test that missing not requried key yields input value - d = {"name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, json={"v": d}) - assert "v" in r.json - assert r.json["v"] == d - # Test that missing required keys yields error - d = {"name": "Merriweather"} - r = client.post(url, json={"v": d}) - assert "error" in r.json +def test_typeddict_not_required(client): + url = "/json/typeddict/not_required" + # Test that all keys yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing not requried key yields input value + d = {"name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing required keys yields error + d = {"name": "Merriweather"} + r = client.post(url, json={"v": d}) + assert "error" in r.json - def test_typeddict_required(client): - url = "/json/typeddict/required" - # Test that all keys yields input value - d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, json={"v": d}) - assert "v" in r.json - assert r.json["v"] == d - # Test that missing not requried key yields input value - d = {"name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, json={"v": d}) - assert "v" in r.json - assert r.json["v"] == d - # Test that missing required keys yields error - d = {"name": "Merriweather"} - r = client.post(url, json={"v": d}) - assert "error" in r.json +def test_typeddict_required(client): + url = "/json/typeddict/required" + # Test that all keys yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing not requried key yields input value + d = {"name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing required keys yields error + d = {"name": "Merriweather"} + r = client.post(url, json={"v": d}) + assert "error" in r.json - def test_typeddict_complex(client): - url = "/json/typeddict/complex" - # Test that correct input yields input value - d = { - "name": "change da world", - "children": [ - { - "id": 4, - "name": "my final message. Goodb ye", - "timestamp": datetime.datetime.now().isoformat(), - } - ], - "left": { - "x": 3.4, - "y": 1.0, - "z": 99999.34455663 - }, - "right": { - "x": 3.2, - "y": 1.1, - "z": 999.3663 - }, - } - r = client.post(url, json={"v": d}) - assert "v" in r.json - assert r.json["v"] == d - # Test that empty children list yields input value - d = { - "name": "change da world", - "children": [], - "left": { - "x": 3.4, - "y": 1.0, - "z": 99999.34455663 - }, - "right": { - "x": 3.2, - "y": 1.1, - "z": 999.3663 - }, - } - r = client.post(url, json={"v": d}) - assert "v" in r.json - assert r.json["v"] == d - # Test that incorrect child TypedDict yields error - d = { - "name": "change da world", - "children": [ - { - "id": 4, - "name": 6, - "timestamp": datetime.datetime.now().isoformat(), - } - ], - "left": { - "x": 3.4, - "y": 1.0, - "z": 99999.34455663 - }, - "right": { - "x": 3.2, - "y": 1.1, - "z": 999.3663 - }, - } - r = client.post(url, json={"v": d}) - assert "error" in r.json - # Test that omitting NotRequired key in child yields input value - d = { - "name": "tags", - "children": [ - { - "id": 4, - "name": "ice my wrist", - "timestamp": datetime.datetime.now().isoformat(), - } - ], - "left": { - "x": 3.4, - "y": 1.0, - "z": 99999.34455663 - }, - "right": { - "x": 3.2, - "y": 1.1, - "z": 999.3663 - }, - } - r = client.post(url, json={"v": d}) - assert "v" in r.json - assert r.json["v"] == d - # Test that incorrect values yields error - d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} - r = client.post(url, json={"v": d}) - assert "error" in r.json +def test_typeddict_complex(client): + url = "/json/typeddict/complex" + # Test that correct input yields input value + d = { + "name": "change da world", + "children": [ + { + "id": 4, + "name": "my final message. Goodb ye", + "timestamp": datetime.datetime.now().isoformat(), + } + ], + "left": { + "x": 3.4, + "y": 1.0, + "z": 99999.34455663 + }, + "right": { + "x": 3.2, + "y": 1.1, + "z": 999.3663 + }, + } + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that empty children list yields input value + d = { + "name": "change da world", + "children": [], + "left": { + "x": 3.4, + "y": 1.0, + "z": 99999.34455663 + }, + "right": { + "x": 3.2, + "y": 1.1, + "z": 999.3663 + }, + } + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that incorrect child TypedDict yields error + d = { + "name": "change da world", + "children": [ + { + "id": 4, + "name": 6, + "timestamp": datetime.datetime.now().isoformat(), + } + ], + "left": { + "x": 3.4, + "y": 1.0, + "z": 99999.34455663 + }, + "right": { + "x": 3.2, + "y": 1.1, + "z": 999.3663 + }, + } + r = client.post(url, json={"v": d}) + assert "error" in r.json + # Test that omitting NotRequired key in child yields input value + d = { + "name": "tags", + "children": [ + { + "id": 4, + "name": "ice my wrist", + "timestamp": datetime.datetime.now().isoformat(), + } + ], + "left": { + "x": 3.4, + "y": 1.0, + "z": 99999.34455663 + }, + "right": { + "x": 3.2, + "y": 1.1, + "z": 999.3663 + }, + } + r = client.post(url, json={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that incorrect values yields error + d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} + r = client.post(url, json={"v": d}) + assert "error" in r.json diff --git a/flask_parameter_validation/test/test_multi_source_params.py b/flask_parameter_validation/test/test_multi_source_params.py index adfcf37..99e516c 100644 --- a/flask_parameter_validation/test/test_multi_source_params.py +++ b/flask_parameter_validation/test/test_multi_source_params.py @@ -769,25 +769,25 @@ def test_multi_source_dict_args_str_list_3_10_union(client, source_a, source_b): r = client.get(url) assert "error" in r.json - @pytest.mark.parametrize(*common_parameters) - def test_multi_source_typeddict(client, source_a, source_b): - if source_a == source_b or "route" in [source_a, source_b]: # Duplicate sources shouldn't be something someone does, so we won't test for it, Route does not support parameters of type 'dict' - return - d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - url = f"/ms_{source_a}_{source_b}/typeddict/" - for source in [source_a, source_b]: - # Test that present input yields input value - r = None - if source == "query": - r = client.get(url, query_string={"v": json.dumps(d)}) - elif source == "form": - r = client.get(url, data={"v": json.dumps(d)}) - elif source == "json": - r = client.get(url, json={"v": d}) - assert r is not None - assert "v" in r.json - assert r.json["v"] == d - # Test that missing input yields error - r = client.get(url) - assert "error" in r.json +@pytest.mark.parametrize(*common_parameters) +def test_multi_source_typeddict(client, source_a, source_b): + if source_a == source_b or "route" in [source_a, source_b]: # Duplicate sources shouldn't be something someone does, so we won't test for it, Route does not support parameters of type 'dict' + return + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + url = f"/ms_{source_a}_{source_b}/typeddict/" + for source in [source_a, source_b]: + # Test that present input yields input value + r = None + if source == "query": + r = client.get(url, query_string={"v": json.dumps(d)}) + elif source == "form": + r = client.get(url, data={"v": json.dumps(d)}) + elif source == "json": + r = client.get(url, json={"v": d}) + assert r is not None + assert "v" in r.json + assert r.json["v"] == d + # Test that missing input yields error + r = client.get(url) + assert "error" in r.json diff --git a/flask_parameter_validation/test/test_query_params.py b/flask_parameter_validation/test/test_query_params.py index c774be5..2feb83c 100644 --- a/flask_parameter_validation/test/test_query_params.py +++ b/flask_parameter_validation/test/test_query_params.py @@ -2850,59 +2850,60 @@ def test_dict_args_str_list_3_10_union(client): r = client.get(url, query_string={"v": json.dumps(d)}) assert "error" in r.json - def test_typeddict_normal(client): - url = "/query/typeddict/" - # Test that correct input yields input value - d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "v" in r.json - assert r.json["v"] == d - # Test that missing keys yields error - d = {"id": 3} - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "error" in r.json - # Test that incorrect values yields error - d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "error" in r.json +def test_typeddict_normal(client): + url = "/query/typeddict/" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json + # Test that incorrect values yields error + d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json - def test_typeddict_functional(client): - url = "/query/typeddict/functional" - # Test that correct input yields input value - d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "v" in r.json - assert r.json["v"] == d - # Test that missing keys yields error - d = {"id": 3} - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "error" in r.json - # Test that incorrect values yields error - d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "error" in r.json +def test_typeddict_functional(client): + url = "/query/typeddict/functional" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json + # Test that incorrect values yields error + d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json - def test_typeddict_optional(client): - url = "/query/typeddict/optional" - # Test that correct input yields input value - d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "v" in r.json - assert r.json["v"] == d - # Test that no input yields input value - d = None - r = client.get(url, query_string={"v": d}) - assert "v" in r.json - assert r.json["v"] == d - # Test that missing keys yields error - d = {"id": 3} - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "error" in r.json - # Test that empty dict yields error - d = {} - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "error" in r.json +def test_typeddict_optional(client): + url = "/query/typeddict/optional" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that no input yields input value + d = None + r = client.get(url, query_string={"v": d}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json + # Test that empty dict yields error + d = {} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json +if sys.version_info >= (3, 10): def test_typeddict_union_optional(client): url = "/query/typeddict/union_optional" # Test that correct input yields input value @@ -2924,183 +2925,183 @@ def test_typeddict_union_optional(client): r = client.get(url, query_string={"v": json.dumps(d)}) assert "error" in r.json - def test_typeddict_default(client): - url = "/query/typeddict/default" - # Test that missing input for required and optional yields default values - r = client.get(url) - assert "n_opt" in r.json - assert r.json["n_opt"] == {"id": 1, "name": "Bob", "timestamp": datetime.datetime(2025, 11, 18, 0, 0).isoformat()} - assert "opt" in r.json - assert r.json["opt"] == {"id": 2, "name": "Billy", "timestamp": datetime.datetime(2025, 11, 18, 5, 30).isoformat()} - # Test that present TypedDict input for required and optional yields input values - d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.get(url, query_string={ - "opt": json.dumps(d), - "n_opt": json.dumps(d), - }) - assert "opt" in r.json - assert r.json["opt"] == d - assert "n_opt" in r.json - assert r.json["n_opt"] == d - # Test that present non-TypedDict input for required yields error - r = client.get(url, query_string={"opt": {"id": 3}, "n_opt": "b"}) - assert "error" in r.json +def test_typeddict_default(client): + url = "/query/typeddict/default" + # Test that missing input for required and optional yields default values + r = client.get(url) + assert "n_opt" in r.json + assert r.json["n_opt"] == {"id": 1, "name": "Bob", "timestamp": datetime.datetime(2025, 11, 18, 0, 0).isoformat()} + assert "opt" in r.json + assert r.json["opt"] == {"id": 2, "name": "Billy", "timestamp": datetime.datetime(2025, 11, 18, 5, 30).isoformat()} + # Test that present TypedDict input for required and optional yields input values + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={ + "opt": json.dumps(d), + "n_opt": json.dumps(d), + }) + assert "opt" in r.json + assert r.json["opt"] == d + assert "n_opt" in r.json + assert r.json["n_opt"] == d + # Test that present non-TypedDict input for required yields error + r = client.get(url, query_string={"opt": {"id": 3}, "n_opt": "b"}) + assert "error" in r.json - def test_typeddict_func(client): - url = "/query/typeddict/func" - # Test that correct input yields input value - d = {"id": 3, "name": "Bill", "timestamp": datetime.datetime.now().isoformat()} - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "v" in r.json - assert r.json["v"] == d - # Test that func failing input yields input value - d = {"id": 3, "name": "Billy Bob Joe", "timestamp": datetime.datetime.now().isoformat()} - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "error" in r.json +def test_typeddict_func(client): + url = "/query/typeddict/func" + # Test that correct input yields input value + d = {"id": 3, "name": "Bill", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that func failing input yields input value + d = {"id": 3, "name": "Billy Bob Joe", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json - def test_typeddict_json_schema(client): - url = "/query/typeddict/json_schema" - # Test that correct input yields input value - d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "v" in r.json - assert r.json["v"] == d - # Test that missing keys yields error - d = {"id": 3} - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "error" in r.json - # Test that incorrect values yields error - d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "error" in r.json +def test_typeddict_json_schema(client): + url = "/query/typeddict/json_schema" + # Test that correct input yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing keys yields error + d = {"id": 3} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json + # Test that incorrect values yields error + d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json - def test_typeddict_not_required(client): - url = "/query/typeddict/not_required" - # Test that all keys yields input value - d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "v" in r.json - assert r.json["v"] == d - # Test that missing not requried key yields input value - d = {"name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "v" in r.json - assert r.json["v"] == d - # Test that missing required keys yields error - d = {"name": "Merriweather"} - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "error" in r.json +def test_typeddict_not_required(client): + url = "/query/typeddict/not_required" + # Test that all keys yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing not requried key yields input value + d = {"name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing required keys yields error + d = {"name": "Merriweather"} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json - def test_typeddict_required(client): - url = "/query/typeddict/required" - # Test that all keys yields input value - d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "v" in r.json - assert r.json["v"] == d - # Test that missing not requried key yields input value - d = {"name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "v" in r.json - assert r.json["v"] == d - # Test that missing required keys yields error - d = {"name": "Merriweather"} - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "error" in r.json +def test_typeddict_required(client): + url = "/query/typeddict/required" + # Test that all keys yields input value + d = {"id": 3, "name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing not requried key yields input value + d = {"name": "Merriweather", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that missing required keys yields error + d = {"name": "Merriweather"} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json - def test_typeddict_complex(client): - url = "/query/typeddict/complex" - # Test that correct input yields input value - d = { - "name": "change da world", - "children": [ - { - "id": 4, - "name": "my final message. Goodb ye", - "timestamp": datetime.datetime.now().isoformat(), - } - ], - "left": { - "x": 3.4, - "y": 1.0, - "z": 99999.34455663 - }, - "right": { - "x": 3.2, - "y": 1.1, - "z": 999.3663 - }, - } - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "v" in r.json - assert r.json["v"] == d - # Test that empty children list yields input value - d = { - "name": "change da world", - "children": [], - "left": { - "x": 3.4, - "y": 1.0, - "z": 99999.34455663 - }, - "right": { - "x": 3.2, - "y": 1.1, - "z": 999.3663 - }, - } - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "v" in r.json - assert r.json["v"] == d - # Test that incorrect child TypedDict yields error - d = { - "name": "change da world", - "children": [ - { - "id": 4, - "name": 6, - "timestamp": datetime.datetime.now().isoformat(), - } - ], - "left": { - "x": 3.4, - "y": 1.0, - "z": 99999.34455663 - }, - "right": { - "x": 3.2, - "y": 1.1, - "z": 999.3663 - }, - } - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "error" in r.json - # Test that omitting NotRequired key in child yields input value - d = { - "name": "tags", - "children": [ - { - "id": 4, - "name": "ice my wrist", - "timestamp": datetime.datetime.now().isoformat(), - } - ], - "left": { - "x": 3.4, - "y": 1.0, - "z": 99999.34455663 - }, - "right": { - "x": 3.2, - "y": 1.1, - "z": 999.3663 - }, - } - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "v" in r.json - assert r.json["v"] == d - # Test that incorrect values yields error - d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} - r = client.get(url, query_string={"v": json.dumps(d)}) - assert "error" in r.json +def test_typeddict_complex(client): + url = "/query/typeddict/complex" + # Test that correct input yields input value + d = { + "name": "change da world", + "children": [ + { + "id": 4, + "name": "my final message. Goodb ye", + "timestamp": datetime.datetime.now().isoformat(), + } + ], + "left": { + "x": 3.4, + "y": 1.0, + "z": 99999.34455663 + }, + "right": { + "x": 3.2, + "y": 1.1, + "z": 999.3663 + }, + } + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that empty children list yields input value + d = { + "name": "change da world", + "children": [], + "left": { + "x": 3.4, + "y": 1.0, + "z": 99999.34455663 + }, + "right": { + "x": 3.2, + "y": 1.1, + "z": 999.3663 + }, + } + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that incorrect child TypedDict yields error + d = { + "name": "change da world", + "children": [ + { + "id": 4, + "name": 6, + "timestamp": datetime.datetime.now().isoformat(), + } + ], + "left": { + "x": 3.4, + "y": 1.0, + "z": 99999.34455663 + }, + "right": { + "x": 3.2, + "y": 1.1, + "z": 999.3663 + }, + } + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json + # Test that omitting NotRequired key in child yields input value + d = { + "name": "tags", + "children": [ + { + "id": 4, + "name": "ice my wrist", + "timestamp": datetime.datetime.now().isoformat(), + } + ], + "left": { + "x": 3.4, + "y": 1.0, + "z": 99999.34455663 + }, + "right": { + "x": 3.2, + "y": 1.1, + "z": 999.3663 + }, + } + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "v" in r.json + assert r.json["v"] == d + # Test that incorrect values yields error + d = {"id": 1.03, "name": "foo", "timestamp": datetime.datetime.now().isoformat()} + r = client.get(url, query_string={"v": json.dumps(d)}) + assert "error" in r.json diff --git a/flask_parameter_validation/test/testing_blueprints/multi_source_blueprint.py b/flask_parameter_validation/test/testing_blueprints/multi_source_blueprint.py index c34ed97..aa62ec2 100644 --- a/flask_parameter_validation/test/testing_blueprints/multi_source_blueprint.py +++ b/flask_parameter_validation/test/testing_blueprints/multi_source_blueprint.py @@ -2,8 +2,11 @@ import datetime import uuid from typing import Optional, List, Union, TypedDict -if sys.version_info >= (3, 10): - from typing import NotRequired, Required + +if sys.version_info >= (3, 11): + from typing import NotRequired, Required, is_typeddict, TypedDict +elif sys.version_info >= (3, 9): + from typing_extensions import NotRequired, Required, is_typeddict, TypedDict from flask import Blueprint, jsonify @@ -240,20 +243,20 @@ def multi_source_dict_str_list_3_10_union(v: dict[str, Union[list[int], bool]] = assert type(ele) is int return jsonify({"v": v}) - class Simple(TypedDict): - id: int - name: str - timestamp: datetime.datetime + class Simple(TypedDict): + id: int + name: str + timestamp: datetime.datetime - @param_bp.route("/typeddict/", methods=["GET", "POST"]) - @ValidateParameters() - def multi_source_typeddict_normal(v: Simple = MultiSource(sources[0], sources[1], list_disable_query_csv=True)): - assert type(v) is dict - assert "id" in v and "name" in v and "timestamp" in v - assert type(v["id"]) is int - assert type(v["name"]) is str - assert type(v["timestamp"]) is datetime.datetime - v["timestamp"] = v["timestamp"].isoformat() - return jsonify({"v": v}) + @param_bp.route("/typeddict/", methods=["GET", "POST"]) + @ValidateParameters() + def multi_source_typeddict_normal(v: Simple = MultiSource(sources[0], sources[1], list_disable_query_csv=True)): + assert type(v) is dict + assert "id" in v and "name" in v and "timestamp" in v + assert type(v["id"]) is int + assert type(v["name"]) is str + assert type(v["timestamp"]) is datetime.datetime + v["timestamp"] = v["timestamp"].isoformat() + return jsonify({"v": v}) return param_bp diff --git a/flask_parameter_validation/test/testing_blueprints/typeddict_blueprint.py b/flask_parameter_validation/test/testing_blueprints/typeddict_blueprint.py index 218ce37..b40c7e7 100644 --- a/flask_parameter_validation/test/testing_blueprints/typeddict_blueprint.py +++ b/flask_parameter_validation/test/testing_blueprints/typeddict_blueprint.py @@ -1,8 +1,11 @@ import datetime import sys -from typing import Optional, TypedDict -if sys.version_info >= (3, 10): - from typing import NotRequired, Required, is_typeddict +from typing import Optional + +if sys.version_info >= (3, 11): + from typing import NotRequired, Required, is_typeddict, TypedDict +elif sys.version_info >= (3, 9): + from typing_extensions import NotRequired, Required, is_typeddict, TypedDict from flask import Blueprint, jsonify @@ -14,9 +17,6 @@ def get_typeddict_blueprint(ParamType: type[Parameter], bp_name: str, http_verb: typeddict_bp = Blueprint(bp_name, __name__, url_prefix="/typeddict") decorator = getattr(typeddict_bp, http_verb) - if sys.version_info < (3, 10): - return typeddict_bp - # TypedDict not currently supported by Route # def path(base: str, route_additions: str) -> str: # return base + (route_additions if ParamType is Route else "") @@ -62,17 +62,18 @@ def optional(v: Optional[Simple] = ParamType(list_disable_query_csv=True)): v["timestamp"] = v["timestamp"].isoformat() return jsonify({"v": v}) - @decorator("/union_optional") - @ValidateParameters() - def union_optional(v: Simple | None = ParamType(list_disable_query_csv=True)): - if v is not None: - assert type(v) is dict - assert "id" in v and "name" in v and "timestamp" in v - assert type(v["id"]) is int - assert type(v["name"]) is str - assert type(v["timestamp"]) is datetime.datetime - v["timestamp"] = v["timestamp"].isoformat() - return jsonify({"v": v}) + if sys.version_info >= (3, 10): + @decorator("/union_optional") + @ValidateParameters() + def union_optional(v: Simple | None = ParamType(list_disable_query_csv=True)): + if v is not None: + assert type(v) is dict + assert "id" in v and "name" in v and "timestamp" in v + assert type(v["id"]) is int + assert type(v["name"]) is str + assert type(v["timestamp"]) is datetime.datetime + v["timestamp"] = v["timestamp"].isoformat() + return jsonify({"v": v}) @decorator("/default") @ValidateParameters() From a6567f8d2b88c1ac5951883a872296d599562fad Mon Sep 17 00:00:00 2001 From: Seth Teichman Date: Thu, 20 Nov 2025 11:27:19 -0500 Subject: [PATCH 7/7] Add Willow to Contributors in README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index aa78454..1b1f74c 100644 --- a/README.md +++ b/README.md @@ -341,6 +341,7 @@ def json_schema(data: dict = Json(json_schema=json_schema)): ## Contributions Many thanks to all those who have made contributions to the project: * [d3-steichman](https://github.com/d3-steichman)/[smt5541](https://github.com/smt5541): API documentation, custom error handling, datetime validation and bug fixes +* [willowrimlinger](https://github.com/willowrimlinger): TypedDict support, dict subtyping, and async view handling bug fixes * [summersz](https://github.com/summersz): Parameter aliases, async support, form type conversion and list bug fixes * [Garcel](https://github.com/Garcel): Allow passing custom validator function * [iml1111](https://github.com/iml1111): Implement regex validation