From 0670b1b403087ec2d311321597b387ad541ea2e0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Johan=20L=C3=BCbcke?= <johan@lubcke.se>
Date: Fri, 24 May 2019 17:15:34 +0200
Subject: [PATCH] Fixed #30485 -- Adjusted django.utils.http.urlencode for
 doseq=False case.

---
 django/utils/http.py           |  4 ++--
 docs/releases/3.0.txt          |  4 ++++
 tests/utils_tests/test_http.py | 24 +++++++++++++++++-------
 3 files changed, 23 insertions(+), 9 deletions(-)

diff --git a/django/utils/http.py b/django/utils/http.py
index 3def0e02a6..d77bfb5992 100644
--- a/django/utils/http.py
+++ b/django/utils/http.py
@@ -116,7 +116,7 @@ def urlencode(query, doseq=False):
                 'Cannot encode None in a query string. Did you mean to pass '
                 'an empty string or omit the value?'
             )
-        elif isinstance(value, (str, bytes)):
+        elif isinstance(value, (str, bytes)) or not doseq:
             query_val = value
         else:
             try:
@@ -124,7 +124,7 @@ def urlencode(query, doseq=False):
             except TypeError:
                 query_val = value
             else:
-                # Consume generators and iterators, even when doseq=True, to
+                # Consume generators and iterators, when doseq=True, to
                 # work around https://bugs.python.org/issue31706.
                 query_val = []
                 for item in itr:
diff --git a/docs/releases/3.0.txt b/docs/releases/3.0.txt
index 5e661d8943..b0a318ff44 100644
--- a/docs/releases/3.0.txt
+++ b/docs/releases/3.0.txt
@@ -392,6 +392,10 @@ Miscellaneous
   now have the ``placeholder`` attribute, which mainly may require some
   adjustments in tests that compare HTML.
 
+* :func:`~django.utils.http.urlencode` now encodes iterable values as they are
+  when ``doseq=False``, rather than iterating them, bringing it into line with
+  the standard library :func:`urllib.parse.urlencode` function.
+
 .. _deprecated-features-3.0:
 
 Features deprecated in 3.0
diff --git a/tests/utils_tests/test_http.py b/tests/utils_tests/test_http.py
index ec934fd2e4..fbc75c65c7 100644
--- a/tests/utils_tests/test_http.py
+++ b/tests/utils_tests/test_http.py
@@ -34,7 +34,20 @@ class URLEncodeTests(SimpleTestCase):
         ])
 
     def test_dict_containing_sequence_not_doseq(self):
-        self.assertEqual(urlencode({'a': [1, 2]}, doseq=False), 'a=%5B%271%27%2C+%272%27%5D')
+        self.assertEqual(urlencode({'a': [1, 2]}, doseq=False), 'a=%5B1%2C+2%5D')
+
+    def test_dict_containing_tuple_not_doseq(self):
+        self.assertEqual(urlencode({'a': (1, 2)}, doseq=False), 'a=%281%2C+2%29')
+
+    def test_custom_iterable_not_doseq(self):
+        class IterableWithStr:
+            def __str__(self):
+                return 'custom'
+
+            def __iter__(self):
+                yield from range(0, 3)
+
+        self.assertEqual(urlencode({'a': IterableWithStr()}, doseq=False), 'a=custom')
 
     def test_dict_containing_sequence_doseq(self):
         self.assertEqual(urlencode({'a': [1, 2]}, doseq=True), 'a=1&a=2')
@@ -61,14 +74,11 @@ class URLEncodeTests(SimpleTestCase):
 
     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')
+        self.assertEqual(urlencode({'a': bytearray(range(2))}, doseq=False), 'a=bytearray%28b%27%5Cx00%5Cx01%27%29')
 
     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')
+        self.assertEqual(urlencode({'a': range(2)}, doseq=True), 'a=0&a=1')
+        self.assertEqual(urlencode({'a': range(2)}, doseq=False), 'a=range%280%2C+2%29')
 
     def test_none(self):
         with self.assertRaisesMessage(TypeError, self.cannot_encode_none_msg):