]> git.ipfire.org Git - thirdparty/httpx.git/commitdiff
Add support for multiple files per POST field (#1032)
authoreuri10 <euri10@users.noreply.github.com>
Wed, 24 Jun 2020 17:17:27 +0000 (19:17 +0200)
committerGitHub <noreply@github.com>
Wed, 24 Jun 2020 17:17:27 +0000 (19:17 +0200)
* 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

docs/advanced.md
httpx/_content_streams.py
httpx/_types.py
tests/test_content_streams.py

index 76321dd1b2c221d6ae44290d3d4c2ae2fb748d2d..9d416e6f01d25b38e9787705a67e7ff80f97b622 100644 (file)
@@ -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
 
index 6436be563a0b26c00c8135e814b9cd8235bc7f29..402fa959c8f07282d585573140dc15e76c2c2c0e 100644 (file)
@@ -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]:
index a8925bbe7693d75e96444ea52b10b091b93d3885..a74020a4aefea59a4579d1f8bc4c6820fd7cb0d5 100644 (file)
@@ -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]]]
index c5eb961ddca014a45670c999d1597a240fd6318b..2b2adc92ae65ff245048cbff53362115191ea449 100644 (file)
@@ -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",
+        ]
+    )