Repository: Azure/msrest-for-python

Issue:
Optional formData parameters crash msrest
If a parameter that is supposed to be formData is optional, we give `None` to requests:
```python
files = [('Text', (None, 'cognituve services')), ('Mode', (None, None)), ('PreContextText', (None, None)), ('PostContextText', (None, None))]
data = {}

    @staticmethod
    def _encode_files(files, data):
        """Build the body for a multipart/form-data request.

            Will successfully encode files when passed as a dict or a list of
            tuples. Order is retained if data is a list of tuples but arbitrary
            if parameters are supplied as a dict.
            The tuples may be 2-tuples (filename, fileobj), 3-tuples (filename, fileobj, contentype)
            or 4-tuples (filename, fileobj, contentype, custom_headers).
            """
        if (not files):
            raise ValueError("Files must be provided.")
        elif isinstance(data, basestring):
            raise ValueError("Data must not be a string.")

        new_fields = []
        fields = to_key_val_list(data or {})
        files = to_key_val_list(files or {})

        for field, val in fields:
            if isinstance(val, basestring) or not hasattr(val, '__iter__'):
                val = [val]
            for v in val:
                if v is not None:
                    # Don't call str() on bytestrings: in Py3 it all goes wrong.
                    if not isinstance(v, bytes):
                        v = str(v)

                    new_fields.append(
                        (field.decode('utf-8') if isinstance(field, bytes) else field,
                         v.encode('utf-8') if isinstance(v, str) else v))

        for (k, v) in files:
            # support for explicit filename
            ft = None
            fh = None
            if isinstance(v, (tuple, list)):
                if len(v) == 2:
                    fn, fp = v
                elif len(v) == 3:
                    fn, fp, ft = v
                else:
                    fn, fp, ft, fh = v
            else:
                fn = guess_filename(v) or k
                fp = v

            if isinstance(fp, (str, bytes, bytearray)):
                fdata = fp
            else:
>               fdata = fp.read()
E               AttributeError: 'NoneType' object has no attribute 'read'
```

Discussion:

Patch:
diff --git a/msrest/service_client.py b/msrest/service_client.py
index eed50c5..d86fcbb 100644
--- a/msrest/service_client.py
+++ b/msrest/service_client.py
@@ -164,10 +164,15 @@ class ServiceClient(object):
         """
         if content is None:
             content = {}
-        file_data = {f: self._format_data(d) for f, d in content.items()}
-        if headers:
-            headers.pop('Content-Type', None)
-        return self.send(request, headers, None, files=file_data, **config)
+        content_type = headers.pop('Content-Type', None) if headers else None
+
+        if content_type and content_type.lower() == 'application/x-www-form-urlencoded':
+            # Do NOT use "add_content" that assumes input is JSON
+            request.data = {f: d for f, d in content.items() if d is not None}
+            return self.send(request, headers, None, **config)
+        else: # Assume "multipart/form-data"
+            file_data = {f: self._format_data(d) for f, d in content.items() if d is not None}
+            return self.send(request, headers, None, files=file_data, **config)
 
     def send(self, request, headers=None, content=None, **config):
         """Prepare and send request object according to configuration.


Test patch:
diff --git a/tests/test_client.py b/tests/test_client.py
index ee10d48..650eac5 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -204,6 +204,17 @@ class TestServiceClient(unittest.TestCase):
         ServiceClient.send_formdata(mock_client, request, {'Content-Type':'1234'}, {'1':'1', '2':'2'})
         mock_client.send.assert_called_with(request, {}, None, files={'1':'formatted', '2':'formatted'})
 
+        ServiceClient.send_formdata(mock_client, request, {'Content-Type':'1234'}, {'1':'1', '2':None})
+        mock_client.send.assert_called_with(request, {}, None, files={'1':'formatted'})
+
+        ServiceClient.send_formdata(mock_client, request, {'Content-Type':'application/x-www-form-urlencoded'}, {'1':'1', '2':'2'})
+        mock_client.send.assert_called_with(request, {}, None)
+        self.assertEqual(request.data, {'1':'1', '2':'2'})
+
+        ServiceClient.send_formdata(mock_client, request, {'Content-Type':'application/x-www-form-urlencoded'}, {'1':'1', '2':None})
+        mock_client.send.assert_called_with(request, {}, None)
+        self.assertEqual(request.data, {'1':'1'})
+
     def test_format_data(self):
 
         mock_client = mock.create_autospec(ServiceClient)