From 9118255d733c88680f95d52169e0bdc767a848d5 Mon Sep 17 00:00:00 2001 From: Jonas Lundberg Date: Tue, 2 Apr 2024 19:26:26 +0200 Subject: [PATCH] Fix `data` pattern with list value --- respx/patterns.py | 16 ++++++++++++---- respx/utils.py | 29 ++++++++++++++++++++--------- tests/test_patterns.py | 6 ++++++ 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/respx/patterns.py b/respx/patterns.py index da2022a..c5b351d 100644 --- a/respx/patterns.py +++ b/respx/patterns.py @@ -548,9 +548,17 @@ class Data(MultiItemsMixin, Pattern): key = "data" value: MultiItems - def clean(self, value: Dict) -> MultiItems: + def _normalize_value(self, value: Any) -> Union[str, List[str]]: + if value is None: + return "" + elif isinstance(value, (tuple, list)): + return [str(v) for v in value] + else: + return str(value) + + def clean(self, value: Dict[str, Any]) -> MultiItems: return MultiItems( - (key, "" if value is None else str(value)) for key, value in value.items() + (key, self._normalize_value(value)) for key, value in value.items() ) def parse(self, request: httpx.Request) -> Any: @@ -563,7 +571,7 @@ class Files(MultiItemsMixin, Pattern): key = "files" value: MultiItems - def _normalize_file_value(self, value: FileTypes) -> Tuple[Any, Any]: + def _normalize_file_value(self, value: FileTypes) -> Tuple[Tuple[Any, Any]]: # Mimic httpx `FileField` to normalize `files` kwarg to shortest tuple style if isinstance(value, tuple): filename, fileobj = value[:2] @@ -580,7 +588,7 @@ def _normalize_file_value(self, value: FileTypes) -> Tuple[Any, Any]: elif isinstance(fileobj, str): fileobj = fileobj.encode() - return filename, fileobj + return ((filename, fileobj),) def clean(self, value: RequestFiles) -> MultiItems: if isinstance(value, Mapping): diff --git a/respx/utils.py b/respx/utils.py index 5eb4715..76db860 100644 --- a/respx/utils.py +++ b/respx/utils.py @@ -1,9 +1,11 @@ import email +from collections import defaultdict from datetime import datetime from email.message import Message from typing import ( Any, Dict, + Iterable, List, NamedTuple, Optional, @@ -23,15 +25,24 @@ import httpx -class MultiItems(dict): +class MultiItems(defaultdict): + def __init__(self, values: Optional[Iterable[Tuple[str, Any]]] = None) -> None: + super().__init__(tuple) + if values is not None: + for key, value in values: + if isinstance(value, (tuple, list)): + self[key] += tuple(value) # Convert list to tuple and extend + else: + self[key] += (value,) # Extend with value + def get_list(self, key: str) -> List[Any]: - try: - return [self[key]] - except KeyError: # pragma: no cover - return [] + return list(self[key]) + + def multi_items(self) -> List[Tuple[str, str]]: + return [(key, value) for key, values in self.items() for value in values] - def multi_items(self) -> List[Tuple[str, Any]]: - return list(self.items()) + def append(self, key: str, value: Any) -> None: + self[key] += (value,) def _parse_multipart_form_data( @@ -55,10 +66,10 @@ def _parse_multipart_form_data( assert isinstance(value, bytes) if content_type.startswith("text/") and filename is None: # Text field - data[name] = value.decode(payload.get_content_charset() or "utf-8") + data.append(name, value.decode(payload.get_content_charset() or "utf-8")) else: # File field - files[name] = filename, value + files.append(name, (filename, value)) return data, files diff --git a/tests/test_patterns.py b/tests/test_patterns.py index 451b0dd..3d51d5e 100644 --- a/tests/test_patterns.py +++ b/tests/test_patterns.py @@ -333,6 +333,12 @@ def test_content_pattern(lookup, content, expected): None, True, ), + ( + Lookup.EQUAL, + {"foo": "bar", "ham": ["spam", "egg"]}, + None, + True, + ), ( Lookup.EQUAL, {"foo": "bar", "ham": "spam"},