diff --git a/AUTHORS.rst b/AUTHORS.rst index 1ff53148..f21d1736 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -110,3 +110,4 @@ Patches and Suggestions - Victoria Mo - Leila Muhtasib - Matthias Rahlf +- Jakub Roztocil diff --git a/requests/models.py b/requests/models.py index 8f31c3a7..136427fe 100644 --- a/requests/models.py +++ b/requests/models.py @@ -10,6 +10,7 @@ This module contains the primary objects that power Requests. import os import socket from datetime import datetime +from io import BytesIO from .hooks import dispatch_hook, HOOKS from .structures import CaseInsensitiveDict @@ -344,36 +345,55 @@ class Request(object): return data def _encode_files(self, files): + """Build the body for a multipart/form-data request. + Will successfully encode files when passed as a dict or a list of + 2-tuples. Order is retained if data is a list of 2-tuples but abritrary + if parameters are supplied as a dict. + + """ if (not files) or isinstance(self.data, str): return None - try: - fields = self.data.copy() - except AttributeError: - fields = dict(self.data) + def tuples(obj): + """Ensure 2-tuples. A dict or a 2-tuples list can be supplied.""" + if isinstance(obj, dict): + return list(obj.items()) + elif hasattr(obj, '__iter__'): + try: + dict(obj) + except ValueError: + pass + else: + return obj + raise ValueError('A dict or a list of 2-tuples required.') - for (k, v) in list(files.items()): + # 2-tuples containing both file and data fields. + fields = [] + + for k, v in tuples(files): # support for explicit filename if isinstance(v, (tuple, list)): fn, fp = v else: fn = guess_filename(v) or k fp = v - if isinstance(fp, (bytes, str)): + if isinstance(fp, str): fp = StringIO(fp) - fields.update({k: (fn, fp.read())}) + if isinstance(fp, bytes): + fp = BytesIO(fp) + fields.append((k, (fn, fp.read()))) - for field in fields: - if isinstance(fields[field], numeric_types): - fields[field] = str(fields[field]) - if isinstance(fields[field], list): - newvalue = ', '.join(fields[field]) - fields[field] = newvalue - - (body, content_type) = encode_multipart_formdata(fields) + for k, vs in tuples(self.data): + if isinstance(vs, list): + for v in vs: + fields.append((k, str(v))) + else: + fields.append((k, str(vs))) - return (body, content_type) + body, content_type = encode_multipart_formdata(fields) + + return body, content_type @property def full_url(self): diff --git a/tests/test_requests.py b/tests/test_requests.py index e8bfc881..f43ccac8 100755 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -7,7 +7,6 @@ import sys import os sys.path.insert(0, os.path.abspath('..')) - import json import os import unittest @@ -981,10 +980,10 @@ class RequestsTestSuite(TestSetup, TestBaseMixin, unittest.TestCase): list for a value in the data argument.""" data = {'field': ['a', 'b']} - files = {'file': 'Garbled data'} + files = {'field': 'Garbled data'} r = post(httpbin('post'), data=data, files=files) t = json.loads(r.text) - self.assertEqual(t.get('form'), {'field': 'a, b'}) + self.assertEqual(t.get('form'), {'field': ['a', 'b']}) self.assertEqual(t.get('files'), files) def test_str_data_content_type(self): @@ -1028,5 +1027,41 @@ class RequestsTestSuite(TestSetup, TestBaseMixin, unittest.TestCase): r = get(URL()) self.assertEqual(r.status_code, 200) + def test_post_fields_with_multiple_values_and_files_as_tuples(self): + """Test that it is possible to POST multiple data and file fields + with the same name. + https://github.com/kennethreitz/requests/pull/746 + """ + + fields = [ + ('__field__', '__value__'), + ('__field__', '__value__'), + ] + + r = post(httpbin('post'), data=fields, files=fields) + t = json.loads(r.text) + + self.assertEqual(t.get('form'), { + '__field__': [ + '__value__', + '__value__', + ] + }) + + # It's not currently possible to test for multiple file fields with + # the same name against httpbin so we need to inspect the encoded + # body manually. + request = r.request + body, content_type = request._encode_files(request.files) + file_field = (b'Content-Disposition: form-data;' + b' name="__field__"; filename="__field__"') + self.assertEqual(body.count(b'__value__'), 4) + self.assertEqual(body.count(file_field), 2) + + def test_bytes_files(self): + """Test that `bytes` can be used as the values of `files`.""" + post(httpbin('post'), files={'test': b'test'}) + + if __name__ == '__main__': unittest.main()