Fixed encoding of fields with the same name.
authorJakub Roztocil <jakub@roztocil.name>
Fri, 27 Jul 2012 15:08:16 +0000 (17:08 +0200)
committerJakub Roztocil <jakub@roztocil.name>
Fri, 10 Aug 2012 17:49:03 +0000 (19:49 +0200)
* 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
requests/models.py
tests/test_requests.py

index 1ff531480002d612a395bbf91bccb3e92108782f..f21d1736892e1c9c3514bef0b111715a3658061b 100644 (file)
@@ -110,3 +110,4 @@ Patches and Suggestions
 - Victoria Mo
 - Leila Muhtasib
 - Matthias Rahlf <matthias@webding.de>
+- Jakub Roztocil <jakub@roztocil.name>
index 8f31c3a7734f508c6437be8a9e843b3f2ab7d232..1a74dc2ba21d13cba8031f5e57f849ba7f5cf918 100644 (file)
@@ -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):
index e8bfc881061a487d5916f0ed17f4626ec78f8be8..abc57e12cb8013cfe1c8e984b1d46bf115262822 100755 (executable)
@@ -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()