RequestFiles,
ResponseContent,
)
+from ._utils import primitive_value_to_str
class PlainByteStream:
def encode_urlencoded_data(
data: dict,
) -> Tuple[Dict[str, str], ByteStream]:
- body = urlencode(data, doseq=True).encode("utf-8")
+ plain_data = []
+ for key, value in data.items():
+ if isinstance(value, (list, tuple)):
+ plain_data.extend([(key, primitive_value_to_str(item)) for item in value])
+ else:
+ plain_data.append((key, primitive_value_to_str(value)))
+ body = urlencode(plain_data, doseq=True).encode("utf-8")
content_length = str(len(body))
content_type = "application/x-www-form-urlencoded"
headers = {"Content-Length": content_length, "Content-Type": content_type}
normalize_header_value,
obfuscate_sensitive_headers,
parse_header_links,
- str_query_param,
+ primitive_value_to_str,
)
else:
items = flatten_queryparams(value)
- self._list = [(str(k), str_query_param(v)) for k, v in items]
- self._dict = {str(k): str_query_param(v) for k, v in items}
+ self._list = [(str(k), primitive_value_to_str(v)) for k, v in items]
+ self._dict = {str(k): primitive_value_to_str(v) for k, v in items}
def keys(self) -> typing.KeysView:
return self._dict.keys()
format_form_param,
guess_content_type,
peek_filelike_length,
+ primitive_value_to_str,
to_bytes,
)
A single form field item, within a multipart form field.
"""
- def __init__(self, name: str, value: typing.Union[str, bytes]) -> None:
+ def __init__(
+ self, name: str, value: typing.Union[str, bytes, int, float, None]
+ ) -> None:
if not isinstance(name, str):
raise TypeError(
f"Invalid type for name. Expected str, got {type(name)}: {name!r}"
)
- if not isinstance(value, (str, bytes)):
+ if value is not None and not isinstance(value, (str, bytes, int, float)):
raise TypeError(
- f"Invalid type for value. Expected str or bytes, got {type(value)}: {value!r}"
+ f"Invalid type for value. Expected primitive type, got {type(value)}: {value!r}"
)
self.name = name
- self.value = value
+ self.value: typing.Union[str, bytes] = (
+ value if isinstance(value, bytes) else primitive_value_to_str(value)
+ )
def render_headers(self) -> bytes:
if not hasattr(self, "_headers"):
return value.encode(encoding or "ascii")
-def str_query_param(value: "PrimitiveData") -> str:
+def primitive_value_to_str(value: "PrimitiveData") -> str:
"""
- Coerce a primitive data type into a string value for query params.
+ Coerce a primitive data type into a string value.
Note that we prefer JSON-style 'true'/'false' for boolean values here.
"""
assert async_content == b"Hello=world%21"
+@pytest.mark.asyncio
+async def test_urlencoded_boolean():
+ headers, stream = encode_request(data={"example": True})
+ assert isinstance(stream, typing.Iterable)
+ assert isinstance(stream, typing.AsyncIterable)
+
+ sync_content = b"".join([part for part in stream])
+ async_content = b"".join([part async for part in stream])
+
+ assert headers == {
+ "Content-Length": "12",
+ "Content-Type": "application/x-www-form-urlencoded",
+ }
+ assert sync_content == b"example=true"
+ assert async_content == b"example=true"
+
+
+@pytest.mark.asyncio
+async def test_urlencoded_none():
+ headers, stream = encode_request(data={"example": None})
+ assert isinstance(stream, typing.Iterable)
+ assert isinstance(stream, typing.AsyncIterable)
+
+ sync_content = b"".join([part for part in stream])
+ async_content = b"".join([part async for part in stream])
+
+ assert headers == {
+ "Content-Length": "8",
+ "Content-Type": "application/x-www-form-urlencoded",
+ }
+ assert sync_content == b"example="
+ assert async_content == b"example="
+
+
+@pytest.mark.asyncio
+async def test_urlencoded_list():
+ headers, stream = encode_request(data={"example": ["a", 1, True]})
+ assert isinstance(stream, typing.Iterable)
+ assert isinstance(stream, typing.AsyncIterable)
+
+ sync_content = b"".join([part for part in stream])
+ async_content = b"".join([part async for part in stream])
+
+ assert headers == {
+ "Content-Length": "32",
+ "Content-Type": "application/x-www-form-urlencoded",
+ }
+ assert sync_content == b"example=a&example=1&example=true"
+ assert async_content == b"example=a&example=1&example=true"
+
+
@pytest.mark.asyncio
async def test_multipart_files_content():
files = {"file": io.BytesIO(b"<file content>")}
assert repr(key) in str(e.value)
-@pytest.mark.parametrize(("value"), (1, 2.3, None, [None, "abc"], {None: "abc"}))
+@pytest.mark.parametrize(("value"), (object(), {"key": "value"}))
def test_multipart_invalid_value(value):
client = httpx.Client(transport=httpx.MockTransport(echo_request_content))
"b": b"C",
"c": ["11", "22", "33"],
"d": "",
+ "e": True,
+ "f": "",
}
files = {"file": ("name.txt", open(path, "rb"))}
'--{0}\r\nContent-Disposition: form-data; name="c"\r\n\r\n22\r\n'
'--{0}\r\nContent-Disposition: form-data; name="c"\r\n\r\n33\r\n'
'--{0}\r\nContent-Disposition: form-data; name="d"\r\n\r\n\r\n'
+ '--{0}\r\nContent-Disposition: form-data; name="e"\r\n\r\ntrue\r\n'
+ '--{0}\r\nContent-Disposition: form-data; name="f"\r\n\r\n\r\n'
'--{0}\r\nContent-Disposition: form-data; name="file";'
' filename="name.txt"\r\n'
"Content-Type: text/plain\r\n\r\n<file content>\r\n"