diff --git a/django/utils/http.py b/django/utils/http.py index c13f44602bd..4fbb786d891 100644 --- a/django/utils/http.py +++ b/django/utils/http.py @@ -88,11 +88,24 @@ def urlencode(query, doseq=False): query = query.lists() elif hasattr(query, 'items'): query = query.items() - return original_urlencode( - [(k, [str(i) for i in v] if isinstance(v, (list, tuple)) else str(v)) - for k, v in query], - doseq - ) + query_params = [] + for key, value in query: + if isinstance(value, (str, bytes)): + query_val = value + else: + try: + iter(value) + except TypeError: + query_val = value + else: + # Consume generators and iterators, even when doseq=True, to + # work around https://bugs.python.org/issue31706. + query_val = [ + item if isinstance(item, bytes) else str(item) + for item in value + ] + query_params.append((key, query_val)) + return original_urlencode(query_params, doseq) def cookie_date(epoch_seconds=None): diff --git a/tests/utils_tests/test_http.py b/tests/utils_tests/test_http.py index a7a670c85bb..41bda819679 100644 --- a/tests/utils_tests/test_http.py +++ b/tests/utils_tests/test_http.py @@ -47,6 +47,23 @@ class URLEncodeTests(unittest.TestCase): 'position=Developer&name=Adrian&name=Simon', ]) + def test_dict_with_bytes_values(self): + self.assertEqual(urlencode({'a': b'abc'}, doseq=True), 'a=abc') + + def test_dict_with_sequence_of_bytes(self): + self.assertEqual(urlencode({'a': [b'spam', b'eggs', b'bacon']}, doseq=True), 'a=spam&a=eggs&a=bacon') + + def test_dict_with_bytearray(self): + self.assertEqual(urlencode({'a': bytearray(range(2))}, doseq=True), 'a=0&a=1') + self.assertEqual(urlencode({'a': bytearray(range(2))}, doseq=False), 'a=%5B%270%27%2C+%271%27%5D') + + def test_generator(self): + def gen(): + yield from range(2) + + self.assertEqual(urlencode({'a': gen()}, doseq=True), 'a=0&a=1') + self.assertEqual(urlencode({'a': gen()}, doseq=False), 'a=%5B%270%27%2C+%271%27%5D') + class Base36IntTests(SimpleTestCase): def test_roundtrip(self):