diff --git a/Taskfile.yml b/Taskfile.yml index 8102511..fa728a8 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -66,7 +66,7 @@ tasks: # Clear cache files rm -rf $PACKAGE_DIR/testing/.pytest_run_cache # Run pytest - poetry run pytest -n auto + poetry run pytest -n auto {{.CLI_ARGS}} #============================================================# #================= SECTION_HEADING ==========================# #============================================================# diff --git a/django_utils_lib/requests.py b/django_utils_lib/requests.py new file mode 100644 index 0000000..15be999 --- /dev/null +++ b/django_utils_lib/requests.py @@ -0,0 +1,41 @@ +from typing import Dict, Optional + + +def object_to_multipart_dict(obj: Dict, existing_multipart_dict: Optional[dict] = None, key_prefix="") -> Dict: + """ + This is basically the inverse of a multi-part form parser, which can additionally + handle nested entries. + + The main use-case for this is constructing requests in Python that emulate + a multipart FormData payload that would normally be sent by the frontend. + + Nested entries get flattened / hoisted, so that the final dict is a flat + key-value map, with bracket notation used for nested entries. List items are + also hoisted up, with indices put within leading brackets. + + Warning: values are not stringified (but would be in a true multipart payload) + + @example + ``` + nested_dict = {"a": 1, "multi": [{"id": "abc"}, {"id": "123"}]} + print(object_to_multipart_dict(nested_dict)) + # > {'a': 1, 'multi[0][id]': 'abc', 'multi[1][id]': '123'} + ``` + """ + result = existing_multipart_dict or {} + for _key, val in obj.items(): + # If this is a nested child, we need to wrap key in brackets + _key = f"[{_key}]" if existing_multipart_dict else _key + key = key_prefix + _key + if isinstance(val, dict): + object_to_multipart_dict(val, result, key) + elif isinstance(val, (list, tuple)): + for i, sub_val in enumerate(val): + sub_key = f"{key}[{i}]" + if isinstance(sub_val, dict): + object_to_multipart_dict(sub_val, result, sub_key) + else: + result[sub_key] = sub_val + else: + result[key] = val + return result diff --git a/django_utils_lib/testing/pytest_plugin.py b/django_utils_lib/testing/pytest_plugin.py index acb3fbd..0c6030b 100644 --- a/django_utils_lib/testing/pytest_plugin.py +++ b/django_utils_lib/testing/pytest_plugin.py @@ -21,10 +21,10 @@ import xdist import xdist.dsession import xdist.workermanage -from constants import PACKAGE_NAME from filelock import FileLock from typing_extensions import NotRequired, TypedDict +from django_utils_lib.constants import PACKAGE_NAME from django_utils_lib.logger import build_heading_block, pkg_logger from django_utils_lib.testing.utils import PytestNodeID, is_main_pytest_runner, validate_requirement_tagging diff --git a/django_utils_lib/tests/test_requests.py b/django_utils_lib/tests/test_requests.py new file mode 100644 index 0000000..b1ea9d8 --- /dev/null +++ b/django_utils_lib/tests/test_requests.py @@ -0,0 +1,22 @@ +from django_utils_lib.requests import object_to_multipart_dict + + +def test_object_to_multipart_dict(): + regular_dict = { + "a": 1, + "b": ("tuple_a", "tuple_b"), + "c": ["list_a", "list_b"], + "nested_dict": {"nested_a_b": {"d": "d test"}, "e": "e test", "f": 24.1}, + "nested_objs_list": [{"name": "nested obj a"}, {"name": "nested obj b"}], + } + multipart_dict = object_to_multipart_dict(regular_dict) + assert multipart_dict.get("a") == 1 + assert multipart_dict.get("b[0]") == "tuple_a" + assert multipart_dict.get("b[1]") == "tuple_b" + assert multipart_dict.get("c[0]") == "list_a" + assert multipart_dict.get("c[1]") == "list_b" + assert multipart_dict.get("nested_dict[nested_a_b][d]") == "d test" + assert multipart_dict.get("nested_dict[e]") == "e test" + assert multipart_dict.get("nested_dict[f]") == 24.1 + assert multipart_dict.get("nested_objs_list[0][name]") == "nested obj a" + assert multipart_dict.get("nested_objs_list[1][name]") == "nested obj b"