File: staticfiles.py

package info (click to toggle)
python-django-debug-toolbar 1%3A5.2.0-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 1,984 kB
  • sloc: python: 6,880; javascript: 631; makefile: 62; sh: 16
file content (171 lines) | stat: -rw-r--r-- 5,814 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
import contextlib
import uuid
from contextvars import ContextVar
from os.path import join, normpath

from django.contrib.staticfiles import finders, storage
from django.dispatch import Signal
from django.utils.translation import gettext_lazy as _, ngettext

from debug_toolbar import panels


class StaticFile:
    """
    Representing the different properties of a static file.
    """

    def __init__(self, *, path, url):
        self.path = path
        self._url = url

    def __str__(self):
        return self.path

    def real_path(self):
        return finders.find(self.path)

    def url(self):
        return self._url


# This will record and map the StaticFile instances with its associated
# request across threads and async concurrent requests state.
request_id_context_var = ContextVar("djdt_request_id_store")
record_static_file_signal = Signal()


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):
    """
    A panel to display the found staticfiles.
    """

    is_async = True
    name = "Static files"
    template = "debug_toolbar/panels/staticfiles.html"

    @property
    def title(self):
        return _("Static files (%(num_found)s found, %(num_used)s used)") % {
            "num_found": self.num_found,
            "num_used": self.num_used,
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.num_found = 0
        self.used_paths = []
        self.request_id = str(uuid.uuid4())

    @classmethod
    def ready(cls):
        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
        # that was used to create the panel.
        # as sender of the signal and this handler will have multiple
        # concurrent connections and we want to avoid storing of same
        # staticfile from other connections as well.
        if request_id_context_var.get() == self.request_id:
            self.used_paths.append(staticfile)

    def enable_instrumentation(self):
        self.ctx_token = request_id_context_var.set(self.request_id)
        record_static_file_signal.connect(self._store_static_files_signal_handler)

    def disable_instrumentation(self):
        record_static_file_signal.disconnect(self._store_static_files_signal_handler)
        request_id_context_var.reset(self.ctx_token)

    @property
    def num_used(self):
        stats = self.get_stats()
        return stats and stats["num_used"]

    nav_title = _("Static files")

    @property
    def nav_subtitle(self):
        num_used = self.num_used
        return ngettext(
            "%(num_used)s file used", "%(num_used)s files used", num_used
        ) % {"num_used": num_used}

    def generate_stats(self, request, response):
        self.record_stats(
            {
                "num_found": self.num_found,
                "num_used": len(self.used_paths),
                "staticfiles": self.used_paths,
                "staticfiles_apps": self.get_staticfiles_apps(),
                "staticfiles_dirs": self.get_staticfiles_dirs(),
                "staticfiles_finders": self.get_staticfiles_finders(),
            }
        )

    def get_staticfiles_finders(self):
        """
        Returns a sorted mapping between the finder path and the list
        of relative and file system paths which that finder was able
        to find.
        """
        finders_mapping = {}
        for finder in finders.get_finders():
            try:
                for path, finder_storage in finder.list([]):
                    if getattr(finder_storage, "prefix", None):
                        prefixed_path = join(finder_storage.prefix, path)
                    else:
                        prefixed_path = path
                    finder_cls = finder.__class__
                    finder_path = ".".join([finder_cls.__module__, finder_cls.__name__])
                    real_path = finder_storage.path(path)
                    payload = (prefixed_path, real_path)
                    finders_mapping.setdefault(finder_path, []).append(payload)
                    self.num_found += 1
            except OSError:
                # This error should be captured and presented as a part of run_checks.
                pass
        return finders_mapping

    def get_staticfiles_dirs(self):
        """
        Returns a list of paths to inspect for additional static files
        """
        dirs = []
        for finder in finders.get_finders():
            if isinstance(finder, finders.FileSystemFinder):
                dirs.extend(finder.locations)
        return [(prefix, normpath(dir)) for prefix, dir in dirs]

    def get_staticfiles_apps(self):
        """
        Returns a list of app paths that have a static directory
        """
        apps = []
        for finder in finders.get_finders():
            if isinstance(finder, finders.AppDirectoriesFinder):
                for app in finder.apps:
                    if app not in apps:
                        apps.append(app)
        return apps