Skip to content

Replace DebugConfiguredStorage with URLMixin in staticfiles panel #2097

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Mar 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5e98abd
Refs #2068: Do not reinstantiate staticfiles storage classes
matthiask Feb 6, 2025
f44ab85
Add URLMixin tests for staticfiles panel
dr-rompecabezas Mar 5, 2025
8c09baa
[pre-commit.ci] pre-commit autoupdate
pre-commit-ci[bot] Feb 10, 2025
1991b88
Update package metadata to include well-known labels (#2078)
andoriyaprashant Feb 14, 2025
004a581
Pinned django-csp's version used for our tests (#2084)
matthiask Feb 25, 2025
6a076d6
[pre-commit.ci] pre-commit autoupdate (#2080)
pre-commit-ci[bot] Feb 25, 2025
4418293
Add resources section to the documentation (#2081)
dr-rompecabezas Feb 25, 2025
b1e7a8c
Make show toolbar callback function async/sync compatible. (#2066)
tim-schilling Feb 25, 2025
d1362f5
Fix typo in resources.rst (#2085)
dr-rompecabezas Feb 25, 2025
8a66da1
Add link to contributing documentation in CONTRIBUTING.md (#2086)
blingblin-g Feb 26, 2025
f33c416
Pull translations from transifex (#2089)
matthiask Mar 1, 2025
ec4837c
Replace ESLint and prettier with biome (#2090)
matthiask Mar 1, 2025
50c1314
Enable the useSingleVarDeclarator style rule
matthiask Mar 1, 2025
25eb3f9
Enable the noUselessElse rule
matthiask Mar 1, 2025
964b6d8
Enable the noParameterAssign rule
matthiask Mar 1, 2025
301f2cd
Enable the noArguments rule
matthiask Mar 1, 2025
9535aac
Prefer template literals to string concatenation
matthiask Mar 1, 2025
9b20825
Prefer arrow functions
matthiask Mar 1, 2025
4b05d1f
Enable the noAssignInExpressions rule
matthiask Mar 1, 2025
01b9261
Replace forEach loops with for...of loops
matthiask Mar 1, 2025
f8f9cdd
[pre-commit.ci] pre-commit autoupdate (#2095)
pre-commit-ci[bot] Mar 4, 2025
2b34fcb
Add help command to the Makefile (#2094)
mrbazzan Mar 4, 2025
5a3b805
Update changelog: added URLMixin
dr-rompecabezas Mar 5, 2025
114ff30
Merge branch 'main' into add-tests-for-2068
matthiask Mar 5, 2025
2a45f80
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 5, 2025
b6b21b0
Add words to the spelling wordlist
matthiask Mar 5, 2025
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
61 changes: 18 additions & 43 deletions debug_toolbar/panels/staticfiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
------------------
Expand Down
3 changes: 3 additions & 0 deletions docs/spelling_wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ async
backend
backends
backported
biome
checkbox
contrib
dicts
Expand Down Expand Up @@ -50,13 +51,15 @@ pylibmc
pyupgrade
querysets
refactoring
reinitializing
resizing
runserver
spellchecking
spooler
stacktrace
stacktraces
startup
staticfiles
theming
timeline
tox
Expand Down
45 changes: 44 additions & 1 deletion tests/panels/test_staticfiles.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down Expand Up @@ -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)