Skip to content

Commit e885fe9

Browse files
Replace DebugConfiguredStorage with URLMixin in staticfiles panel (#2097)
* Refs #2068: Do not reinstantiate staticfiles storage classes * Add URLMixin tests for staticfiles panel - Test storage state preservation to ensure URLMixin doesn't affect storage attributes - Test context variable lifecycle for static file tracking - Test multiple initialization safety to prevent URLMixin stacking * Update changelog: added URLMixin * Add words to the spelling wordlist Co-authored-by: Matthias Kestenholz <[email protected]>
1 parent b5dc19c commit e885fe9

File tree

4 files changed

+66
-44
lines changed

4 files changed

+66
-44
lines changed

debug_toolbar/panels/staticfiles.py

+18-43
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@
33
from contextvars import ContextVar
44
from os.path import join, normpath
55

6-
from django.conf import settings
76
from django.contrib.staticfiles import finders, storage
87
from django.dispatch import Signal
9-
from django.utils.functional import LazyObject
108
from django.utils.translation import gettext_lazy as _, ngettext
119

1210
from debug_toolbar import panels
@@ -37,46 +35,21 @@ def url(self):
3735
record_static_file_signal = Signal()
3836

3937

40-
class DebugConfiguredStorage(LazyObject):
41-
"""
42-
A staticfiles storage class to be used for collecting which paths
43-
are resolved by using the {% static %} template tag (which uses the
44-
`url` method).
45-
"""
46-
47-
def _setup(self):
48-
try:
49-
# From Django 4.2 use django.core.files.storage.storages in favor
50-
# of the deprecated django.core.files.storage.get_storage_class
51-
from django.core.files.storage import storages
52-
53-
configured_storage_cls = storages["staticfiles"].__class__
54-
except ImportError:
55-
# Backwards compatibility for Django versions prior to 4.2
56-
from django.core.files.storage import get_storage_class
57-
58-
configured_storage_cls = get_storage_class(settings.STATICFILES_STORAGE)
59-
60-
class DebugStaticFilesStorage(configured_storage_cls):
61-
def url(self, path):
62-
url = super().url(path)
63-
with contextlib.suppress(LookupError):
64-
# For LookupError:
65-
# The ContextVar wasn't set yet. Since the toolbar wasn't properly
66-
# configured to handle this request, we don't need to capture
67-
# the static file.
68-
request_id = request_id_context_var.get()
69-
record_static_file_signal.send(
70-
sender=self,
71-
staticfile=StaticFile(path=str(path), url=url),
72-
request_id=request_id,
73-
)
74-
return url
75-
76-
self._wrapped = DebugStaticFilesStorage()
77-
78-
79-
_original_storage = storage.staticfiles_storage
38+
class URLMixin:
39+
def url(self, path):
40+
url = super().url(path)
41+
with contextlib.suppress(LookupError):
42+
# For LookupError:
43+
# The ContextVar wasn't set yet. Since the toolbar wasn't properly
44+
# configured to handle this request, we don't need to capture
45+
# the static file.
46+
request_id = request_id_context_var.get()
47+
record_static_file_signal.send(
48+
sender=self,
49+
staticfile=StaticFile(path=str(path), url=url),
50+
request_id=request_id,
51+
)
52+
return url
8053

8154

8255
class StaticFilesPanel(panels.Panel):
@@ -103,7 +76,9 @@ def __init__(self, *args, **kwargs):
10376

10477
@classmethod
10578
def ready(cls):
106-
storage.staticfiles_storage = DebugConfiguredStorage()
79+
cls = storage.staticfiles_storage.__class__
80+
if URLMixin not in cls.mro():
81+
cls.__bases__ = (URLMixin, *cls.__bases__)
10782

10883
def _store_static_files_signal_handler(self, sender, staticfile, **kwargs):
10984
# Only record the static file if the request_id matches the one

docs/changes.rst

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Pending
1414
* Replaced ESLint and prettier with biome in our pre-commit configuration.
1515
* Added a Makefile target (``make help``) to get a quick overview
1616
of each target.
17+
* Avoided reinitializing the staticfiles storage during instrumentation.
1718

1819
5.0.1 (2025-01-13)
1920
------------------

docs/spelling_wordlist.txt

+3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ async
1313
backend
1414
backends
1515
backported
16+
biome
1617
checkbox
1718
contrib
1819
dicts
@@ -50,13 +51,15 @@ pylibmc
5051
pyupgrade
5152
querysets
5253
refactoring
54+
reinitializing
5355
resizing
5456
runserver
5557
spellchecking
5658
spooler
5759
stacktrace
5860
stacktraces
5961
startup
62+
staticfiles
6063
theming
6164
timeline
6265
tox

tests/panels/test_staticfiles.py

+44-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from pathlib import Path
22

33
from django.conf import settings
4-
from django.contrib.staticfiles import finders
4+
from django.contrib.staticfiles import finders, storage
55
from django.shortcuts import render
66
from django.test import AsyncRequestFactory, RequestFactory
77

8+
from debug_toolbar.panels.staticfiles import URLMixin
9+
810
from ..base import BaseTestCase
911

1012

@@ -76,3 +78,44 @@ def get_response(request):
7678
self.panel.generate_stats(self.request, response)
7779
self.assertEqual(self.panel.num_used, 1)
7880
self.assertIn('"/static/additional_static/base.css"', self.panel.content)
81+
82+
def test_storage_state_preservation(self):
83+
"""Ensure the URLMixin doesn't affect storage state"""
84+
original_storage = storage.staticfiles_storage
85+
original_attrs = dict(original_storage.__dict__)
86+
87+
# Trigger mixin injection
88+
self.panel.ready()
89+
90+
# Verify all original attributes are preserved
91+
self.assertEqual(original_attrs, dict(original_storage.__dict__))
92+
93+
def test_context_variable_lifecycle(self):
94+
"""Test the request_id context variable lifecycle"""
95+
from debug_toolbar.panels.staticfiles import request_id_context_var
96+
97+
# Should not raise when context not set
98+
url = storage.staticfiles_storage.url("test.css")
99+
self.assertTrue(url.startswith("/static/"))
100+
101+
# Should track when context is set
102+
token = request_id_context_var.set("test-request-id")
103+
try:
104+
url = storage.staticfiles_storage.url("test.css")
105+
self.assertTrue(url.startswith("/static/"))
106+
# Verify file was tracked
107+
self.assertIn("test.css", [f.path for f in self.panel.used_paths])
108+
finally:
109+
request_id_context_var.reset(token)
110+
111+
def test_multiple_initialization(self):
112+
"""Ensure multiple panel initializations don't stack URLMixin"""
113+
storage_class = storage.staticfiles_storage.__class__
114+
115+
# Initialize panel multiple times
116+
for _ in range(3):
117+
self.panel.ready()
118+
119+
# Verify URLMixin appears exactly once in bases
120+
mixin_count = sum(1 for base in storage_class.__bases__ if base == URLMixin)
121+
self.assertEqual(mixin_count, 1)

0 commit comments

Comments
 (0)