Skip to content

Commit

Permalink
Add support for multiple files per POST field (#1032)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
euri10 authored Jun 24, 2020
1 parent 0f7d644 commit 4d28795
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 2 deletions.
10 changes: 10 additions & 0 deletions docs/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion httpx/_content_streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,8 @@ def _iter_fields(
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]:
Expand Down
2 changes: 1 addition & 1 deletion httpx/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,4 @@
# (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]]]
47 changes: 47 additions & 0 deletions tests/test_content_streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
)

0 comments on commit 4d28795

Please sign in to comment.