diff --git a/django/http/response.py b/django/http/response.py index abe71718f2..1dbaf46add 100644 --- a/django/http/response.py +++ b/django/http/response.py @@ -21,6 +21,7 @@ from django.http.cookie import SimpleCookie from django.utils import timezone from django.utils.datastructures import CaseInsensitiveMapping from django.utils.encoding import iri_to_uri +from django.utils.functional import cached_property from django.utils.http import content_disposition_header, http_date from django.utils.regex_helper import _lazy_re_compile @@ -408,6 +409,11 @@ class HttpResponse(HttpResponseBase): content = self.make_bytes(value) # Create a list of properly encoded bytestrings to support write(). self._container = [content] + self.__dict__.pop("text", None) + + @cached_property + def text(self): + return self.content.decode(self.charset or "utf-8") def __iter__(self): return iter(self._container) @@ -460,6 +466,12 @@ class StreamingHttpResponse(HttpResponseBase): "`streaming_content` instead." % self.__class__.__name__ ) + @property + def text(self): + raise AttributeError( + "This %s instance has no `text` attribute." % self.__class__.__name__ + ) + @property def streaming_content(self): if self.is_async: diff --git a/docs/ref/request-response.txt b/docs/ref/request-response.txt index 31111a435a..afebd00d8b 100644 --- a/docs/ref/request-response.txt +++ b/docs/ref/request-response.txt @@ -833,6 +833,13 @@ Attributes A bytestring representing the content, encoded from a string if necessary. +.. attribute:: HttpResponse.text + + .. versionadded:: 5.2 + + A string representation of :attr:`HttpResponse.content`, decoded using the + response's :attr:`HttpResponse.charset` (defaulting to ``UTF-8`` if empty). + .. attribute:: HttpResponse.cookies A :py:obj:`http.cookies.SimpleCookie` object holding the cookies included @@ -1272,6 +1279,9 @@ with the following notable differences: :attr:`~StreamingHttpResponse.streaming_content` attribute. This can be used in middleware to wrap the response iterable, but should not be consumed. +* It has no ``text`` attribute, as it would require iterating the response + object. + * You cannot use the file-like object ``tell()`` or ``write()`` methods. Doing so will raise an exception. diff --git a/docs/releases/5.2.txt b/docs/releases/5.2.txt index 806abfa26d..9090f8b70a 100644 --- a/docs/releases/5.2.txt +++ b/docs/releases/5.2.txt @@ -268,6 +268,9 @@ Models Requests and Responses ~~~~~~~~~~~~~~~~~~~~~~ +* The new :attr:`.HttpResponse.text` property provides the string representation + of :attr:`.HttpResponse.content`. + * The new :meth:`.HttpRequest.get_preferred_type` method can be used to query the preferred media type the client accepts. diff --git a/tests/httpwrappers/tests.py b/tests/httpwrappers/tests.py index 2197c6f7ea..154e9517fe 100644 --- a/tests/httpwrappers/tests.py +++ b/tests/httpwrappers/tests.py @@ -530,6 +530,22 @@ class HttpResponseTests(SimpleTestCase): headers={"Content-Type": "text/csv"}, ) + def test_text_updates_when_content_updates(self): + response = HttpResponse("Hello, world!") + self.assertEqual(response.text, "Hello, world!") + response.content = "Updated content" + self.assertEqual(response.text, "Updated content") + + def test_text_charset(self): + for content_type, content in [ + (None, b"Ol\xc3\xa1 Mundo"), + ("text/plain; charset=utf-8", b"Ol\xc3\xa1 Mundo"), + ("text/plain; charset=iso-8859-1", b"Ol\xe1 Mundo"), + ]: + with self.subTest(content_type=content_type): + response = HttpResponse(content, content_type=content_type) + self.assertEqual(response.text, "Olá Mundo") + class HttpResponseSubclassesTests(SimpleTestCase): def test_redirect(self): @@ -756,6 +772,13 @@ class StreamingHttpResponseTests(SimpleTestCase): with self.assertWarnsMessage(Warning, msg): self.assertEqual(b"hello", await anext(aiter(r))) + def test_text_attribute_error(self): + r = StreamingHttpResponse(iter(["hello", "world"])) + msg = "This %s instance has no `text` attribute." % r.__class__.__name__ + + with self.assertRaisesMessage(AttributeError, msg): + r.text + class FileCloseTests(SimpleTestCase): def setUp(self):