diff --git a/AUTHORS.rst b/AUTHORS.rst index f21d1736..47ffd102 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -111,3 +111,4 @@ Patches and Suggestions - Leila Muhtasib - Matthias Rahlf - Jakub Roztocil +- Ian Cordasco @sigmavirus24 diff --git a/requests/models.py b/requests/models.py index 87d795c7..9270f0ea 100644 --- a/requests/models.py +++ b/requests/models.py @@ -31,7 +31,7 @@ from .exceptions import ( from .utils import ( get_encoding_from_headers, stream_untransfer, guess_filename, requote_uri, stream_decode_response_unicode, get_netrc_auth, get_environ_proxies, - DEFAULT_CA_BUNDLE_PATH) + to_key_val_list, DEFAULT_CA_BUNDLE_PATH) from .compat import ( cookielib, urlparse, urlunparse, urljoin, urlsplit, urlencode, str, bytes, StringIO, is_py2, chardet, json, builtin_str, numeric_types) @@ -41,8 +41,8 @@ CONTENT_CHUNK_SIZE = 10 * 1024 class Request(object): - """The :class:`Request ` object. It carries out all functionality of - Requests. Recommended interface is with the Requests functions. + """The :class:`Request ` object. It carries out all functionality + of Requests. Recommended interface is with the Requests functions. """ def __init__(self, @@ -322,21 +322,13 @@ class Request(object): if parameters are supplied as a dict. """ - if isinstance(data, bytes): - return data - if isinstance(data, str): + if isinstance(data, (str, bytes)): return data elif hasattr(data, 'read'): return data elif hasattr(data, '__iter__'): - try: - dict(data) - except ValueError: - raise ValueError('Unable to encode lists with elements that are not 2-tuples.') - - params = list(data.items() if isinstance(data, dict) else data) result = [] - for k, vs in params: + for k, vs in to_key_val_list(data): for v in isinstance(vs, list) and vs or [vs]: result.append( (k.encode('utf-8') if isinstance(k, str) else k, @@ -356,23 +348,11 @@ class Request(object): if (not files) or isinstance(self.data, str): return None - 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.') + new_fields = [] + fields = to_key_val_list(self.data) + files = to_key_val_list(files) - # 2-tuples containing both file and data fields. - fields = [] - - for k, v in tuples(files): + for (k, v) in files: # support for explicit filename if isinstance(v, (tuple, list)): fn, fp = v @@ -383,16 +363,15 @@ class Request(object): fp = StringIO(fp) if isinstance(fp, bytes): fp = BytesIO(fp) - fields.append((k, (fn, fp.read()))) + new_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))) + for field, val in fields: + if isinstance(val, list): + for v in val: + new_fields.append((k, str(v))) else: - fields.append((k, str(vs))) - - body, content_type = encode_multipart_formdata(fields) + new_fields.append((field, str(val))) + body, content_type = encode_multipart_formdata(new_fields) return body, content_type diff --git a/requests/sessions.py b/requests/sessions.py index 1201498e..cd05490d 100644 --- a/requests/sessions.py +++ b/requests/sessions.py @@ -15,7 +15,7 @@ from .cookies import cookiejar_from_dict, remove_cookie_by_name from .defaults import defaults from .models import Request from .hooks import dispatch_hook -from .utils import header_expand +from .utils import header_expand, to_key_val_list from .packages.urllib3.poolmanager import PoolManager @@ -38,12 +38,14 @@ def merge_kwargs(local_kwarg, default_kwarg): if not hasattr(default_kwarg, 'items'): return local_kwarg + local_kwarg = to_key_val_list(local_kwarg) + # Update new values. kwargs = default_kwarg.copy() kwargs.update(local_kwarg) # Remove keys that are set to None. - for (k, v) in list(local_kwarg.items()): + for (k, v) in local_kwarg: if v is None: del kwargs[k] @@ -70,12 +72,13 @@ class Session(object): verify=True, cert=None): + #self.headers = to_key_val_list(headers or []) self.headers = headers or {} self.auth = auth self.timeout = timeout - self.proxies = proxies or {} + self.proxies = to_key_val_list(proxies or []) self.hooks = hooks or {} - self.params = params or {} + self.params = to_key_val_list(params or []) self.config = config or {} self.prefetch = prefetch self.verify = verify @@ -156,10 +159,10 @@ class Session(object): method = str(method).upper() # Default empty dicts for dict params. - data = {} if data is None else data - files = {} if files is None else files + data = [] if data is None else data + files = [] if files is None else files headers = {} if headers is None else headers - params = {} if params is None else params + params = [] if params is None else params hooks = {} if hooks is None else hooks prefetch = prefetch if prefetch is not None else self.prefetch @@ -169,6 +172,8 @@ class Session(object): # Expand header values. if headers: + #e = [(k, header_expand(v)) for k, v in to_key_val_list(headers)] + #headers = e for k, v in list(headers.items()) or {}: headers[k] = header_expand(v) @@ -184,7 +189,7 @@ class Session(object): hooks=hooks, timeout=timeout, allow_redirects=allow_redirects, - proxies=proxies, + proxies=to_key_val_list(proxies), config=config, prefetch=prefetch, verify=verify, diff --git a/requests/utils.py b/requests/utils.py index 864760cc..9b8ea21d 100644 --- a/requests/utils.py +++ b/requests/utils.py @@ -114,6 +114,33 @@ def guess_filename(obj): return name +def to_key_val_list(value): + """Take an object and test to see if it can be represented as a + dictionary. Unless it can not be represented as such, return a list of + tuples, e.g.,: + + >>> to_key_val_list([('key', 'val')]) + [('key', 'val')] + >>> to_key_val_list('string') + ValueError: ... + >>> to_key_val_list({'key': 'val'}) + [('key', 'val')] + """ + if value is None: + return None + + try: + dict(value) + except ValueError: + raise ValueError('Unable to encode lists with elements that are not ' + '2-tuples.') + + if isinstance(value, dict) or hasattr(value, 'items'): + value = value.items() + + return list(value) + + # From mitsuhiko/werkzeug (used with permission). def parse_list_header(value): """Parse lists as described by RFC 2068 Section 2. diff --git a/tests/test_requests.py b/tests/test_requests.py index 25e6bbdc..ff768cbe 100755 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -52,7 +52,6 @@ class TestSetup(object): # time.sleep(1) _httpbin = True - class TestBaseMixin(object): def assertCookieHas(self, cookie, **kwargs): @@ -62,7 +61,6 @@ class TestBaseMixin(object): message = 'Failed comparison for %s: %s != %s' % (attr, cookie_attr, expected_value) self.assertEqual(cookie_attr, expected_value, message) - class RequestsTestSuite(TestSetup, TestBaseMixin, unittest.TestCase): """Requests test cases.""" @@ -95,6 +93,11 @@ class RequestsTestSuite(TestSetup, TestBaseMixin, unittest.TestCase): self.assertEqual(request.full_url, "http://example.com/path?key=value&a=b#fragment") + def test_params_accepts_kv_list(self): + request = requests.Request('http://example.com/path', + params=[('a', 'b')]) + self.assertEqual(request.full_url, 'http://example.com/path?a=b') + def test_HTTP_200_OK_GET(self): r = get(httpbin('get')) self.assertEqual(r.status_code, 200) @@ -311,11 +314,18 @@ class RequestsTestSuite(TestSetup, TestBaseMixin, unittest.TestCase): with open(__file__) as f: post2 = post(url, files={'some': f}) + post3 = post(url, files=[('some', f)]) self.assertEqual(post2.status_code, 200) - - post3 = post(url, data='[{"some": "json"}]') self.assertEqual(post3.status_code, 200) + post4 = post(url, data='[{"some": "json"}]') + self.assertEqual(post4.status_code, 200) + + try: + post(url, files=['bad file data']) + except ValueError: + pass + def test_POSTBIN_GET_POST_FILES_WITH_PARAMS(self): for service in SERVICES: @@ -325,8 +335,13 @@ class RequestsTestSuite(TestSetup, TestBaseMixin, unittest.TestCase): post1 = post(url, files={'some': f}, data={'some': 'data'}) + post2 = post(url, data={'some': 'data'}, files=[('some', f)]) + post3 = post(url, data=[('some', 'data')], + files=[('some', f)]) self.assertEqual(post1.status_code, 200) + self.assertEqual(post2.status_code, 200) + self.assertEqual(post3.status_code, 200) def test_POSTBIN_GET_POST_FILES_WITH_HEADERS(self): @@ -351,10 +366,12 @@ class RequestsTestSuite(TestSetup, TestBaseMixin, unittest.TestCase): post1 = post(url, files={'fname.txt': 'fdata'}) self.assertEqual(post1.status_code, 200) - post2 = post(url, files={'fname.txt': 'fdata', 'fname2.txt': 'more fdata'}) + post2 = post(url, files={'fname.txt': 'fdata', + 'fname2.txt': 'more fdata'}) self.assertEqual(post2.status_code, 200) - post3 = post(url, files={'fname.txt': 'fdata', 'fname2.txt': open(__file__, 'rb')}) + post3 = post(url, files={'fname.txt': 'fdata', + 'fname2.txt': open(__file__, 'rb')}) self.assertEqual(post3.status_code, 200) post4 = post(url, files={'fname.txt': 'fdata'}) @@ -376,9 +393,30 @@ class RequestsTestSuite(TestSetup, TestBaseMixin, unittest.TestCase): post7 = post(url, files={'fname.txt': 'fdata to verify'}) rbody = json.loads(post7.text) self.assertTrue(rbody.get('files', None)) - self.assertTrue(rbody['files'].get('fname.txt'), None) + self.assertTrue(rbody['files'].get('fname.txt', None)) self.assertEqual(rbody['files']['fname.txt'], 'fdata to verify') + post8 = post(url, files=[('fname.txt', 'fdata')]) + self.assertEqual(post8.status_code, 200) + resp_body = post8.json + self.assertTrue(resp_body.get('files', None)) + self.assertTrue(resp_body['files'].get('fname.txt', None)) + self.assertEqual(resp_body['files']['fname.txt'], 'fdata') + + post9 = post(url, files=[('fname.txt', fdata)]) + self.assertEqual(post9.status_code, 200) + + post10 = post(url, files=[('file', + ('file.txt', 'more file data'))]) + self.assertEqual(post10.status_code, 200) + + post11 = post(url, files=[('fname.txt', 'fdata'), + ('fname2.txt', 'more fdata')]) + post12 = post(url, files=[('fname.txt', 'fdata'), + ('fname2.txt', open(__file__, 'rb'))]) + self.assertEqual(post11.status_code, 200) + self.assertEqual(post12.status_code, 200) + def test_nonzero_evaluation(self): for service in SERVICES: @@ -933,13 +971,14 @@ class RequestsTestSuite(TestSetup, TestBaseMixin, unittest.TestCase): # Don't choke on headers with none in the value. requests.get(httpbin('headers'), headers={'Foo': None}) except TypeError: - self.fail() + self.fail('Not able to have none in header values') def test_danger_mode_redirects(self): s = requests.session() s.config['danger_mode'] = True s.get(httpbin('redirect', '4')) + def test_empty_response(self): r = requests.get(httpbin('status', '404')) r.text @@ -985,6 +1024,8 @@ class RequestsTestSuite(TestSetup, TestBaseMixin, unittest.TestCase): t = json.loads(r.text) self.assertEqual(t.get('form'), {'field': ['a', 'b']}) self.assertEqual(t.get('files'), files) + r = post(httpbin('post'), data=data, files=files.items()) + self.assertEqual(t.get('files'), files) def test_str_data_content_type(self): data = 'test string data'