From cfa627ae62cebff20edf0816ce327c61603ef6a8 Mon Sep 17 00:00:00 2001 From: Jakub Roztocil Date: Fri, 27 Jul 2012 17:08:16 +0200 Subject: [PATCH] Fixed encoding of fields with the same name. * Properly handle repeated data fields for multipart/form-data requests (#737) * Allow a list of 2-tuples as the `files` agument. * Consistently serialize lists a of parameters (#729). --- AUTHORS.rst | 1 + requests/models.py | 51 +++++++++++++++++++++++++++++++++----------------- tests/test_requests.py | 38 +++++++++++++++++++++++++++++++++++-- 3 files changed, 71 insertions(+), 19 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 1ff5314..f21d173 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 8f31c3a..1a74dc2 100644 --- a/requests/models.py +++ b/requests/models.py @@ -344,16 +344,33 @@ 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.') + + # 2-tuples containing both file and data fields. + fields = [] - for (k, v) in list(files.items()): + for k, v in tuples(files): # support for explicit filename if isinstance(v, (tuple, list)): fn, fp = v @@ -362,18 +379,18 @@ class Request(object): fp = v if isinstance(fp, (bytes, str)): fp = StringIO(fp) - fields.update({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) - - return (body, content_type) + fields.append((k, (fn, fp.read()))) + + 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))) + + 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 e8bfc88..abc57e1 100755 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -981,10 +981,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 +1028,39 @@ 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.""" + + data = [ + ('__field__', '__value__'), + ('__field__', '__value__'), + ] + files = [ + ('__field__', '__value__'), + ('__field__', '__value__'), + ] + + r = post(httpbin('post'), data=data, files=files) + 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 = ('Content-Disposition: form-data;' + ' name="__field__"; filename="__field__"') + self.assertEqual(body.count('__value__'), 4) + self.assertEqual(body.count(file_field), 2) + + if __name__ == '__main__': unittest.main() -- 2.7.4