diff --git a/AUTHORS b/AUTHORS index d8a3cf9103..7660d3a154 100644 --- a/AUTHORS +++ b/AUTHORS @@ -95,6 +95,7 @@ answer newbie questions, and generally made Django that much better: Antti Haapala Antti Kaihola Anubhav Joshi + Anvesh Mishra Aram Dulyan arien Armin Ronacher diff --git a/django/http/response.py b/django/http/response.py index 62b8b1c087..2bcd549f34 100644 --- a/django/http/response.py +++ b/django/http/response.py @@ -366,12 +366,31 @@ class HttpResponse(HttpResponseBase): """ streaming = False + non_picklable_attrs = frozenset( + [ + "resolver_match", + # Non-picklable attributes added by test clients. + "asgi_request", + "client", + "context", + "json", + "templates", + "wsgi_request", + ] + ) def __init__(self, content=b"", *args, **kwargs): super().__init__(*args, **kwargs) # Content is a bytestring. See the `content` property methods. self.content = content + def __getstate__(self): + obj_dict = self.__dict__.copy() + for attr in self.non_picklable_attrs: + if attr in obj_dict: + del obj_dict[attr] + return obj_dict + def __repr__(self): return "<%(cls)s status_code=%(status_code)d%(content_type)s>" % { "cls": self.__class__.__name__, diff --git a/django/template/response.py b/django/template/response.py index c38b95e9de..b4f0e171f1 100644 --- a/django/template/response.py +++ b/django/template/response.py @@ -8,7 +8,9 @@ class ContentNotRenderedError(Exception): class SimpleTemplateResponse(HttpResponse): - rendering_attrs = ["template_name", "context_data", "_post_render_callbacks"] + non_picklable_attrs = HttpResponse.non_picklable_attrs | frozenset( + ["template_name", "context_data", "_post_render_callbacks"] + ) def __init__( self, @@ -55,16 +57,11 @@ class SimpleTemplateResponse(HttpResponse): Raise an exception if trying to pickle an unrendered response. Pickle only rendered data, not the data used to construct the response. """ - obj_dict = self.__dict__.copy() if not self._is_rendered: raise ContentNotRenderedError( "The response content must be rendered before it can be pickled." ) - for attr in self.rendering_attrs: - if attr in obj_dict: - del obj_dict[attr] - - return obj_dict + return super().__getstate__() def resolve_template(self, template): """Accept a template object, path-to-template, or list of paths.""" @@ -145,7 +142,9 @@ class SimpleTemplateResponse(HttpResponse): class TemplateResponse(SimpleTemplateResponse): - rendering_attrs = SimpleTemplateResponse.rendering_attrs + ["_request"] + non_picklable_attrs = SimpleTemplateResponse.non_picklable_attrs | frozenset( + ["_request"] + ) def __init__( self, diff --git a/docs/releases/4.2.txt b/docs/releases/4.2.txt index 124470cf5b..ca1c2a0eaa 100644 --- a/docs/releases/4.2.txt +++ b/docs/releases/4.2.txt @@ -249,7 +249,8 @@ PostgreSQL 12 and higher. Miscellaneous ------------- -* ... +* The undocumented ``SimpleTemplateResponse.rendering_attrs`` and + ``TemplateResponse.rendering_attrs`` are renamed to ``non_picklable_attrs``. .. _deprecated-features-4.2: diff --git a/tests/test_client/tests.py b/tests/test_client/tests.py index ddc063f33d..57dc22ea0c 100644 --- a/tests/test_client/tests.py +++ b/tests/test_client/tests.py @@ -20,6 +20,7 @@ rather than the HTML rendered to the end-user. """ import itertools +import pickle import tempfile from unittest import mock @@ -80,6 +81,21 @@ class ClientTest(TestCase): self.assertEqual(response.context["var"], "\xf2") self.assertEqual(response.templates[0].name, "GET Template") + def test_pickling_response(self): + tests = ["/cbv_view/", "/get_view/"] + for url in tests: + with self.subTest(url=url): + response = self.client.get(url) + dump = pickle.dumps(response) + response_from_pickle = pickle.loads(dump) + self.assertEqual(repr(response), repr(response_from_pickle)) + + async def test_pickling_response_async(self): + response = await self.async_client.get("/async_get_view/") + dump = pickle.dumps(response) + response_from_pickle = pickle.loads(dump) + self.assertEqual(repr(response), repr(response_from_pickle)) + def test_query_string_encoding(self): # WSGI requires latin-1 encoded strings. response = self.client.get("/get_view/?var=1\ufffd") diff --git a/tests/test_client/urls.py b/tests/test_client/urls.py index 2508346cf8..228e6c6a78 100644 --- a/tests/test_client/urls.py +++ b/tests/test_client/urls.py @@ -7,6 +7,7 @@ from . import views urlpatterns = [ path("upload_view/", views.upload_view, name="upload_view"), path("get_view/", views.get_view, name="get_view"), + path("cbv_view/", views.CBView.as_view()), path("post_view/", views.post_view), path("post_then_get_view/", views.post_then_get_view), path("put_view/", views.put_view), diff --git a/tests/test_client/views.py b/tests/test_client/views.py index cff0463788..773e9e4e98 100644 --- a/tests/test_client/views.py +++ b/tests/test_client/views.py @@ -18,6 +18,7 @@ from django.shortcuts import render from django.template import Context, Template from django.test import Client from django.utils.decorators import method_decorator +from django.views.generic import TemplateView def get_view(request): @@ -418,3 +419,7 @@ class TwoArgException(Exception): def two_arg_exception(request): raise TwoArgException("one", "two") + + +class CBView(TemplateView): + template_name = "base.html"