Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions debug_toolbar/panels/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class CachePanel(Panel):
is_async = True

_context_locals = Local()
_missing_key = object()

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down Expand Up @@ -116,7 +117,12 @@ def _store_call_info(
template_info,
backend,
):
if name == "get" or name == "get_or_set":
if name == "get":
if return_value is self._missing_key:
self.misses += 1
else:
self.hits += 1
elif name == "get_or_set":
if return_value is None:
self.misses += 1
else:
Expand Down Expand Up @@ -149,9 +155,22 @@ def _record_call(self, cache, alias, name, original_method, args, kwargs):
# monkey-patched cache methods to skip recording additional calls made during
# the course of this call, and then reset it back afterward.
cache._djdt_panel = None
user_default = None
try:
start_time = perf_counter()
value = original_method(*args, **kwargs)
if name == "get":
user_default = kwargs.get("default", args[1] if len(args) > 1 else None)
# Replace the caller's default with an internal sentinel so a cache miss
# can be distinguished from a cached value equal to the supplied default.
if "default" in kwargs:
call_args = args
call_kwargs = {**kwargs, "default": self._missing_key}
else:
call_args = (args[0], self._missing_key, *args[2:])
call_kwargs = kwargs
value = original_method(*call_args, **call_kwargs)
else:
value = original_method(*args, **kwargs)
t = perf_counter() - start_time
finally:
cache._djdt_panel = self
Expand All @@ -166,6 +185,10 @@ def _record_call(self, cache, alias, name, original_method, args, kwargs):
template_info=get_template_info(),
backend=f"{alias} ({type(cache).__name__})",
)
# Preserve the original cache.get() behavior by returning the caller's
# default instead of the internal sentinel on a cache miss.
if name == "get" and value is self._missing_key:
return user_default
return value

# Implement the Panel API
Expand Down
4 changes: 4 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ Pending
* Fixed the Django version check in the SQL panel test suite for Django's
boolean parameter handling.

* Fixed incorrect cache hit/miss reporting in the Cache panel for
``cache.get()`` when a default value is supplied or the cached value is
``None``.

7.0.0 (2026-06-17)
------------------

Expand Down
31 changes: 31 additions & 0 deletions tests/panels/test_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,37 @@ def test_hits_and_misses(self):
self.assertEqual(self.panel.hits, 4)
self.assertEqual(self.panel.misses, 2)

def test_cached_none_counts_as_hit(self):
cache.cache.clear()

cache.cache.set("foo", None)
self.assertIsNone(cache.cache.get("foo"))
self.assertEqual(self.panel.hits, 1)
self.assertEqual(self.panel.misses, 0)

def test_missing_key_with_default_counts_as_miss(self):
cache.cache.clear()

self.assertIsNone(cache.cache.get("foo", None))
self.assertEqual(self.panel.hits, 0)
self.assertEqual(self.panel.misses, 1)

def test_missing_key_returns_supplied_default(self):
cache.cache.clear()

self.assertEqual(cache.cache.get("foo", "bar"), "bar")
self.assertEqual(self.panel.hits, 0)
self.assertEqual(self.panel.misses, 1)

def test_cached_value_equal_to_default_counts_as_hit(self):
cache.cache.clear()

cache.cache.set("foo", "bar")
self.assertEqual(cache.cache.get("foo", "bar"), "bar")

self.assertEqual(self.panel.hits, 1)
self.assertEqual(self.panel.misses, 0)

def test_get_or_set_value(self):
cache.cache.get_or_set("baz", "val")
self.assertEqual(cache.cache.get("baz"), "val")
Expand Down
Loading