Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #16326 -- Fixed re-pickling of unpickled TemplateResponse insta…

…nces. Thanks, natrius and lrekucki.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16568 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 5fffe574bdd624f69eb246ed0833d7a2aba16edb 1 parent 94f7481
Jannis Leidel authored July 29, 2011
76  django/template/response.py
... ...
@@ -1,22 +1,28 @@
1 1
 from django.http import HttpResponse
2 2
 from django.template import loader, Context, RequestContext
3 3
 
  4
+
4 5
 class ContentNotRenderedError(Exception):
5 6
     pass
6 7
 
  8
+
  9
+class DiscardedAttributeError(AttributeError):
  10
+    pass
  11
+
  12
+
7 13
 class SimpleTemplateResponse(HttpResponse):
  14
+    rendering_attrs = ['template_name', 'context_data', '_post_render_callbacks']
8 15
 
9 16
     def __init__(self, template, context=None, mimetype=None, status=None,
10 17
             content_type=None):
11 18
         # It would seem obvious to call these next two members 'template' and
12  
-        # 'context', but those names are reserved as part of the test Client API.
13  
-        # To avoid the name collision, we use
14  
-        # tricky-to-debug problems
  19
+        # 'context', but those names are reserved as part of the test Client
  20
+        # API. To avoid the name collision, we use tricky-to-debug problems
15 21
         self.template_name = template
16 22
         self.context_data = context
17 23
 
18  
-        # _is_rendered tracks whether the template and context has been baked into
19  
-        # a final response.
  24
+        # _is_rendered tracks whether the template and context has been
  25
+        # baked into a final response.
20 26
         self._is_rendered = False
21 27
 
22 28
         self._post_render_callbacks = []
@@ -36,13 +42,21 @@ def __getstate__(self):
36 42
         """
37 43
         obj_dict = self.__dict__.copy()
38 44
         if not self._is_rendered:
39  
-            raise ContentNotRenderedError('The response content must be rendered before it can be pickled.')
40  
-        del obj_dict['template_name']
41  
-        del obj_dict['context_data']
42  
-        del obj_dict['_post_render_callbacks']
  45
+            raise ContentNotRenderedError('The response content must be '
  46
+                                          'rendered before it can be pickled.')
  47
+        for attr in self.rendering_attrs:
  48
+            if attr in obj_dict:
  49
+                del obj_dict[attr]
43 50
 
44 51
         return obj_dict
45 52
 
  53
+    def __getattr__(self, name):
  54
+        if name in self.rendering_attrs:
  55
+            raise DiscardedAttributeError('The %s attribute was discarded '
  56
+                                          'when this %s class was pickled.' %
  57
+                                          (name, self.__class__.__name__))
  58
+        return super(SimpleTemplateResponse, self).__getattr__(name)
  59
+
46 60
     def resolve_template(self, template):
47 61
         "Accepts a template object, path-to-template or list of paths"
48 62
         if isinstance(template, (list, tuple)):
@@ -53,7 +67,7 @@ def resolve_template(self, template):
53 67
             return template
54 68
 
55 69
     def resolve_context(self, context):
56  
-        """Convert context data into a full Context object
  70
+        """Converts context data into a full Context object
57 71
         (assuming it isn't already a Context object).
58 72
         """
59 73
         if isinstance(context, Context):
@@ -76,9 +90,10 @@ def rendered_content(self):
76 90
         return content
77 91
 
78 92
     def add_post_render_callback(self, callback):
79  
-        """Add a new post-rendering callback.
  93
+        """Adds a new post-rendering callback.
80 94
 
81  
-        If the response has already been rendered, invoke the callback immediately.
  95
+        If the response has already been rendered,
  96
+        invoke the callback immediately.
82 97
         """
83 98
         if self._is_rendered:
84 99
             callback(self)
@@ -86,7 +101,7 @@ def add_post_render_callback(self, callback):
86 101
             self._post_render_callbacks.append(callback)
87 102
 
88 103
     def render(self):
89  
-        """Render (thereby finalizing) the content of the response.
  104
+        """Renders (thereby finalizing) the content of the response.
90 105
 
91 106
         If the content has already been rendered, this is a no-op.
92 107
 
@@ -101,20 +116,25 @@ def render(self):
101 116
                     retval = newretval
102 117
         return retval
103 118
 
104  
-    is_rendered = property(lambda self: self._is_rendered)
  119
+    @property
  120
+    def is_rendered(self):
  121
+        return self._is_rendered
105 122
 
106 123
     def __iter__(self):
107 124
         if not self._is_rendered:
108  
-            raise ContentNotRenderedError('The response content must be rendered before it can be iterated over.')
  125
+            raise ContentNotRenderedError('The response content must be '
  126
+                                          'rendered before it can be iterated over.')
109 127
         return super(SimpleTemplateResponse, self).__iter__()
110 128
 
111 129
     def _get_content(self):
112 130
         if not self._is_rendered:
113  
-            raise ContentNotRenderedError('The response content must be rendered before it can be accessed.')
  131
+            raise ContentNotRenderedError('The response content must be '
  132
+                                          'rendered before it can be accessed.')
114 133
         return super(SimpleTemplateResponse, self)._get_content()
115 134
 
116 135
     def _set_content(self, value):
117  
-        "Sets the content for the response"
  136
+        """Sets the content for the response
  137
+        """
118 138
         super(SimpleTemplateResponse, self)._set_content(value)
119 139
         self._is_rendered = True
120 140
 
@@ -122,6 +142,9 @@ def _set_content(self, value):
122 142
 
123 143
 
124 144
 class TemplateResponse(SimpleTemplateResponse):
  145
+    rendering_attrs = SimpleTemplateResponse.rendering_attrs + \
  146
+        ['_request', '_current_app']
  147
+
125 148
     def __init__(self, request, template, context=None, mimetype=None,
126 149
             status=None, content_type=None, current_app=None):
127 150
         # self.request gets over-written by django.test.client.Client - and
@@ -134,27 +157,10 @@ def __init__(self, request, template, context=None, mimetype=None,
134 157
         super(TemplateResponse, self).__init__(
135 158
             template, context, mimetype, status, content_type)
136 159
 
137  
-    def __getstate__(self):
138  
-        """Pickling support function.
139  
-
140  
-        Ensures that the object can't be pickled before it has been
141  
-        rendered, and that the pickled state only includes rendered
142  
-        data, not the data used to construct the response.
143  
-        """
144  
-        obj_dict = super(TemplateResponse, self).__getstate__()
145  
-
146  
-        del obj_dict['_request']
147  
-        del obj_dict['_current_app']
148  
-
149  
-        return obj_dict
150  
-
151 160
     def resolve_context(self, context):
152 161
         """Convert context data into a full RequestContext object
153 162
         (assuming it isn't already a Context object).
154 163
         """
155 164
         if isinstance(context, Context):
156 165
             return context
157  
-        else:
158  
-            return RequestContext(self._request, context, current_app=self._current_app)
159  
-
160  
-
  166
+        return RequestContext(self._request, context, current_app=self._current_app)
54  tests/regressiontests/templates/response.py
... ...
@@ -1,3 +1,4 @@
  1
+from __future__ import with_statement
1 2
 from datetime import datetime
2 3
 import os
3 4
 import pickle
@@ -8,7 +9,8 @@
8 9
 import django.template.context
9 10
 from django.template import Template, Context
10 11
 from django.template.response import (TemplateResponse, SimpleTemplateResponse,
11  
-                                      ContentNotRenderedError)
  12
+                                      ContentNotRenderedError,
  13
+                                      DiscardedAttributeError)
12 14
 
13 15
 def test_processor(request):
14 16
     return {'processors': 'yes'}
@@ -190,9 +192,27 @@ def test_pickling(self):
190 192
 
191 193
         # ...and the unpickled reponse doesn't have the
192 194
         # template-related attributes, so it can't be re-rendered
193  
-        self.assertFalse(hasattr(unpickled_response, 'template_name'))
194  
-        self.assertFalse(hasattr(unpickled_response, 'context_data'))
195  
-        self.assertFalse(hasattr(unpickled_response, '_post_render_callbacks'))
  195
+        template_attrs = ('template_name', 'context_data', '_post_render_callbacks')
  196
+        for attr in template_attrs:
  197
+            self.assertFalse(hasattr(unpickled_response, attr))
  198
+
  199
+        # ...and requesting any of those attributes raises an exception
  200
+        for attr in template_attrs:
  201
+            with self.assertRaises(DiscardedAttributeError) as cm:
  202
+                getattr(unpickled_response, attr)
  203
+
  204
+    def test_repickling(self):
  205
+        response = SimpleTemplateResponse('first/test.html', {
  206
+                'value': 123,
  207
+                'fn': datetime.now,
  208
+            })
  209
+        self.assertRaises(ContentNotRenderedError,
  210
+                          pickle.dumps, response)
  211
+
  212
+        response.render()
  213
+        pickled_response = pickle.dumps(response)
  214
+        unpickled_response = pickle.loads(pickled_response)
  215
+        repickled_response = pickle.dumps(unpickled_response)
196 216
 
197 217
 class TemplateResponseTest(BaseTemplateResponseTest):
198 218
 
@@ -255,10 +275,28 @@ def test_pickling(self):
255 275
 
256 276
         # ...and the unpickled reponse doesn't have the
257 277
         # template-related attributes, so it can't be re-rendered
258  
-        self.assertFalse(hasattr(unpickled_response, '_request'))
259  
-        self.assertFalse(hasattr(unpickled_response, 'template_name'))
260  
-        self.assertFalse(hasattr(unpickled_response, 'context_data'))
261  
-        self.assertFalse(hasattr(unpickled_response, '_post_render_callbacks'))
  278
+        template_attrs = ('template_name', 'context_data',
  279
+            '_post_render_callbacks', '_request', '_current_app')
  280
+        for attr in template_attrs:
  281
+            self.assertFalse(hasattr(unpickled_response, attr))
  282
+
  283
+        # ...and requesting any of those attributes raises an exception
  284
+        for attr in template_attrs:
  285
+            with self.assertRaises(DiscardedAttributeError) as cm:
  286
+                getattr(unpickled_response, attr)
  287
+
  288
+    def test_repickling(self):
  289
+        response = SimpleTemplateResponse('first/test.html', {
  290
+                'value': 123,
  291
+                'fn': datetime.now,
  292
+            })
  293
+        self.assertRaises(ContentNotRenderedError,
  294
+                          pickle.dumps, response)
  295
+
  296
+        response.render()
  297
+        pickled_response = pickle.dumps(response)
  298
+        unpickled_response = pickle.loads(pickled_response)
  299
+        repickled_response = pickle.dumps(unpickled_response)
262 300
 
263 301
 
264 302
 class CustomURLConfTest(TestCase):

0 notes on commit 5fffe57

Please sign in to comment.
Something went wrong with that request. Please try again.