Fixed #18523 -- Added stream-like API to HttpResponse.

Added getvalue() to HttpResponse to return the content of the response,
along with a few other methods to partially match io.IOBase.

Thanks Claude Paroz for the suggestion and Nick Sanford for review.
This commit is contained in:
Michael Kelly 2014-04-14 11:58:59 -04:00 committed by Tim Graham
parent f7969b0920
commit ebc8e79cf3
5 changed files with 93 additions and 3 deletions

View File

@ -112,6 +112,7 @@ class HttpResponseBase(six.Iterator):
# historical behavior of request_finished. # historical behavior of request_finished.
self._handler_class = None self._handler_class = None
self.cookies = SimpleCookie() self.cookies = SimpleCookie()
self.closed = False
if status is not None: if status is not None:
self.status_code = status self.status_code = status
if reason is not None: if reason is not None:
@ -313,16 +314,26 @@ class HttpResponseBase(six.Iterator):
closable.close() closable.close()
except Exception: except Exception:
pass pass
self.closed = True
signals.request_finished.send(sender=self._handler_class) signals.request_finished.send(sender=self._handler_class)
def write(self, content): def write(self, content):
raise Exception("This %s instance is not writable" % self.__class__.__name__) raise IOError("This %s instance is not writable" % self.__class__.__name__)
def flush(self): def flush(self):
pass pass
def tell(self): def tell(self):
raise Exception("This %s instance cannot tell its position" % self.__class__.__name__) raise IOError("This %s instance cannot tell its position" % self.__class__.__name__)
# These methods partially implement a stream-like object interface.
# See https://docs.python.org/library/io.html#io.IOBase
def writable(self):
return False
def writelines(self, lines):
raise IOError("This %s instance is not writable" % self.__class__.__name__)
class HttpResponse(HttpResponseBase): class HttpResponse(HttpResponseBase):
@ -373,6 +384,16 @@ class HttpResponse(HttpResponseBase):
def tell(self): def tell(self):
return len(self.content) return len(self.content)
def getvalue(self):
return self.content
def writable(self):
return True
def writelines(self, lines):
for line in lines:
self.write(line)
class StreamingHttpResponse(HttpResponseBase): class StreamingHttpResponse(HttpResponseBase):
""" """
@ -410,6 +431,9 @@ class StreamingHttpResponse(HttpResponseBase):
def __iter__(self): def __iter__(self):
return self.streaming_content return self.streaming_content
def getvalue(self):
return b''.join(self.streaming_content)
class HttpResponseRedirectBase(HttpResponse): class HttpResponseRedirectBase(HttpResponse):
allowed_schemes = ['http', 'https', 'ftp'] allowed_schemes = ['http', 'https', 'ftp']

View File

@ -651,6 +651,12 @@ Attributes
This attribute exists so middleware can treat streaming responses This attribute exists so middleware can treat streaming responses
differently from regular responses. differently from regular responses.
.. attribute:: HttpResponse.closed
.. versionadded:: 1.8
``True`` if the response has been closed.
Methods Methods
------- -------
@ -769,6 +775,27 @@ Methods
This method makes an :class:`HttpResponse` instance a file-like object. This method makes an :class:`HttpResponse` instance a file-like object.
.. method:: HttpResponse.getvalue()
.. versionadded:: 1.8
Returns the value of :attr:`HttpResponse.content`. This method makes
an :class:`HttpResponse` instance a stream-like object.
.. method:: HttpResponse.writable()
.. versionadded:: 1.8
Always ``True``. This method makes an :class:`HttpResponse` instance a
stream-like object.
.. method:: HttpResponse.writelines(lines)
.. versionadded:: 1.8
Writes a list of lines to the response. Line seperators are not added. This
method makes an :class:`HttpResponse` instance a stream-like object.
.. _HTTP status code: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10 .. _HTTP status code: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10
.. _ref-httpresponse-subclasses: .. _ref-httpresponse-subclasses:

View File

@ -385,6 +385,10 @@ Requests and Responses
<django.http.HttpRequest.get_full_path>` method now escapes unsafe characters <django.http.HttpRequest.get_full_path>` method now escapes unsafe characters
from the path portion of a Uniform Resource Identifier (URI) properly. from the path portion of a Uniform Resource Identifier (URI) properly.
* :class:`~django.http.HttpResponse` now implements a few additional methods
like :meth:`~django.http.HttpResponse.getvalue` so that instances can be used
as stream objects.
Tests Tests
^^^^^ ^^^^^

View File

@ -407,6 +407,15 @@ class HttpResponseTests(unittest.TestCase):
r.write(b'def') r.write(b'def')
self.assertEqual(r.content, b'abcdef') self.assertEqual(r.content, b'abcdef')
def test_stream_interface(self):
r = HttpResponse('asdf')
self.assertEqual(r.getvalue(), b'asdf')
r = HttpResponse()
self.assertEqual(r.writable(), True)
r.writelines(['foo\n', 'bar\n', 'baz\n'])
self.assertEqual(r.content, b'foo\nbar\nbaz\n')
def test_unsafe_redirect(self): def test_unsafe_redirect(self):
bad_urls = [ bad_urls = [
'data:text/html,<script>window.alert("xss")</script>', 'data:text/html,<script>window.alert("xss")</script>',
@ -537,6 +546,9 @@ class StreamingHttpResponseTests(TestCase):
with self.assertRaises(Exception): with self.assertRaises(Exception):
r.tell() r.tell()
r = StreamingHttpResponse(iter(['hello', 'world']))
self.assertEqual(r.getvalue(), b'helloworld')
class FileCloseTests(TestCase): class FileCloseTests(TestCase):

View File

@ -4,14 +4,37 @@ from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.http import HttpResponse from django.http import HttpResponse
from django.http.response import HttpResponseBase
from django.test import SimpleTestCase from django.test import SimpleTestCase
UTF8 = 'utf-8' UTF8 = 'utf-8'
ISO88591 = 'iso-8859-1' ISO88591 = 'iso-8859-1'
class HttpResponseTests(SimpleTestCase): class HttpResponseBaseTests(SimpleTestCase):
def test_closed(self):
r = HttpResponseBase()
self.assertIs(r.closed, False)
r.close()
self.assertIs(r.closed, True)
def test_write(self):
r = HttpResponseBase()
self.assertIs(r.writable(), False)
with self.assertRaisesMessage(IOError, 'This HttpResponseBase instance is not writable'):
r.write('asdf')
with self.assertRaisesMessage(IOError, 'This HttpResponseBase instance is not writable'):
r.writelines(['asdf\n', 'qwer\n'])
def test_tell(self):
r = HttpResponseBase()
with self.assertRaisesMessage(IOError, 'This HttpResponseBase instance cannot tell its position'):
r.tell()
class HttpResponseTests(SimpleTestCase):
def test_status_code(self): def test_status_code(self):
resp = HttpResponse(status=418) resp = HttpResponse(status=418)
self.assertEqual(resp.status_code, 418) self.assertEqual(resp.status_code, 418)