Browse Source

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.
Michael Kelly 11 years ago
parent
commit
ebc8e79cf3

+ 26 - 2
django/http/response.py

@@ -112,6 +112,7 @@ class HttpResponseBase(six.Iterator):
         # historical behavior of request_finished.
         self._handler_class = None
         self.cookies = SimpleCookie()
+        self.closed = False
         if status is not None:
             self.status_code = status
         if reason is not None:
@@ -313,16 +314,26 @@ class HttpResponseBase(six.Iterator):
                 closable.close()
             except Exception:
                 pass
+        self.closed = True
         signals.request_finished.send(sender=self._handler_class)
 
     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):
         pass
 
     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):
@@ -373,6 +384,16 @@ class HttpResponse(HttpResponseBase):
     def tell(self):
         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):
     """
@@ -410,6 +431,9 @@ class StreamingHttpResponse(HttpResponseBase):
     def __iter__(self):
         return self.streaming_content
 
+    def getvalue(self):
+        return b''.join(self.streaming_content)
+
 
 class HttpResponseRedirectBase(HttpResponse):
     allowed_schemes = ['http', 'https', 'ftp']

+ 27 - 0
docs/ref/request-response.txt

@@ -651,6 +651,12 @@ Attributes
     This attribute exists so middleware can treat streaming responses
     differently from regular responses.
 
+.. attribute:: HttpResponse.closed
+
+    .. versionadded:: 1.8
+
+    ``True`` if the response has been closed.
+
 Methods
 -------
 
@@ -769,6 +775,27 @@ Methods
 
     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
 
 .. _ref-httpresponse-subclasses:

+ 4 - 0
docs/releases/1.8.txt

@@ -385,6 +385,10 @@ Requests and Responses
   <django.http.HttpRequest.get_full_path>` method now escapes unsafe characters
   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
 ^^^^^
 

+ 12 - 0
tests/httpwrappers/tests.py

@@ -407,6 +407,15 @@ class HttpResponseTests(unittest.TestCase):
         r.write(b'def')
         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):
         bad_urls = [
             'data:text/html,<script>window.alert("xss")</script>',
@@ -537,6 +546,9 @@ class StreamingHttpResponseTests(TestCase):
         with self.assertRaises(Exception):
             r.tell()
 
+        r = StreamingHttpResponse(iter(['hello', 'world']))
+        self.assertEqual(r.getvalue(), b'helloworld')
+
 
 class FileCloseTests(TestCase):
 

+ 24 - 1
tests/responses/tests.py

@@ -4,14 +4,37 @@ from __future__ import unicode_literals
 
 from django.conf import settings
 from django.http import HttpResponse
+from django.http.response import HttpResponseBase
 from django.test import SimpleTestCase
 
 UTF8 = 'utf-8'
 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):
         resp = HttpResponse(status=418)
         self.assertEqual(resp.status_code, 418)