diff --git a/django/http/response.py b/django/http/response.py index 0cc14d1346..9e8280a307 100644 --- a/django/http/response.py +++ b/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'] diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 2a43756889..a86f416620 100644 --- a/docs/ref/request-response.txt +++ b/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: diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index 492fad478f..00a2c9844c 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -385,6 +385,10 @@ Requests and Responses ` 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 ^^^^^ diff --git a/tests/httpwrappers/tests.py b/tests/httpwrappers/tests.py index 9b9b19e180..4e705e2aeb 100644 --- a/tests/httpwrappers/tests.py +++ b/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,', @@ -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): diff --git a/tests/responses/tests.py b/tests/responses/tests.py index e80e466a56..9642790b9a 100644 --- a/tests/responses/tests.py +++ b/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)