Add support for multiple files per POST field (#1032)
* Changed RequestFiles type * Changed RequestFiles type 2 * Added test for multiple files same field * Lint * Mypy no idea * Added doc * Fixed some docs typos * Checking the right instance type and deleting the mypy ignore * Docs clarification * Back on images form field, with other files modified
This commit is contained in:
parent
0f7d644b8d
commit
4d287956fd
@ -464,6 +464,16 @@ MIME header field.
|
||||
It is safe to upload large files this way. File uploads are streaming by default, meaning that only one chunk will be loaded into memory at a time.
|
||||
|
||||
Non-file data fields can be included in the multipart form using by passing them to `data=...`.
|
||||
|
||||
You can also send multiple files in one go with a multiple file field form.
|
||||
To do that, pass a list of `(field, <file>)` items instead of a dictionary, allowing you to pass multiple items with the same `field`.
|
||||
For instance this request sends 2 files, `foo.png` and `bar.png` in one request on the `images` form field:
|
||||
|
||||
```python
|
||||
>>> files = [('images', ('foo.png', open('foo.png', 'rb'), 'image/png')),
|
||||
('images', ('bar.png', open('bar.png', 'rb'), 'image/png'))]
|
||||
>>> r = httpx.post("https://httpbin.org/post", files=files)
|
||||
```
|
||||
|
||||
## Customizing authentication
|
||||
|
||||
|
||||
@ -328,7 +328,8 @@ class MultipartStream(ContentStream):
|
||||
else:
|
||||
yield self.DataField(name=name, value=value)
|
||||
|
||||
for name, value in files.items():
|
||||
file_items = files.items() if isinstance(files, typing.Mapping) else files
|
||||
for name, value in file_items:
|
||||
yield self.FileField(name=name, value=value)
|
||||
|
||||
def iter_chunks(self) -> typing.Iterator[bytes]:
|
||||
|
||||
@ -72,4 +72,4 @@ FileTypes = Union[
|
||||
# (filename, file (or text), content_type)
|
||||
Tuple[Optional[str], FileContent, Optional[str]],
|
||||
]
|
||||
RequestFiles = Mapping[str, FileTypes]
|
||||
RequestFiles = Union[Mapping[str, FileTypes], List[Tuple[str, FileTypes]]]
|
||||
|
||||
@ -204,3 +204,50 @@ async def test_empty_request():
|
||||
def test_invalid_argument():
|
||||
with pytest.raises(TypeError):
|
||||
encode(123)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multipart_multiple_files_single_input_content():
|
||||
files = [
|
||||
("file", io.BytesIO(b"<file content 1>")),
|
||||
("file", io.BytesIO(b"<file content 2>")),
|
||||
]
|
||||
stream = encode(files=files, boundary=b"+++")
|
||||
sync_content = b"".join([part for part in stream])
|
||||
async_content = b"".join([part async for part in stream])
|
||||
|
||||
assert stream.can_replay()
|
||||
assert stream.get_headers() == {
|
||||
"Content-Length": "271",
|
||||
"Content-Type": "multipart/form-data; boundary=+++",
|
||||
}
|
||||
assert sync_content == b"".join(
|
||||
[
|
||||
b"--+++\r\n",
|
||||
b'Content-Disposition: form-data; name="file"; filename="upload"\r\n',
|
||||
b"Content-Type: application/octet-stream\r\n",
|
||||
b"\r\n",
|
||||
b"<file content 1>\r\n",
|
||||
b"--+++\r\n",
|
||||
b'Content-Disposition: form-data; name="file"; filename="upload"\r\n',
|
||||
b"Content-Type: application/octet-stream\r\n",
|
||||
b"\r\n",
|
||||
b"<file content 2>\r\n",
|
||||
b"--+++--\r\n",
|
||||
]
|
||||
)
|
||||
assert async_content == b"".join(
|
||||
[
|
||||
b"--+++\r\n",
|
||||
b'Content-Disposition: form-data; name="file"; filename="upload"\r\n',
|
||||
b"Content-Type: application/octet-stream\r\n",
|
||||
b"\r\n",
|
||||
b"<file content 1>\r\n",
|
||||
b"--+++\r\n",
|
||||
b'Content-Disposition: form-data; name="file"; filename="upload"\r\n',
|
||||
b"Content-Type: application/octet-stream\r\n",
|
||||
b"\r\n",
|
||||
b"<file content 2>\r\n",
|
||||
b"--+++--\r\n",
|
||||
]
|
||||
)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user