Skip to content
10 changes: 8 additions & 2 deletions debug_toolbar/panels/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,13 @@ def _store_call_info(
template_info,
backend,
):
if name == "get" or name == "get_or_set":
if name == "get":
default = kwargs.get("default", args[1] if len(args) > 1 else None)
if return_value == default:
self.misses += 1
else:
self.hits += 1
elif name == "get_or_set":
if return_value is None:
self.misses += 1
else:
Expand All @@ -143,7 +149,7 @@ def _store_call_info(

def _record_call(self, cache, alias, name, original_method, args, kwargs):
# Some cache backends implement certain cache methods in terms of other cache
# methods (e.g. get_or_set() in terms of get() and add()). In order to only
# methods (e.g. get_or_set() in terms of get() and add()). In order to only
# record the calls made directly by the user code, set the cache's _djdt_panel
# attribute to None before invoking the original method, which will cause the
# monkey-patched cache methods to skip recording additional calls made during
Expand Down
9 changes: 8 additions & 1 deletion debug_toolbar/static/debug_toolbar/css/toolbar.css
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,14 @@
height: 50px;
}

#djDebug .djDebugPanelTitle code {
#djDebug .djdt-cache-note {
margin: 1em 0;
padding: 8px 10px;
border: 1px solid var(--djdt-table-border-color);
background-color: var(--djdt-panel-content-table-strip-background-color);
}
#djDebug .djDebugPanelTitle code,
#djDebug .djdt-cache-note code {
display: inline;
font-size: inherit;
}
Expand Down
5 changes: 5 additions & 0 deletions debug_toolbar/templates/debug_toolbar/panels/cache.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ <h4>{% translate "Summary" %}</h4>
</tr>
</tbody>
</table>
<p class="djdt-cache-note">
Cache hit/miss statistics for <code>cache.get()</code> calls may not always be accurate.
See <a href="https://github.com/django-commons/django-debug-toolbar/discussions/2441">the discussion</a>
for details and to share feedback about improving cache hit/miss tracking.
</p>
<h4>{% translate "Commands" %}</h4>
<table>
<thead>
Expand Down
3 changes: 3 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ Pending
* Stopped the history panel buttons from submitting their form when clicked
before the panel script has loaded, which navigated away from the page.
* Added support for Django 6.1.
* Improved cache hit/miss reporting in the Cache panel for ``cache.get()``
calls with a supplied default value, while documenting the remaining
ambiguity when a cached value equals the supplied default.

7.0.0 (2026-06-17)
------------------
Expand Down
6 changes: 6 additions & 0 deletions docs/panels.rst
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ Cache

Cache queries. Is incompatible with Django's per-site caching.

Cache hit/miss statistics for ``cache.get()`` calls may not always be
accurate. See the `discussion`_ for details and to share feedback about
improving cache hit/miss tracking.

.. _discussion: https://github.com/django-commons/django-debug-toolbar/discussions/2441

Signals
~~~~~~~

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

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

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

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

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

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)

Comment thread
Harsh3006 marked this conversation as resolved.
def test_get_with_keyword_default_is_cache_miss(self):
cache.cache.clear()

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

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

self.assertEqual(cache.cache.get(key="foo", default="default"), "default")
self.assertEqual(self.panel.hits, 0)
self.assertEqual(self.panel.misses, 1)

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

cache.cache.set("foo", "bar", version=1)
self.assertEqual(cache.cache.get(key="foo", version=1), "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 Expand Up @@ -114,6 +166,13 @@ def test_get_or_set_does_not_override_existing_value(self):
},
)

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

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

def test_insert_content(self):
"""
Test that the panel only inserts content after generate_stats and
Expand Down