Fixed #16326 -- Fixed re-pickling of unpickled TemplateResponse instances. Thanks, natrius and lrekucki.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16568 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Jannis Leidel 2011-07-29 09:40:50 +00:00
parent 94f7481396
commit 5fffe574bd
2 changed files with 87 additions and 43 deletions

View File

@ -1,22 +1,28 @@
from django.http import HttpResponse from django.http import HttpResponse
from django.template import loader, Context, RequestContext from django.template import loader, Context, RequestContext
class ContentNotRenderedError(Exception): class ContentNotRenderedError(Exception):
pass pass
class DiscardedAttributeError(AttributeError):
pass
class SimpleTemplateResponse(HttpResponse): class SimpleTemplateResponse(HttpResponse):
rendering_attrs = ['template_name', 'context_data', '_post_render_callbacks']
def __init__(self, template, context=None, mimetype=None, status=None, def __init__(self, template, context=None, mimetype=None, status=None,
content_type=None): content_type=None):
# It would seem obvious to call these next two members 'template' and # It would seem obvious to call these next two members 'template' and
# 'context', but those names are reserved as part of the test Client API. # 'context', but those names are reserved as part of the test Client
# To avoid the name collision, we use # API. To avoid the name collision, we use tricky-to-debug problems
# tricky-to-debug problems
self.template_name = template self.template_name = template
self.context_data = context self.context_data = context
# _is_rendered tracks whether the template and context has been baked into # _is_rendered tracks whether the template and context has been
# a final response. # baked into a final response.
self._is_rendered = False self._is_rendered = False
self._post_render_callbacks = [] self._post_render_callbacks = []
@ -36,13 +42,21 @@ class SimpleTemplateResponse(HttpResponse):
""" """
obj_dict = self.__dict__.copy() obj_dict = self.__dict__.copy()
if not self._is_rendered: if not self._is_rendered:
raise ContentNotRenderedError('The response content must be rendered before it can be pickled.') raise ContentNotRenderedError('The response content must be '
del obj_dict['template_name'] 'rendered before it can be pickled.')
del obj_dict['context_data'] for attr in self.rendering_attrs:
del obj_dict['_post_render_callbacks'] if attr in obj_dict:
del obj_dict[attr]
return obj_dict return obj_dict
def __getattr__(self, name):
if name in self.rendering_attrs:
raise DiscardedAttributeError('The %s attribute was discarded '
'when this %s class was pickled.' %
(name, self.__class__.__name__))
return super(SimpleTemplateResponse, self).__getattr__(name)
def resolve_template(self, template): def resolve_template(self, template):
"Accepts a template object, path-to-template or list of paths" "Accepts a template object, path-to-template or list of paths"
if isinstance(template, (list, tuple)): if isinstance(template, (list, tuple)):
@ -53,7 +67,7 @@ class SimpleTemplateResponse(HttpResponse):
return template return template
def resolve_context(self, context): def resolve_context(self, context):
"""Convert context data into a full Context object """Converts context data into a full Context object
(assuming it isn't already a Context object). (assuming it isn't already a Context object).
""" """
if isinstance(context, Context): if isinstance(context, Context):
@ -76,9 +90,10 @@ class SimpleTemplateResponse(HttpResponse):
return content return content
def add_post_render_callback(self, callback): def add_post_render_callback(self, callback):
"""Add a new post-rendering callback. """Adds a new post-rendering callback.
If the response has already been rendered, invoke the callback immediately. If the response has already been rendered,
invoke the callback immediately.
""" """
if self._is_rendered: if self._is_rendered:
callback(self) callback(self)
@ -86,7 +101,7 @@ class SimpleTemplateResponse(HttpResponse):
self._post_render_callbacks.append(callback) self._post_render_callbacks.append(callback)
def render(self): def render(self):
"""Render (thereby finalizing) the content of the response. """Renders (thereby finalizing) the content of the response.
If the content has already been rendered, this is a no-op. If the content has already been rendered, this is a no-op.
@ -101,20 +116,25 @@ class SimpleTemplateResponse(HttpResponse):
retval = newretval retval = newretval
return retval return retval
is_rendered = property(lambda self: self._is_rendered) @property
def is_rendered(self):
return self._is_rendered
def __iter__(self): def __iter__(self):
if not self._is_rendered: if not self._is_rendered:
raise ContentNotRenderedError('The response content must be rendered before it can be iterated over.') raise ContentNotRenderedError('The response content must be '
'rendered before it can be iterated over.')
return super(SimpleTemplateResponse, self).__iter__() return super(SimpleTemplateResponse, self).__iter__()
def _get_content(self): def _get_content(self):
if not self._is_rendered: if not self._is_rendered:
raise ContentNotRenderedError('The response content must be rendered before it can be accessed.') raise ContentNotRenderedError('The response content must be '
'rendered before it can be accessed.')
return super(SimpleTemplateResponse, self)._get_content() return super(SimpleTemplateResponse, self)._get_content()
def _set_content(self, value): def _set_content(self, value):
"Sets the content for the response" """Sets the content for the response
"""
super(SimpleTemplateResponse, self)._set_content(value) super(SimpleTemplateResponse, self)._set_content(value)
self._is_rendered = True self._is_rendered = True
@ -122,6 +142,9 @@ class SimpleTemplateResponse(HttpResponse):
class TemplateResponse(SimpleTemplateResponse): class TemplateResponse(SimpleTemplateResponse):
rendering_attrs = SimpleTemplateResponse.rendering_attrs + \
['_request', '_current_app']
def __init__(self, request, template, context=None, mimetype=None, def __init__(self, request, template, context=None, mimetype=None,
status=None, content_type=None, current_app=None): status=None, content_type=None, current_app=None):
# self.request gets over-written by django.test.client.Client - and # self.request gets over-written by django.test.client.Client - and
@ -134,27 +157,10 @@ class TemplateResponse(SimpleTemplateResponse):
super(TemplateResponse, self).__init__( super(TemplateResponse, self).__init__(
template, context, mimetype, status, content_type) template, context, mimetype, status, content_type)
def __getstate__(self):
"""Pickling support function.
Ensures that the object can't be pickled before it has been
rendered, and that the pickled state only includes rendered
data, not the data used to construct the response.
"""
obj_dict = super(TemplateResponse, self).__getstate__()
del obj_dict['_request']
del obj_dict['_current_app']
return obj_dict
def resolve_context(self, context): def resolve_context(self, context):
"""Convert context data into a full RequestContext object """Convert context data into a full RequestContext object
(assuming it isn't already a Context object). (assuming it isn't already a Context object).
""" """
if isinstance(context, Context): if isinstance(context, Context):
return context return context
else: return RequestContext(self._request, context, current_app=self._current_app)
return RequestContext(self._request, context, current_app=self._current_app)

View File

@ -1,3 +1,4 @@
from __future__ import with_statement
from datetime import datetime from datetime import datetime
import os import os
import pickle import pickle
@ -8,7 +9,8 @@ from django.conf import settings
import django.template.context import django.template.context
from django.template import Template, Context from django.template import Template, Context
from django.template.response import (TemplateResponse, SimpleTemplateResponse, from django.template.response import (TemplateResponse, SimpleTemplateResponse,
ContentNotRenderedError) ContentNotRenderedError,
DiscardedAttributeError)
def test_processor(request): def test_processor(request):
return {'processors': 'yes'} return {'processors': 'yes'}
@ -190,9 +192,27 @@ class SimpleTemplateResponseTest(BaseTemplateResponseTest):
# ...and the unpickled reponse doesn't have the # ...and the unpickled reponse doesn't have the
# template-related attributes, so it can't be re-rendered # template-related attributes, so it can't be re-rendered
self.assertFalse(hasattr(unpickled_response, 'template_name')) template_attrs = ('template_name', 'context_data', '_post_render_callbacks')
self.assertFalse(hasattr(unpickled_response, 'context_data')) for attr in template_attrs:
self.assertFalse(hasattr(unpickled_response, '_post_render_callbacks')) self.assertFalse(hasattr(unpickled_response, attr))
# ...and requesting any of those attributes raises an exception
for attr in template_attrs:
with self.assertRaises(DiscardedAttributeError) as cm:
getattr(unpickled_response, attr)
def test_repickling(self):
response = SimpleTemplateResponse('first/test.html', {
'value': 123,
'fn': datetime.now,
})
self.assertRaises(ContentNotRenderedError,
pickle.dumps, response)
response.render()
pickled_response = pickle.dumps(response)
unpickled_response = pickle.loads(pickled_response)
repickled_response = pickle.dumps(unpickled_response)
class TemplateResponseTest(BaseTemplateResponseTest): class TemplateResponseTest(BaseTemplateResponseTest):
@ -255,10 +275,28 @@ class TemplateResponseTest(BaseTemplateResponseTest):
# ...and the unpickled reponse doesn't have the # ...and the unpickled reponse doesn't have the
# template-related attributes, so it can't be re-rendered # template-related attributes, so it can't be re-rendered
self.assertFalse(hasattr(unpickled_response, '_request')) template_attrs = ('template_name', 'context_data',
self.assertFalse(hasattr(unpickled_response, 'template_name')) '_post_render_callbacks', '_request', '_current_app')
self.assertFalse(hasattr(unpickled_response, 'context_data')) for attr in template_attrs:
self.assertFalse(hasattr(unpickled_response, '_post_render_callbacks')) self.assertFalse(hasattr(unpickled_response, attr))
# ...and requesting any of those attributes raises an exception
for attr in template_attrs:
with self.assertRaises(DiscardedAttributeError) as cm:
getattr(unpickled_response, attr)
def test_repickling(self):
response = SimpleTemplateResponse('first/test.html', {
'value': 123,
'fn': datetime.now,
})
self.assertRaises(ContentNotRenderedError,
pickle.dumps, response)
response.render()
pickled_response = pickle.dumps(response)
unpickled_response = pickle.loads(pickled_response)
repickled_response = pickle.dumps(unpickled_response)
class CustomURLConfTest(TestCase): class CustomURLConfTest(TestCase):