Skip to content

Commit c89c38b

Browse files
vanhanitlucagiove
authored andcommitted
feat: Add support for uploading files as a list
Uploading a list of files with the same key is supported by the Requests library. This allows the receiving server to accept a list of files for the same key, i.e. accepting any number of files. Issue: #401
1 parent 1e34855 commit c89c38b

File tree

5 files changed

+129
-54
lines changed

5 files changed

+129
-54
lines changed

atests/http_server/helpers.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -110,15 +110,16 @@ def get_files():
110110

111111
files = dict()
112112

113-
for k, v in request.files.items():
114-
content_type = request.files[k].content_type or 'application/octet-stream'
115-
val = json_safe(v.read(), content_type)
116-
if files.get(k):
117-
if not isinstance(files[k], list):
118-
files[k] = [files[k]]
119-
files[k].append(val)
120-
else:
121-
files[k] = val
113+
for k in request.files.keys():
114+
for v in request.files.getlist(k):
115+
content_type = v.content_type or 'application/octet-stream'
116+
val = json_safe(v.read(), content_type)
117+
if files.get(k):
118+
if not isinstance(files[k], list):
119+
files[k] = [files[k]]
120+
files[k].append(val)
121+
else:
122+
files[k] = val
122123

123124
return files
124125

atests/test_post_multipart.robot

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,42 @@ Library RequestsLibrary
33

44

55
*** Test Cases ***
6-
Test Post On Session Multipart
6+
Test Post Dictionary On Session Multipart
77
${file_1}= Get File For Streaming Upload atests/randombytes.bin
88
${file_2}= Get File For Streaming Upload atests/randombytes.bin
99
${files}= Create Dictionary randombytes1 ${file_1} randombytes2 ${file_2}
1010

11+
Should Not Be True ${file_1.closed}
12+
Should Not Be True ${file_2.closed}
13+
1114
${resp}= POST On Session ${GLOBAL_SESSION} /anything files=${files}
1215

16+
Should Be True ${file_1.closed}
17+
Should Be True ${file_2.closed}
18+
1319
Should Contain ${resp.json()}[headers][Content-Type] multipart/form-data; boundary=
1420
Should Contain ${resp.json()}[headers][Content-Length] 480
1521
Should Contain ${resp.json()}[files] randombytes1
1622
Should Contain ${resp.json()}[files] randombytes2
23+
24+
Test Post List On Session Multipart
25+
${file_1}= Get File For Streaming Upload atests/randombytes.bin
26+
${file_2}= Get File For Streaming Upload atests/randombytes.bin
27+
${file_1_tuple}= Create List file1.bin ${file_1}
28+
${file_2_tuple}= Create List file2.bin ${file_2}
29+
${file_1_upload}= Create List randombytes ${file_1_tuple}
30+
${file_2_upload}= Create List randombytes ${file_2_tuple}
31+
${files}= Create List ${file_1_upload} ${file_2_upload}
32+
33+
Should Not Be True ${file_1.closed}
34+
Should Not Be True ${file_2.closed}
35+
36+
${resp}= POST On Session ${GLOBAL_SESSION} /anything files=${files}
37+
38+
Should Be True ${file_1.closed}
39+
Should Be True ${file_2.closed}
40+
41+
Should Contain ${resp.json()}[headers][Content-Type] multipart/form-data; boundary=
42+
Should Contain ${resp.json()}[headers][Content-Length] 466
43+
Should Contain ${resp.json()}[files] randombytes
44+
Length Should Be ${resp.json()}[files][randombytes] 2

doc/RequestsLibrary.html

Lines changed: 66 additions & 38 deletions
Large diffs are not rendered by default.

src/RequestsLibrary/RequestsKeywords.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from RequestsLibrary import log
77
from RequestsLibrary.compat import urljoin
88
from RequestsLibrary.utils import (
9+
is_list_or_tuple,
910
is_file_descriptor,
1011
warn_if_equal_symbol_in_url_session_less,
1112
)
@@ -48,14 +49,29 @@ def _common_request(self, method, session, uri, **kwargs):
4849

4950
files = kwargs.get("files", {}) or {}
5051
data = kwargs.get("data", []) or []
51-
files_descriptor_to_close = filter(
52-
is_file_descriptor, list(files.values()) + [data]
53-
)
54-
for file_descriptor in files_descriptor_to_close:
55-
file_descriptor.close()
52+
53+
self._close_file_descriptors(files, data)
5654

5755
return resp
5856

57+
@staticmethod
58+
def _close_file_descriptors(files, data):
59+
"""
60+
Helper method that closes any open file descriptors.
61+
"""
62+
63+
if is_list_or_tuple(files):
64+
files_descriptor_to_close = filter(
65+
is_file_descriptor, [file[1][1] for file in files] + [data]
66+
)
67+
else:
68+
files_descriptor_to_close = filter(
69+
is_file_descriptor, list(files.values()) + [data]
70+
)
71+
72+
for file_descriptor in files_descriptor_to_close:
73+
file_descriptor.close()
74+
5975
@staticmethod
6076
def _merge_url(session, uri):
6177
"""
@@ -176,7 +192,7 @@ def session_less_get(
176192
| ``json`` | A JSON serializable Python object to send in the body of the request. |
177193
| ``headers`` | Dictionary of HTTP Headers to send with the request. |
178194
| ``cookies`` | Dict or CookieJar object to send with the request. |
179-
| ``files`` | Dictionary of file-like-objects (or ``{'name': file-tuple}``) for multipart encoding upload. ``file-tuple`` can be a 2-tuple ``('filename', fileobj)``, 3-tuple ``('filename', fileobj, 'content_type')`` or a 4-tuple ``('filename', fileobj, 'content_type', custom_headers)``, where ``'content-type'`` is a string defining the content type of the given file and ``custom_headers`` a dict-like object containing additional headers to add for the file. |
195+
| ``files`` | Dictionary of file-like-objects (or ``{'name': file-tuple}``) for multipart encoding upload. ``file-tuple`` can be a 2-tuple ``('filename', fileobj)``, 3-tuple ``('filename', fileobj, 'content_type')`` or a 4-tuple ``('filename', fileobj, 'content_type', custom_headers)``, where ``'content-type'`` is a string defining the content type of the given file and ``custom_headers`` a dict-like object containing additional headers to add for the file. List or tuple of ``('key': file-tuple)`` allows uploading multiple files with the same key, resulting in a list of files on the receiving end. |
180196
| ``auth`` | Auth tuple to enable Basic/Digest/Custom HTTP Auth. |
181197
| ``timeout`` | How many seconds to wait for the server to send data before giving up, as a float, or a ``(connect timeout, read timeout)`` tuple. |
182198
| ``allow_redirects`` | Boolean. Enable/disable (values ``${True}`` or ``${False}``). Only for HEAD method keywords allow_redirection defaults to ``${False}``, all others ``${True}``. |

src/RequestsLibrary/utils.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ def is_string_type(data):
7474
def is_file_descriptor(fd):
7575
return isinstance(fd, io.IOBase)
7676

77+
def is_list_or_tuple(data):
78+
return isinstance(data, (list, tuple))
7779

7880
def utf8_urlencode(data):
7981
if is_string_type(data):

0 commit comments

Comments
 (0)