diff --git a/debug_toolbar/panels/staticfiles.py b/debug_toolbar/panels/staticfiles.py index 3dd29e979..9f1970ef6 100644 --- a/debug_toolbar/panels/staticfiles.py +++ b/debug_toolbar/panels/staticfiles.py @@ -3,10 +3,8 @@ from contextvars import ContextVar from os.path import join, normpath -from django.conf import settings from django.contrib.staticfiles import finders, storage from django.dispatch import Signal -from django.utils.functional import LazyObject from django.utils.translation import gettext_lazy as _, ngettext from debug_toolbar import panels @@ -37,46 +35,21 @@ def url(self): record_static_file_signal = Signal() -class DebugConfiguredStorage(LazyObject): - """ - A staticfiles storage class to be used for collecting which paths - are resolved by using the {% static %} template tag (which uses the - `url` method). - """ - - def _setup(self): - try: - # From Django 4.2 use django.core.files.storage.storages in favor - # of the deprecated django.core.files.storage.get_storage_class - from django.core.files.storage import storages - - configured_storage_cls = storages["staticfiles"].__class__ - except ImportError: - # Backwards compatibility for Django versions prior to 4.2 - from django.core.files.storage import get_storage_class - - configured_storage_cls = get_storage_class(settings.STATICFILES_STORAGE) - - class DebugStaticFilesStorage(configured_storage_cls): - def url(self, path): - url = super().url(path) - with contextlib.suppress(LookupError): - # For LookupError: - # The ContextVar wasn't set yet. Since the toolbar wasn't properly - # configured to handle this request, we don't need to capture - # the static file. - request_id = request_id_context_var.get() - record_static_file_signal.send( - sender=self, - staticfile=StaticFile(path=str(path), url=url), - request_id=request_id, - ) - return url - - self._wrapped = DebugStaticFilesStorage() - - -_original_storage = storage.staticfiles_storage +class URLMixin: + def url(self, path): + url = super().url(path) + with contextlib.suppress(LookupError): + # For LookupError: + # The ContextVar wasn't set yet. Since the toolbar wasn't properly + # configured to handle this request, we don't need to capture + # the static file. + request_id = request_id_context_var.get() + record_static_file_signal.send( + sender=self, + staticfile=StaticFile(path=str(path), url=url), + request_id=request_id, + ) + return url class StaticFilesPanel(panels.Panel): @@ -103,7 +76,9 @@ def __init__(self, *args, **kwargs): @classmethod def ready(cls): - storage.staticfiles_storage = DebugConfiguredStorage() + cls = storage.staticfiles_storage.__class__ + if URLMixin not in cls.mro(): + cls.__bases__ = (URLMixin, *cls.__bases__) def _store_static_files_signal_handler(self, sender, staticfile, **kwargs): # Only record the static file if the request_id matches the one diff --git a/docs/changes.rst b/docs/changes.rst index 8ca86692d..608843e0f 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -14,6 +14,7 @@ Pending * Replaced ESLint and prettier with biome in our pre-commit configuration. * Added a Makefile target (``make help``) to get a quick overview of each target. +* Avoided reinitializing the staticfiles storage during instrumentation. 5.0.1 (2025-01-13) ------------------ diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 662e6df4f..8db8072b7 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -13,6 +13,7 @@ async backend backends backported +biome checkbox contrib dicts @@ -50,6 +51,7 @@ pylibmc pyupgrade querysets refactoring +reinitializing resizing runserver spellchecking @@ -57,6 +59,7 @@ spooler stacktrace stacktraces startup +staticfiles theming timeline tox diff --git a/tests/panels/test_staticfiles.py b/tests/panels/test_staticfiles.py index 334b0b6a3..2306c8365 100644 --- a/tests/panels/test_staticfiles.py +++ b/tests/panels/test_staticfiles.py @@ -1,10 +1,12 @@ from pathlib import Path from django.conf import settings -from django.contrib.staticfiles import finders +from django.contrib.staticfiles import finders, storage from django.shortcuts import render from django.test import AsyncRequestFactory, RequestFactory +from debug_toolbar.panels.staticfiles import URLMixin + from ..base import BaseTestCase @@ -76,3 +78,44 @@ def get_response(request): self.panel.generate_stats(self.request, response) self.assertEqual(self.panel.num_used, 1) self.assertIn('"/static/additional_static/base.css"', self.panel.content) + + def test_storage_state_preservation(self): + """Ensure the URLMixin doesn't affect storage state""" + original_storage = storage.staticfiles_storage + original_attrs = dict(original_storage.__dict__) + + # Trigger mixin injection + self.panel.ready() + + # Verify all original attributes are preserved + self.assertEqual(original_attrs, dict(original_storage.__dict__)) + + def test_context_variable_lifecycle(self): + """Test the request_id context variable lifecycle""" + from debug_toolbar.panels.staticfiles import request_id_context_var + + # Should not raise when context not set + url = storage.staticfiles_storage.url("test.css") + self.assertTrue(url.startswith("/static/")) + + # Should track when context is set + token = request_id_context_var.set("test-request-id") + try: + url = storage.staticfiles_storage.url("test.css") + self.assertTrue(url.startswith("/static/")) + # Verify file was tracked + self.assertIn("test.css", [f.path for f in self.panel.used_paths]) + finally: + request_id_context_var.reset(token) + + def test_multiple_initialization(self): + """Ensure multiple panel initializations don't stack URLMixin""" + storage_class = storage.staticfiles_storage.__class__ + + # Initialize panel multiple times + for _ in range(3): + self.panel.ready() + + # Verify URLMixin appears exactly once in bases + mixin_count = sum(1 for base in storage_class.__bases__ if base == URLMixin) + self.assertEqual(mixin_count, 1)