From 269cffae22a481bd70c9fff801091334119bad5f Mon Sep 17 00:00:00 2001 From: euri10 Date: Wed, 24 Jun 2020 14:05:28 +0200 Subject: [PATCH 01/10] Changed RequestFiles type --- httpx/_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpx/_types.py b/httpx/_types.py index a8925bbe76..93c8837dc8 100644 --- a/httpx/_types.py +++ b/httpx/_types.py @@ -72,4 +72,4 @@ # (filename, file (or text), content_type) Tuple[Optional[str], FileContent, Optional[str]], ] -RequestFiles = Mapping[str, FileTypes] +RequestFiles = Union[Mapping[str, FileEntry], List[Tuple[str, FileEntry]]] From aee3ce409633524bbb007ef5c10153a215de69e7 Mon Sep 17 00:00:00 2001 From: euri10 Date: Wed, 24 Jun 2020 14:07:07 +0200 Subject: [PATCH 02/10] Changed RequestFiles type 2 --- httpx/_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpx/_types.py b/httpx/_types.py index 93c8837dc8..a74020a4ae 100644 --- a/httpx/_types.py +++ b/httpx/_types.py @@ -72,4 +72,4 @@ # (filename, file (or text), content_type) Tuple[Optional[str], FileContent, Optional[str]], ] -RequestFiles = Union[Mapping[str, FileEntry], List[Tuple[str, FileEntry]]] +RequestFiles = Union[Mapping[str, FileTypes], List[Tuple[str, FileTypes]]] From 196f225f4089cc9cbb46395fc2e0693248adb1d8 Mon Sep 17 00:00:00 2001 From: euri10 Date: Wed, 24 Jun 2020 15:22:25 +0200 Subject: [PATCH 03/10] Added test for multiple files same field --- httpx/_content_streams.py | 3 ++- tests/test_content_streams.py | 47 +++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/httpx/_content_streams.py b/httpx/_content_streams.py index 6436be563a..2545041721 100644 --- a/httpx/_content_streams.py +++ b/httpx/_content_streams.py @@ -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, dict) else files + for name, value in file_items: yield self.FileField(name=name, value=value) def iter_chunks(self) -> typing.Iterator[bytes]: diff --git a/tests/test_content_streams.py b/tests/test_content_streams.py index c5eb961ddc..c662addf18 100644 --- a/tests/test_content_streams.py +++ b/tests/test_content_streams.py @@ -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", io.BytesIO(b"")), + ] + 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"\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"\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"\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"\r\n", + b"--+++--\r\n", + ] + ) \ No newline at end of file From d3d7d6f38637f44f1f992f8be46ab8c08b0dc9d8 Mon Sep 17 00:00:00 2001 From: euri10 Date: Wed, 24 Jun 2020 15:35:17 +0200 Subject: [PATCH 04/10] Lint --- tests/test_content_streams.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_content_streams.py b/tests/test_content_streams.py index c662addf18..2b2adc92ae 100644 --- a/tests/test_content_streams.py +++ b/tests/test_content_streams.py @@ -250,4 +250,4 @@ async def test_multipart_multiple_files_single_input_content(): b"\r\n", b"--+++--\r\n", ] - ) \ No newline at end of file + ) From 07a9c33405501443e50219a090e085716fad4b9f Mon Sep 17 00:00:00 2001 From: euri10 Date: Wed, 24 Jun 2020 16:18:22 +0200 Subject: [PATCH 05/10] Mypy no idea --- httpx/_content_streams.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpx/_content_streams.py b/httpx/_content_streams.py index 2545041721..5c58effd2d 100644 --- a/httpx/_content_streams.py +++ b/httpx/_content_streams.py @@ -329,7 +329,7 @@ def _iter_fields( yield self.DataField(name=name, value=value) file_items = files.items() if isinstance(files, dict) else files - for name, value in file_items: + for name, value in file_items: # type: ignore yield self.FileField(name=name, value=value) def iter_chunks(self) -> typing.Iterator[bytes]: From b5410c95897992c63c096e1fb6819baed776e3b4 Mon Sep 17 00:00:00 2001 From: euri10 Date: Wed, 24 Jun 2020 16:18:33 +0200 Subject: [PATCH 06/10] Added doc --- docs/advanced.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/advanced.md b/docs/advanced.md index 76321dd1b2..475d7146eb 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -464,6 +464,17 @@ 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 filed form. +To do that you'll need to pass a list of tuples of the form `(form_field, (2_or_3_elements_tuple))` where the `2_or_3_elements_tuple` is described above and the `form_field` is the name of your form field. +For instance this request sends 2 files, `foo.png` and `bar.png` in one request on the `images` form field: + +```python +>>> multiple_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=multiple_files) +>>> print(r.text) +``` ## Customizing authentication From f36984f9e3fa9c20a1db8481eb0b660043fd173b Mon Sep 17 00:00:00 2001 From: euri10 Date: Wed, 24 Jun 2020 16:23:56 +0200 Subject: [PATCH 07/10] Fixed some docs typos --- docs/advanced.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/advanced.md b/docs/advanced.md index 475d7146eb..40055611bd 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -465,15 +465,14 @@ MIME header field. 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 filed form. -To do that you'll need to pass a list of tuples of the form `(form_field, (2_or_3_elements_tuple))` where the `2_or_3_elements_tuple` is described above and the `form_field` is the name of your form field. +You can also send multiple files in one go with a multiple file field form. +To do that you'll need to pass a list of tuples that looks like `(form_field, (2_or_3_elements_tuple))` where the `2_or_3_elements_tuple` is described above and the `form_field` is the name of your form field. For instance this request sends 2 files, `foo.png` and `bar.png` in one request on the `images` form field: ```python >>> multiple_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=multiple_files) ->>> print(r.text) ``` ## Customizing authentication From 6974cccaff94d0ee96629ca7ede4ae3cc4566a64 Mon Sep 17 00:00:00 2001 From: euri10 Date: Wed, 24 Jun 2020 16:41:29 +0200 Subject: [PATCH 08/10] Checking the right instance type and deleting the mypy ignore --- httpx/_content_streams.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httpx/_content_streams.py b/httpx/_content_streams.py index 5c58effd2d..402fa959c8 100644 --- a/httpx/_content_streams.py +++ b/httpx/_content_streams.py @@ -328,8 +328,8 @@ def _iter_fields( else: yield self.DataField(name=name, value=value) - file_items = files.items() if isinstance(files, dict) else files - for name, value in file_items: # type: ignore + 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]: From e08c549bb0d7042f240ef8f42f48b659e621fdb4 Mon Sep 17 00:00:00 2001 From: euri10 Date: Wed, 24 Jun 2020 16:50:20 +0200 Subject: [PATCH 09/10] Docs clarification --- docs/advanced.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/advanced.md b/docs/advanced.md index 40055611bd..e60318cb0e 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -466,12 +466,12 @@ MIME header field. 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 you'll need to pass a list of tuples that looks like `(form_field, (2_or_3_elements_tuple))` where the `2_or_3_elements_tuple` is described above and the `form_field` is the name of your form field. -For instance this request sends 2 files, `foo.png` and `bar.png` in one request on the `images` form field: +To do that, pass a list of `(field, )` 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 `files` form field: ```python ->>> multiple_files = [('images', ('foo.png', open('foo.png', 'rb'), 'image/png')), - ('images', ('bar.png', open('bar.png', 'rb'), 'image/png'))] +>>> multiple_files = [('files', ('foo.png', open('foo.png', 'rb'), 'image/png')), + ('files', ('bar.png', open('bar.png', 'rb'), 'image/png'))] >>> r = httpx.post("https://httpbin.org/post", files=multiple_files) ``` From d9278b0c2daea86d433816df7a364438ab38de2b Mon Sep 17 00:00:00 2001 From: euri10 Date: Wed, 24 Jun 2020 18:59:01 +0200 Subject: [PATCH 10/10] Back on images form field, with other files modified --- docs/advanced.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/advanced.md b/docs/advanced.md index e60318cb0e..9d416e6f01 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -467,12 +467,12 @@ MIME header field. You can also send multiple files in one go with a multiple file field form. To do that, pass a list of `(field, )` 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 `files` form field: +For instance this request sends 2 files, `foo.png` and `bar.png` in one request on the `images` form field: ```python ->>> multiple_files = [('files', ('foo.png', open('foo.png', 'rb'), 'image/png')), - ('files', ('bar.png', open('bar.png', 'rb'), 'image/png'))] ->>> r = httpx.post("https://httpbin.org/post", files=multiple_files) +>>> 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