Skip to content

Commit 6cc6265

Browse files
Changed cache monkey-patching for Django 3.2+ (#1497)
* Changed cache monkey-patching for Django 3.2+ Changed cache monkey-patching for Django 3.2+ to iterate over existing caches and patch them individually rather than attempting to patch django.core.caches as a whole. The middleware.cache is still being patched as a whole in order to attempt to catch any cache usages before enable_instrumentation is called. Test cache panel is disabled for middleware before it. * Apply suggestions from code review Co-authored-by: Matthias Kestenholz <mk@feinheit.ch>
1 parent 6d3eb44 commit 6cc6265

File tree

7 files changed

+105
-14
lines changed

7 files changed

+105
-14
lines changed

debug_toolbar/panels/cache.py

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
except ImportError:
99
ConnectionProxy = None
1010

11+
import django
1112
from django.conf import settings
1213
from django.core import cache
1314
from django.core.cache import (
@@ -140,10 +141,27 @@ def decr_version(self, *args, **kwargs):
140141
return self.cache.decr_version(*args, **kwargs)
141142

142143

143-
class CacheHandlerPatch(CacheHandler):
144-
def __getitem__(self, alias):
145-
actual_cache = super().__getitem__(alias)
146-
return CacheStatTracker(actual_cache)
144+
if django.VERSION < (3, 2):
145+
146+
class CacheHandlerPatch(CacheHandler):
147+
def __getitem__(self, alias):
148+
actual_cache = super().__getitem__(alias)
149+
return CacheStatTracker(actual_cache)
150+
151+
152+
else:
153+
154+
class CacheHandlerPatch(CacheHandler):
155+
def __init__(self, settings=None):
156+
self._djdt_wrap = True
157+
super().__init__(settings=settings)
158+
159+
def create_connection(self, alias):
160+
actual_cache = super().create_connection(alias)
161+
if self._djdt_wrap:
162+
return CacheStatTracker(actual_cache)
163+
else:
164+
return actual_cache
147165

148166

149167
middleware_cache.caches = CacheHandlerPatch()
@@ -251,22 +269,40 @@ def title(self):
251269
)
252270

253271
def enable_instrumentation(self):
254-
if isinstance(middleware_cache.caches, CacheHandlerPatch):
255-
cache.caches = middleware_cache.caches
272+
if django.VERSION < (3, 2):
273+
if isinstance(middleware_cache.caches, CacheHandlerPatch):
274+
cache.caches = middleware_cache.caches
275+
else:
276+
cache.caches = CacheHandlerPatch()
256277
else:
257-
cache.caches = CacheHandlerPatch()
278+
for alias in cache.caches:
279+
if not isinstance(cache.caches[alias], CacheStatTracker):
280+
cache.caches[alias] = CacheStatTracker(cache.caches[alias])
281+
282+
if not isinstance(middleware_cache.caches, CacheHandlerPatch):
283+
middleware_cache.caches = cache.caches
258284

259285
# Wrap the patched cache inside Django's ConnectionProxy
260286
if ConnectionProxy:
261287
cache.cache = ConnectionProxy(cache.caches, DEFAULT_CACHE_ALIAS)
262288

263289
def disable_instrumentation(self):
264-
cache.caches = original_caches
265-
cache.cache = original_cache
266-
# While it can be restored to the original, any views that were
267-
# wrapped with the cache_page decorator will continue to use a
268-
# monkey patched cache.
269-
middleware_cache.caches = original_caches
290+
if django.VERSION < (3, 2):
291+
cache.caches = original_caches
292+
cache.cache = original_cache
293+
# While it can be restored to the original, any views that were
294+
# wrapped with the cache_page decorator will continue to use a
295+
# monkey patched cache.
296+
middleware_cache.caches = original_caches
297+
else:
298+
for alias in cache.caches:
299+
if isinstance(cache.caches[alias], CacheStatTracker):
300+
cache.caches[alias] = cache.caches[alias].cache
301+
if ConnectionProxy:
302+
cache.cache = ConnectionProxy(cache.caches, DEFAULT_CACHE_ALIAS)
303+
# While it can be restored to the original, any views that were
304+
# wrapped with the cache_page decorator will continue to use a
305+
# monkey patched cache.
270306

271307
def generate_stats(self, request, response):
272308
self.record_stats(

docs/changes.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ Change log
44
Next version
55
------------
66

7+
* Changed cache monkey-patching for Django 3.2+ to iterate over existing
8+
caches and patch them individually rather than attempting to patch
9+
``django.core.caches`` as a whole. The ``middleware.cache`` is still
10+
being patched as a whole in order to attempt to catch any cache
11+
usages before ``enable_instrumentation`` is called.
12+
713

814
3.2.2 (2021-08-14)
915
------------------

tests/middleware.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from django.core.cache import cache
2+
3+
4+
class UseCacheAfterToolbar:
5+
"""
6+
This middleware exists to use the cache before and after
7+
the toolbar is setup.
8+
"""
9+
10+
def __init__(self, get_response):
11+
self.get_response = get_response
12+
13+
def __call__(self, request):
14+
cache.set("UseCacheAfterToolbar.before", 1)
15+
response = self.get_response(request)
16+
cache.set("UseCacheAfterToolbar.after", 1)
17+
return response

tests/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
MEDIA_URL = "/media/" # Avoids https://code.djangoproject.com/ticket/21451
3131

3232
MIDDLEWARE = [
33+
"tests.middleware.UseCacheAfterToolbar",
3334
"debug_toolbar.middleware.DebugToolbarMiddleware",
3435
"django.middleware.security.SecurityMiddleware",
3536
"django.contrib.sessions.middleware.SessionMiddleware",

tests/test_integration.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import html5lib
77
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
88
from django.core import signing
9+
from django.core.cache import cache
910
from django.db import connection
1011
from django.http import HttpResponse
1112
from django.template.loader import get_template
@@ -102,6 +103,25 @@ def test_cache_page(self):
102103
self.client.get("/cached_view/")
103104
self.assertEqual(len(self.toolbar.get_panel_by_id("CachePanel").calls), 5)
104105

106+
def test_low_level_cache_view(self):
107+
"""Test cases when low level caching API is used within a request."""
108+
self.client.get("/cached_low_level_view/")
109+
self.assertEqual(len(self.toolbar.get_panel_by_id("CachePanel").calls), 2)
110+
self.client.get("/cached_low_level_view/")
111+
self.assertEqual(len(self.toolbar.get_panel_by_id("CachePanel").calls), 3)
112+
113+
def test_cache_disable_instrumentation(self):
114+
"""
115+
Verify that middleware cache usages before and after
116+
DebugToolbarMiddleware are not counted.
117+
"""
118+
self.assertIsNone(cache.set("UseCacheAfterToolbar.before", None))
119+
self.assertIsNone(cache.set("UseCacheAfterToolbar.after", None))
120+
self.client.get("/execute_sql/")
121+
self.assertEqual(cache.get("UseCacheAfterToolbar.before"), 1)
122+
self.assertEqual(cache.get("UseCacheAfterToolbar.after"), 1)
123+
self.assertEqual(len(self.toolbar.get_panel_by_id("CachePanel").calls), 0)
124+
105125
def test_is_toolbar_request(self):
106126
self.request.path = "/__debug__/render_panel/"
107127
self.assertTrue(self.toolbar.is_toolbar_request(self.request))
@@ -376,7 +396,7 @@ def test_view_returns_template_response(self):
376396
self.assertEqual(response.status_code, 200)
377397

378398
@override_settings(DEBUG_TOOLBAR_CONFIG={"DISABLE_PANELS": set()})
379-
def test_incercept_redirects(self):
399+
def test_intcercept_redirects(self):
380400
response = self.client.get("/redirect/")
381401
self.assertEqual(response.status_code, 200)
382402
# Link to LOCATION header.

tests/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
path("new_user/", views.new_user),
2121
path("execute_sql/", views.execute_sql),
2222
path("cached_view/", views.cached_view),
23+
path("cached_low_level_view/", views.cached_low_level_view),
2324
path("json_view/", views.json_view),
2425
path("redirect/", views.redirect_view),
2526
path("login_without_redirect/", LoginView.as_view(redirect_field_name=None)),

tests/views.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.contrib.auth.models import User
2+
from django.core.cache import cache
23
from django.http import HttpResponseRedirect, JsonResponse
34
from django.shortcuts import render
45
from django.template.response import TemplateResponse
@@ -33,6 +34,15 @@ def cached_view(request):
3334
return render(request, "base.html")
3435

3536

37+
def cached_low_level_view(request):
38+
key = "spam"
39+
value = cache.get(key)
40+
if not value:
41+
value = "eggs"
42+
cache.set(key, value, 60)
43+
return render(request, "base.html")
44+
45+
3646
def json_view(request):
3747
return JsonResponse({"foo": "bar"})
3848

0 commit comments

Comments
 (0)