diff --git a/rest_framework/response.py b/rest_framework/response.py index 7a459c8f5..006d7eebb 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -45,3 +45,13 @@ class Response(SimpleTemplateResponse): # TODO: Deprecate and use a template tag instead # TODO: Status code text for RFC 6585 status codes return STATUS_CODE_TEXT.get(self.status_code, '') + + def __getstate__(self): + """ + Remove attributes from the response that shouldn't be cached + """ + state = super(Response, self).__getstate__() + for key in ('accepted_renderer', 'renderer_context', 'data'): + if key in state: + del state[key] + return state diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py index 951b1e72b..b48f85e4e 100644 --- a/rest_framework/runtests/settings.py +++ b/rest_framework/runtests/settings.py @@ -21,6 +21,12 @@ DATABASES = { } } +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + } +} + # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # although not all choices may be available on all operating systems. diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py index 48d8d9bd7..9be4b1146 100644 --- a/rest_framework/tests/renderers.py +++ b/rest_framework/tests/renderers.py @@ -1,6 +1,8 @@ +import pickle import re from django.conf.urls.defaults import patterns, url, include +from django.core.cache import cache from django.test import TestCase from django.test.client import RequestFactory @@ -83,6 +85,7 @@ class HTMLView1(APIView): urlpatterns = patterns('', url(r'^.*\.(?P.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])), url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])), + url(r'^cache$', MockGETView.as_view()), url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])), url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])), url(r'^html$', HTMLView.as_view()), @@ -416,3 +419,89 @@ class XMLRendererTestCase(TestCase): self.assertTrue(xml.startswith('\n')) self.assertTrue(xml.endswith('')) self.assertTrue(string in xml, '%r not in %r' % (string, xml)) + + +# Tests for caching issue, #346 +class CacheRenderTest(TestCase): + """ + Tests specific to caching responses + """ + + urls = 'rest_framework.tests.renderers' + + cache_key = 'just_a_cache_key' + + @classmethod + def _get_pickling_errors(cls, obj, seen=None): + """ Return any errors that would be raised if `obj' is pickled + Courtesy of koffie @ http://stackoverflow.com/a/7218986/109897 + """ + if seen == None: + seen = [] + try: + state = obj.__getstate__() + except AttributeError: + return + if state == None: + return + if isinstance(state,tuple): + if not isinstance(state[0],dict): + state=state[1] + else: + state=state[0].update(state[1]) + result = {} + for i in state: + try: + pickle.dumps(state[i],protocol=2) + except pickle.PicklingError: + if not state[i] in seen: + seen.append(state[i]) + result[i] = cls._get_pickling_errors(state[i],seen) + return result + + def http_resp(self, http_method, url): + """ + Simple wrapper for Client http requests + Removes the `client' and `request' attributes from as they are + added by django.test.client.Client and not part of caching + responses outside of tests. + """ + method = getattr(self.client, http_method) + resp = method(url) + del resp.client, resp.request + return resp + + def test_obj_pickling(self): + """ + Test that responses are properly pickled + """ + resp = self.http_resp('get', '/cache') + + # Make sure that no pickling errors occurred + self.assertEqual(self._get_pickling_errors(resp), {}) + + # Unfortunately LocMem backend doesn't raise PickleErrors but returns + # None instead. + cache.set(self.cache_key, resp) + self.assertTrue(cache.get(self.cache_key) is not None) + + def test_head_caching(self): + """ + Test caching of HEAD requests + """ + resp = self.http_resp('head', '/cache') + cache.set(self.cache_key, resp) + + cached_resp = cache.get(self.cache_key) + self.assertIsInstance(cached_resp, Response) + + def test_get_caching(self): + """ + Test caching of GET requests + """ + resp = self.http_resp('get', '/cache') + cache.set(self.cache_key, resp) + + cached_resp = cache.get(self.cache_key) + self.assertIsInstance(cached_resp, Response) + self.assertEqual(cached_resp.content, resp.content)