From 50255e33053714ec3b363cf39e2d9a7f4b0ed83c Mon Sep 17 00:00:00 2001 From: Paul McMillan Date: Wed, 14 Sep 2011 23:27:35 +0000 Subject: [PATCH] Fixed #16494 by normalizing HttpResponse behavior with non-string input. HttpResponse now always converts content to string on output, regardless of input type. git-svn-id: http://code.djangoproject.com/svn/django/trunk@16829 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/http/__init__.py | 25 +++++------ docs/ref/request-response.txt | 10 +++-- tests/regressiontests/httpwrappers/tests.py | 49 ++++++++++++++++++++- 3 files changed, 65 insertions(+), 19 deletions(-) diff --git a/django/http/__init__.py b/django/http/__init__.py index 5a68e03d9d..913c7eb704 100644 --- a/django/http/__init__.py +++ b/django/http/__init__.py @@ -544,12 +544,7 @@ class HttpResponse(object): if not content_type: content_type = "%s; charset=%s" % (settings.DEFAULT_CONTENT_TYPE, self._charset) - if not isinstance(content, basestring) and hasattr(content, '__iter__'): - self._container = content - self._is_string = False - else: - self._container = [content] - self._is_string = True + HttpResponse._set_content(self, content) self.cookies = SimpleCookie() if status: self.status_code = status @@ -649,12 +644,16 @@ class HttpResponse(object): def _get_content(self): if self.has_header('Content-Encoding'): - return ''.join(self._container) - return smart_str(''.join(self._container), self._charset) + return ''.join([str(e) for e in self._container]) + return ''.join([smart_str(e, self._charset) for e in self._container]) def _set_content(self, value): - self._container = [value] - self._is_string = True + if hasattr(value, '__iter__'): + self._container = value + self._base_content_is_iter = True + else: + self._container = [value] + self._base_content_is_iter = False content = property(_get_content, _set_content) @@ -675,7 +674,7 @@ class HttpResponse(object): # The remaining methods partially implement the file-like object interface. # See http://docs.python.org/lib/bltin-file-objects.html def write(self, content): - if not self._is_string: + if self._base_content_is_iter: raise Exception("This %s instance is not writable" % self.__class__) self._container.append(content) @@ -683,9 +682,9 @@ class HttpResponse(object): pass def tell(self): - if not self._is_string: + if self._base_content_is_iter: raise Exception("This %s instance cannot tell its position" % self.__class__) - return sum([len(chunk) for chunk in self._container]) + return sum([len(str(chunk)) for chunk in self._container]) class HttpResponseRedirect(HttpResponse): status_code = 302 diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 88b3d771ef..e15fa5016f 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -587,7 +587,7 @@ Attributes .. attribute:: HttpResponse.content - A normal Python string representing the content, encoded from a Unicode + A string representing the content, encoded from a Unicode object if necessary. .. attribute:: HttpResponse.status_code @@ -603,9 +603,11 @@ Methods string) and MIME type. The :setting:`DEFAULT_CONTENT_TYPE` is ``'text/html'``. - ``content`` can be an iterator or a string. If it's an iterator, it should - return strings, and those strings will be joined together to form the - content of the response. + ``content`` should be an iterator or a string. If it's an + iterator, it should return strings, and those strings will be + joined together to form the content of the response. If it is not + an iterator or a string, it will be converted to a string when + accessed. ``status`` is the `HTTP Status code`_ for the response. diff --git a/tests/regressiontests/httpwrappers/tests.py b/tests/regressiontests/httpwrappers/tests.py index 66bd804259..81065564c7 100644 --- a/tests/regressiontests/httpwrappers/tests.py +++ b/tests/regressiontests/httpwrappers/tests.py @@ -216,13 +216,13 @@ class HttpResponseTests(unittest.TestCase): r['value'] = u'test value' self.assertTrue(isinstance(r['value'], str)) - # An error is raised ~hen a unicode object with non-ascii is assigned. + # An error is raised when a unicode object with non-ascii is assigned. self.assertRaises(UnicodeEncodeError, r.__setitem__, 'value', u't\xebst value') # An error is raised when a unicode object with non-ASCII format is # passed as initial mimetype or content_type. self.assertRaises(UnicodeEncodeError, HttpResponse, - mimetype=u't\xebst value') + content_type=u't\xebst value') # HttpResponse headers must be convertible to ASCII. self.assertRaises(UnicodeEncodeError, HttpResponse, @@ -250,6 +250,51 @@ class HttpResponseTests(unittest.TestCase): r = HttpResponse() self.assertEqual(r.get('test'), None) + def test_non_string_content(self): + #Bug 16494: HttpResponse should behave consistently with non-strings + r = HttpResponse(12345) + self.assertEqual(r.content, '12345') + + #test content via property + r = HttpResponse() + r.content = 12345 + self.assertEqual(r.content, '12345') + + def test_iter_content(self): + r = HttpResponse(['abc', 'def', 'ghi']) + self.assertEqual(r.content, 'abcdefghi') + + #test iter content via property + r = HttpResponse() + r.content = ['idan', 'alex', 'jacob'] + self.assertEqual(r.content, 'idanalexjacob') + + r = HttpResponse() + r.content = [1, 2, 3] + self.assertEqual(r.content, '123') + + #test retrieval explicitly using iter and odd inputs + r = HttpResponse() + r.content = ['1', u'2', 3, unichr(1950)] + result = [] + my_iter = r.__iter__() + while True: + try: + result.append(my_iter.next()) + except StopIteration: + break + #'\xde\x9e' == unichr(1950).encode('utf-8') + self.assertEqual(result, ['1', '2', '3', '\xde\x9e']) + self.assertEqual(r.content, '123\xde\x9e') + + #with Content-Encoding header + r = HttpResponse([1,1,2,4,8]) + r['Content-Encoding'] = 'winning' + self.assertEqual(r.content, '11248') + r.content = [unichr(1950),] + self.assertRaises(UnicodeEncodeError, + getattr, r, 'content') + class CookieTests(unittest.TestCase): def test_encode(self): """