From 48ac250cf25bf23efb904fc5770c5c688a695d94 Mon Sep 17 00:00:00 2001
From: andoriyaprashant
Date: Wed, 18 Jun 2025 15:49:24 +0530
Subject: [PATCH 1/9] Add .prof File Download Support to Profiling Panel
---
debug_toolbar/panels/profiling.py | 13 +++++++++++--
debug_toolbar/panels/sql/tracking.py | 11 ++++++++++-
.../debug_toolbar/panels/profiling.html | 9 +++++++++
debug_toolbar/urls.py | 12 +++++++++++-
debug_toolbar/views.py | 19 ++++++++++++++++++-
5 files changed, 59 insertions(+), 5 deletions(-)
diff --git a/debug_toolbar/panels/profiling.py b/debug_toolbar/panels/profiling.py
index 4613a3cad..2de17a81e 100644
--- a/debug_toolbar/panels/profiling.py
+++ b/debug_toolbar/panels/profiling.py
@@ -2,6 +2,7 @@
import os
from colorsys import hsv_to_rgb
from pstats import Stats
+import tempfile
from django.conf import settings
from django.utils.html import format_html
@@ -168,8 +169,11 @@ def generate_stats(self, request, response):
self.stats = Stats(self.profiler)
self.stats.calc_callees()
- root_func = cProfile.label(super().process_request.__code__)
+ prof_file_path = os.path.join(tempfile.gettempdir(), next(tempfile._get_candidate_names()) + ".prof")
+ self.profiler.dump_stats(prof_file_path)
+ self.prof_file_path = prof_file_path
+ root_func = cProfile.label(super().process_request.__code__)
if root_func in self.stats.stats:
root = FunctionCall(self.stats, root_func, depth=0)
func_list = []
@@ -182,4 +186,9 @@ def generate_stats(self, request, response):
dt_settings.get_config()["PROFILER_MAX_DEPTH"],
cum_time_threshold,
)
- self.record_stats({"func_list": func_list})
+ self.record_stats({
+ "func_list": func_list,
+ "prof_file_path": self.prof_file_path
+ })
+
+
diff --git a/debug_toolbar/panels/sql/tracking.py b/debug_toolbar/panels/sql/tracking.py
index 477106fdd..927ab1427 100644
--- a/debug_toolbar/panels/sql/tracking.py
+++ b/debug_toolbar/panels/sql/tracking.py
@@ -142,7 +142,16 @@ def _last_executed_query(self, sql, params):
# process during the .last_executed_query() call.
self.db._djdt_logger = None
try:
- return self.db.ops.last_executed_query(self.cursor, sql, params)
+ # Handle executemany: take the first set of parameters for formatting
+ if isinstance(params, (list, tuple)) and len(params) > 0 and isinstance(params[0], (list, tuple)):
+ sample_params = params[0]
+ else:
+ sample_params = params
+
+ try:
+ return self.db.ops.last_executed_query(self.cursor, sql, sample_params)
+ except Exception:
+ return sql
finally:
self.db._djdt_logger = self.logger
diff --git a/debug_toolbar/templates/debug_toolbar/panels/profiling.html b/debug_toolbar/templates/debug_toolbar/panels/profiling.html
index 0c2206a13..39e8eeb93 100644
--- a/debug_toolbar/templates/debug_toolbar/panels/profiling.html
+++ b/debug_toolbar/templates/debug_toolbar/panels/profiling.html
@@ -1,4 +1,13 @@
{% load i18n %}
+
+{% if prof_file_path %}
+
+{% endif %}
+
diff --git a/debug_toolbar/urls.py b/debug_toolbar/urls.py
index 5aa0d69e9..b7e0d3363 100644
--- a/debug_toolbar/urls.py
+++ b/debug_toolbar/urls.py
@@ -1,5 +1,15 @@
+from django.urls import path
from debug_toolbar import APP_NAME
+from debug_toolbar import views as debug_toolbar_views
from debug_toolbar.toolbar import DebugToolbar
+from debug_toolbar import APP_NAME
app_name = APP_NAME
-urlpatterns = DebugToolbar.get_urls()
+
+urlpatterns = DebugToolbar.get_urls() + [
+ path(
+ "download_prof_file/",
+ debug_toolbar_views.download_prof_file,
+ name="debug_toolbar_download_prof_file"
+ ),
+]
diff --git a/debug_toolbar/views.py b/debug_toolbar/views.py
index b9a410db5..ffaeb8968 100644
--- a/debug_toolbar/views.py
+++ b/debug_toolbar/views.py
@@ -1,7 +1,10 @@
-from django.http import JsonResponse
+import os
+from django.http import JsonResponse, FileResponse, Http404
from django.utils.html import escape
from django.utils.translation import gettext as _
+from django.views.decorators.http import require_GET
+
from debug_toolbar._compat import login_not_required
from debug_toolbar.decorators import render_with_toolbar_language, require_show_toolbar
from debug_toolbar.toolbar import DebugToolbar
@@ -25,3 +28,17 @@ def render_panel(request):
content = panel.content
scripts = panel.scripts
return JsonResponse({"content": content, "scripts": scripts})
+
+
+@require_GET
+def download_prof_file(request):
+ file_path = request.GET.get("path")
+ print("Serving .prof file:", file_path)
+ if not file_path or not os.path.exists(file_path):
+ print("File does not exist:", file_path)
+ raise Http404("File not found.")
+
+ response = FileResponse(open(file_path, 'rb'), content_type='application/octet-stream')
+ response['Content-Disposition'] = f'attachment; filename="{os.path.basename(file_path)}"'
+ return response
+
From 49f29eb7eaf7aad37c2f551034f6e30fb59a3265 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Wed, 18 Jun 2025 10:22:32 +0000
Subject: [PATCH 2/9] [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---
debug_toolbar/panels/profiling.py | 17 ++++++++---------
debug_toolbar/panels/sql/tracking.py | 6 +++++-
debug_toolbar/urls.py | 9 ++++-----
debug_toolbar/views.py | 17 ++++++++++-------
4 files changed, 27 insertions(+), 22 deletions(-)
diff --git a/debug_toolbar/panels/profiling.py b/debug_toolbar/panels/profiling.py
index 2de17a81e..8ae1db0e9 100644
--- a/debug_toolbar/panels/profiling.py
+++ b/debug_toolbar/panels/profiling.py
@@ -1,8 +1,8 @@
import cProfile
import os
+import tempfile
from colorsys import hsv_to_rgb
from pstats import Stats
-import tempfile
from django.conf import settings
from django.utils.html import format_html
@@ -169,9 +169,11 @@ def generate_stats(self, request, response):
self.stats = Stats(self.profiler)
self.stats.calc_callees()
- prof_file_path = os.path.join(tempfile.gettempdir(), next(tempfile._get_candidate_names()) + ".prof")
+ prof_file_path = os.path.join(
+ tempfile.gettempdir(), next(tempfile._get_candidate_names()) + ".prof"
+ )
self.profiler.dump_stats(prof_file_path)
- self.prof_file_path = prof_file_path
+ self.prof_file_path = prof_file_path
root_func = cProfile.label(super().process_request.__code__)
if root_func in self.stats.stats:
@@ -186,9 +188,6 @@ def generate_stats(self, request, response):
dt_settings.get_config()["PROFILER_MAX_DEPTH"],
cum_time_threshold,
)
- self.record_stats({
- "func_list": func_list,
- "prof_file_path": self.prof_file_path
- })
-
-
+ self.record_stats(
+ {"func_list": func_list, "prof_file_path": self.prof_file_path}
+ )
diff --git a/debug_toolbar/panels/sql/tracking.py b/debug_toolbar/panels/sql/tracking.py
index 927ab1427..c2aff7609 100644
--- a/debug_toolbar/panels/sql/tracking.py
+++ b/debug_toolbar/panels/sql/tracking.py
@@ -143,7 +143,11 @@ def _last_executed_query(self, sql, params):
self.db._djdt_logger = None
try:
# Handle executemany: take the first set of parameters for formatting
- if isinstance(params, (list, tuple)) and len(params) > 0 and isinstance(params[0], (list, tuple)):
+ if (
+ isinstance(params, (list, tuple))
+ and len(params) > 0
+ and isinstance(params[0], (list, tuple))
+ ):
sample_params = params[0]
else:
sample_params = params
diff --git a/debug_toolbar/urls.py b/debug_toolbar/urls.py
index b7e0d3363..6559d4874 100644
--- a/debug_toolbar/urls.py
+++ b/debug_toolbar/urls.py
@@ -1,15 +1,14 @@
from django.urls import path
-from debug_toolbar import APP_NAME
-from debug_toolbar import views as debug_toolbar_views
+
+from debug_toolbar import APP_NAME, views as debug_toolbar_views
from debug_toolbar.toolbar import DebugToolbar
-from debug_toolbar import APP_NAME
app_name = APP_NAME
urlpatterns = DebugToolbar.get_urls() + [
path(
"download_prof_file/",
- debug_toolbar_views.download_prof_file,
- name="debug_toolbar_download_prof_file"
+ debug_toolbar_views.download_prof_file,
+ name="debug_toolbar_download_prof_file",
),
]
diff --git a/debug_toolbar/views.py b/debug_toolbar/views.py
index ffaeb8968..e7c2ece66 100644
--- a/debug_toolbar/views.py
+++ b/debug_toolbar/views.py
@@ -1,8 +1,8 @@
import os
-from django.http import JsonResponse, FileResponse, Http404
+
+from django.http import FileResponse, Http404, JsonResponse
from django.utils.html import escape
from django.utils.translation import gettext as _
-
from django.views.decorators.http import require_GET
from debug_toolbar._compat import login_not_required
@@ -33,12 +33,15 @@ def render_panel(request):
@require_GET
def download_prof_file(request):
file_path = request.GET.get("path")
- print("Serving .prof file:", file_path)
+ print("Serving .prof file:", file_path)
if not file_path or not os.path.exists(file_path):
- print("File does not exist:", file_path)
+ print("File does not exist:", file_path)
raise Http404("File not found.")
- response = FileResponse(open(file_path, 'rb'), content_type='application/octet-stream')
- response['Content-Disposition'] = f'attachment; filename="{os.path.basename(file_path)}"'
+ response = FileResponse(
+ open(file_path, "rb"), content_type="application/octet-stream"
+ )
+ response["Content-Disposition"] = (
+ f'attachment; filename="{os.path.basename(file_path)}"'
+ )
return response
-
From fe4e3051d353c3fe7680030b2868507c0a3b9cea Mon Sep 17 00:00:00 2001
From: Johanan Oppong Amoateng
Date: Wed, 17 Dec 2025 19:47:24 +0000
Subject: [PATCH 3/9] Refactor profiling panel and add download functionality
for .prof files
---
.../static/debug_toolbar/css/toolbar.css | 4 +++
.../debug_toolbar/panels/profiling.html | 36 +++++++++----------
debug_toolbar/toolbar.py | 5 +++
debug_toolbar/urls.py | 12 ++-----
4 files changed, 29 insertions(+), 28 deletions(-)
diff --git a/debug_toolbar/static/debug_toolbar/css/toolbar.css b/debug_toolbar/static/debug_toolbar/css/toolbar.css
index 044e15e5f..ce91bde09 100644
--- a/debug_toolbar/static/debug_toolbar/css/toolbar.css
+++ b/debug_toolbar/static/debug_toolbar/css/toolbar.css
@@ -1223,3 +1223,7 @@ To regenerate:
#djDebug .djdt-community-panel a:hover {
text-decoration: underline;
}
+
+#djDebug .djdt-profiling-control {
+ margin-bottom: 10px;
+}
diff --git a/debug_toolbar/templates/debug_toolbar/panels/profiling.html b/debug_toolbar/templates/debug_toolbar/panels/profiling.html
index 5307f5e54..870b81891 100644
--- a/debug_toolbar/templates/debug_toolbar/panels/profiling.html
+++ b/debug_toolbar/templates/debug_toolbar/panels/profiling.html
@@ -1,11 +1,11 @@
{% load i18n %}
{% if prof_file_path %}
-
{% endif %}
@@ -22,22 +22,22 @@
{% for call in func_list %}
-
-
- {% if call.has_subfuncs %}
+
+
+ {% if call.has_subfuncs %}
- {% else %}
-
- {% endif %}
- {{ call.func_std_string|safe }}
-
- |
- {{ call.cumtime|floatformat:3 }} |
- {{ call.cumtime_per_call|floatformat:3 }} |
- {{ call.tottime|floatformat:3 }} |
- {{ call.tottime_per_call|floatformat:3 }} |
- {{ call.count }} |
- |
+ {% else %}
+
+ {% endif %}
+ {{ call.func_std_string|safe }}
+
+
+ {{ call.cumtime|floatformat:3 }} |
+ {{ call.cumtime_per_call|floatformat:3 }} |
+ {{ call.tottime|floatformat:3 }} |
+ {{ call.tottime_per_call|floatformat:3 }} |
+ {{ call.count }} |
+
{% endfor %}
diff --git a/debug_toolbar/toolbar.py b/debug_toolbar/toolbar.py
index 0e22c8f06..805d34489 100644
--- a/debug_toolbar/toolbar.py
+++ b/debug_toolbar/toolbar.py
@@ -163,6 +163,11 @@ def get_urls(cls) -> list[URLPattern | URLResolver]:
# Global URLs
urlpatterns = [
path("render_panel/", views.render_panel, name="render_panel"),
+ path(
+ "download_prof_file/",
+ views.download_prof_file,
+ name="debug_toolbar_download_prof_file",
+ ),
]
# Per-panel URLs
for panel_class in cls.get_panel_classes():
diff --git a/debug_toolbar/urls.py b/debug_toolbar/urls.py
index 6559d4874..38ed785aa 100644
--- a/debug_toolbar/urls.py
+++ b/debug_toolbar/urls.py
@@ -1,14 +1,6 @@
-from django.urls import path
-
-from debug_toolbar import APP_NAME, views as debug_toolbar_views
+from debug_toolbar import APP_NAME
from debug_toolbar.toolbar import DebugToolbar
app_name = APP_NAME
-urlpatterns = DebugToolbar.get_urls() + [
- path(
- "download_prof_file/",
- debug_toolbar_views.download_prof_file,
- name="debug_toolbar_download_prof_file",
- ),
-]
+urlpatterns = DebugToolbar.get_urls()
From 5db558d3151abad44b41d1763946575bd44b1871 Mon Sep 17 00:00:00 2001
From: Johanan Oppong Amoateng
Date: Wed, 17 Dec 2025 20:04:57 +0000
Subject: [PATCH 4/9] Add profiling data download functionality and related
settings
---
debug_toolbar/panels/profiling.py | 17 +++++++++++------
debug_toolbar/settings.py | 1 +
debug_toolbar/views.py | 29 +++++++++++++++++++----------
docs/changes.rst | 3 +++
docs/configuration.rst | 11 +++++++++++
5 files changed, 45 insertions(+), 16 deletions(-)
diff --git a/debug_toolbar/panels/profiling.py b/debug_toolbar/panels/profiling.py
index df96b7add..63a680afb 100644
--- a/debug_toolbar/panels/profiling.py
+++ b/debug_toolbar/panels/profiling.py
@@ -1,10 +1,11 @@
import cProfile
import os
-import tempfile
+import uuid
from colorsys import hsv_to_rgb
from pstats import Stats
from django.conf import settings
+from django.core import signing
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
@@ -184,11 +185,15 @@ def generate_stats(self, request, response):
self.stats = Stats(self.profiler)
self.stats.calc_callees()
- prof_file_path = os.path.join(
- tempfile.gettempdir(), next(tempfile._get_candidate_names()) + ".prof"
- )
- self.profiler.dump_stats(prof_file_path)
- self.prof_file_path = prof_file_path
+ self.stats.calc_callees()
+
+ if (
+ root := dt_settings.get_config()["PROFILER_PROFILE_ROOT"]
+ ) and os.path.exists(root):
+ filename = f"{uuid.uuid4().hex}.prof"
+ prof_file_path = os.path.join(root, filename)
+ self.profiler.dump_stats(prof_file_path)
+ self.prof_file_path = signing.dumps(filename)
root_func = cProfile.label(super().process_request.__code__)
if root_func in self.stats.stats:
diff --git a/debug_toolbar/settings.py b/debug_toolbar/settings.py
index ba64c8273..8cddcba92 100644
--- a/debug_toolbar/settings.py
+++ b/debug_toolbar/settings.py
@@ -52,6 +52,7 @@ def _is_running_tests():
"PRETTIFY_SQL": True,
"PROFILER_CAPTURE_PROJECT_CODE": True,
"PROFILER_MAX_DEPTH": 10,
+ "PROFILER_PROFILE_ROOT": None,
"PROFILER_THRESHOLD_RATIO": 8,
"SHOW_TEMPLATE_CONTEXT": True,
"SKIP_TEMPLATE_PREFIXES": ("django/forms/widgets/", "admin/widgets/"),
diff --git a/debug_toolbar/views.py b/debug_toolbar/views.py
index 6395b054b..b9089e671 100644
--- a/debug_toolbar/views.py
+++ b/debug_toolbar/views.py
@@ -1,10 +1,12 @@
-import os
+import pathlib
+from django.core import signing
from django.http import FileResponse, Http404, JsonResponse
from django.utils.html import escape
from django.utils.translation import gettext as _
from django.views.decorators.http import require_GET
+from debug_toolbar import settings as dt_settings
from debug_toolbar._compat import login_not_required
from debug_toolbar.decorators import render_with_toolbar_language, require_show_toolbar
from debug_toolbar.toolbar import DebugToolbar
@@ -32,16 +34,23 @@ def render_panel(request):
@require_GET
def download_prof_file(request):
- file_path = request.GET.get("path")
- print("Serving .prof file:", file_path)
- if not file_path or not os.path.exists(file_path):
- print("File does not exist:", file_path)
- raise Http404("File not found.")
+ if not (root := dt_settings.get_config()["PROFILER_PROFILE_ROOT"]):
+ raise Http404
+
+ if not (file_path := request.GET.get("path")):
+ raise Http404
+
+ try:
+ filename = signing.loads(file_path)
+ except signing.BadSignature:
+ return Http404
+
+ resolved_path = pathlib.Path(root) / filename
+ if not resolved_path.exists():
+ raise Http404
response = FileResponse(
- open(file_path, "rb"), content_type="application/octet-stream"
- )
- response["Content-Disposition"] = (
- f'attachment; filename="{os.path.basename(file_path)}"'
+ open(resolved_path, "rb"), content_type="application/octet-stream"
)
+ response["Content-Disposition"] = f'attachment; filename="{resolved_path.name}"'
return response
diff --git a/docs/changes.rst b/docs/changes.rst
index 948b90dec..4164c389a 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -18,6 +18,9 @@ Pending
* Added test to confirm Django's ``TestCase.assertNumQueries`` works.
* Fixed string representation of values in settings panel.
* Declared support for Django 6.0.
+* Added the ability to download the profiling data as a file. This feature is
+ disabled by default and requires the ``PROFILER_PROFILE_ROOT`` setting to be
+ configured.
6.1.0 (2025-10-30)
------------------
diff --git a/docs/configuration.rst b/docs/configuration.rst
index 2ff363888..bbcce5db4 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -351,6 +351,17 @@ Panel options
This setting affects the depth of function calls in the profiler's
analysis.
+* ``PROFILER_PROFILE_ROOT``
+
+ Default: ``None``
+
+ Panel: profiling
+
+ This setting controls the directory where profile files are saved. If set
+ to ``None`` (the default), the profile file is not saved and the download
+ link is not shown. This directory must exist and be writable by the
+ web server process.
+
* ``PROFILER_THRESHOLD_RATIO``
Default: ``8``
From 74ddf5e9b50cff5f76eaee64cb6d498c309560c0 Mon Sep 17 00:00:00 2001
From: Johanan Oppong Amoateng
Date: Wed, 17 Dec 2025 20:12:40 +0000
Subject: [PATCH 5/9] Add tests for profiling stats generation and download
functionality
---
debug_toolbar/panels/profiling.py | 2 +-
tests/panels/test_profiling.py | 69 +++++++++++++++++++++++++++++++
2 files changed, 70 insertions(+), 1 deletion(-)
diff --git a/debug_toolbar/panels/profiling.py b/debug_toolbar/panels/profiling.py
index 63a680afb..8e7f4098a 100644
--- a/debug_toolbar/panels/profiling.py
+++ b/debug_toolbar/panels/profiling.py
@@ -185,7 +185,7 @@ def generate_stats(self, request, response):
self.stats = Stats(self.profiler)
self.stats.calc_callees()
- self.stats.calc_callees()
+ self.prof_file_path = None
if (
root := dt_settings.get_config()["PROFILER_PROFILE_ROOT"]
diff --git a/tests/panels/test_profiling.py b/tests/panels/test_profiling.py
index 320c657ac..01b80e51d 100644
--- a/tests/panels/test_profiling.py
+++ b/tests/panels/test_profiling.py
@@ -1,10 +1,16 @@
+import os
+import shutil
import sys
+import tempfile
import unittest
from django.contrib.auth.models import User
+from django.core import signing
from django.db import IntegrityError, transaction
from django.http import HttpResponse
+from django.test import TestCase
from django.test.utils import override_settings
+from django.urls import reverse
from debug_toolbar.panels.profiling import ProfilingPanel
@@ -77,6 +83,24 @@ def test_generate_stats_no_profiler(self):
response = HttpResponse()
self.assertIsNone(self.panel.generate_stats(self.request, response))
+ @override_settings(
+ DEBUG_TOOLBAR_CONFIG={"PROFILER_PROFILE_ROOT": tempfile.gettempdir()}
+ )
+ def test_generate_stats_signed_path(self):
+ self.panel.process_request(self.request)
+ self.panel.generate_stats(self.request, self.response)
+ path = self.panel.prof_file_path
+ self.assertTrue(path)
+ # Check that it's a valid signature
+ filename = signing.loads(path)
+ self.assertTrue(filename.endswith(".prof"))
+
+ def test_generate_stats_no_root(self):
+ self.panel.process_request(self.request)
+ self.panel.generate_stats(self.request, self.response)
+ # Should not have a path if root is not set
+ self.assertFalse(hasattr(self.panel, "prof_file_path"))
+
def test_generate_stats_no_root_func(self):
"""
Test generating stats using profiler without root function.
@@ -103,3 +127,48 @@ def test_view_executed_once(self):
with self.assertRaises(IntegrityError), transaction.atomic():
response = self.client.get("/new_user/")
self.assertEqual(User.objects.count(), 1)
+
+
+class ProfilingDownloadViewTestCase(TestCase):
+ def setUp(self):
+ self.root = tempfile.mkdtemp()
+ self.filename = "test.prof"
+ self.filepath = os.path.join(self.root, self.filename)
+ with open(self.filepath, "wb") as f:
+ f.write(b"data")
+ self.signed_path = signing.dumps(self.filename)
+
+ def tearDown(self):
+ shutil.rmtree(self.root)
+
+ def test_download_no_root_configured(self):
+ response = self.client.get(reverse("djdt:debug_toolbar_download_prof_file"))
+ self.assertEqual(response.status_code, 404)
+
+ def test_download_valid(self):
+ with override_settings(
+ DEBUG_TOOLBAR_CONFIG={"PROFILER_PROFILE_ROOT": self.root}
+ ):
+ url = reverse("djdt:debug_toolbar_download_prof_file")
+ response = self.client.get(url, {"path": self.signed_path})
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(list(response.streaming_content), [b"data"])
+
+ def test_download_invalid_signature(self):
+ with override_settings(
+ DEBUG_TOOLBAR_CONFIG={"PROFILER_PROFILE_ROOT": self.root}
+ ):
+ url = reverse("djdt:debug_toolbar_download_prof_file")
+ # Tamper with the signature
+ response = self.client.get(url, {"path": self.signed_path + "bad"})
+ self.assertEqual(response.status_code, 400)
+
+ def test_download_missing_file(self):
+ with override_settings(
+ DEBUG_TOOLBAR_CONFIG={"PROFILER_PROFILE_ROOT": self.root}
+ ):
+ url = reverse("djdt:debug_toolbar_download_prof_file")
+ # Sign a filename that doesn't exist
+ path = signing.dumps("missing.prof")
+ response = self.client.get(url, {"path": path})
+ self.assertEqual(response.status_code, 404)
From 5bbb211c0009757e28f77fcb3381e9039bdb4bbc Mon Sep 17 00:00:00 2001
From: Johanan Oppong Amoateng
Date: Wed, 17 Dec 2025 20:19:54 +0000
Subject: [PATCH 6/9] fix
---
debug_toolbar/views.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/debug_toolbar/views.py b/debug_toolbar/views.py
index b9089e671..968c734c7 100644
--- a/debug_toolbar/views.py
+++ b/debug_toolbar/views.py
@@ -35,19 +35,19 @@ def render_panel(request):
@require_GET
def download_prof_file(request):
if not (root := dt_settings.get_config()["PROFILER_PROFILE_ROOT"]):
- raise Http404
+ raise Http404()
if not (file_path := request.GET.get("path")):
- raise Http404
+ raise Http404()
try:
filename = signing.loads(file_path)
except signing.BadSignature:
- return Http404
+ raise Http404() from None
resolved_path = pathlib.Path(root) / filename
if not resolved_path.exists():
- raise Http404
+ raise Http404()
response = FileResponse(
open(resolved_path, "rb"), content_type="application/octet-stream"
From a285d827eec84f947d751482214d72af02fb9494 Mon Sep 17 00:00:00 2001
From: Johanan Oppong Amoateng
Date: Wed, 17 Dec 2025 20:29:35 +0000
Subject: [PATCH 7/9] fix
---
tests/panels/test_profiling.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/tests/panels/test_profiling.py b/tests/panels/test_profiling.py
index 01b80e51d..e76c4a2f0 100644
--- a/tests/panels/test_profiling.py
+++ b/tests/panels/test_profiling.py
@@ -87,8 +87,8 @@ def test_generate_stats_no_profiler(self):
DEBUG_TOOLBAR_CONFIG={"PROFILER_PROFILE_ROOT": tempfile.gettempdir()}
)
def test_generate_stats_signed_path(self):
- self.panel.process_request(self.request)
- self.panel.generate_stats(self.request, self.response)
+ response = self.panel.process_request(self.request)
+ self.panel.generate_stats(self.request, response)
path = self.panel.prof_file_path
self.assertTrue(path)
# Check that it's a valid signature
@@ -96,8 +96,8 @@ def test_generate_stats_signed_path(self):
self.assertTrue(filename.endswith(".prof"))
def test_generate_stats_no_root(self):
- self.panel.process_request(self.request)
- self.panel.generate_stats(self.request, self.response)
+ response = self.panel.process_request(self.request)
+ self.panel.generate_stats(self.request, response)
# Should not have a path if root is not set
self.assertFalse(hasattr(self.panel, "prof_file_path"))
From 2bd5cfe6a17be6a658980466ec9afe3d6f1bf75e Mon Sep 17 00:00:00 2001
From: Johanan Oppong Amoateng
Date: Wed, 17 Dec 2025 20:41:06 +0000
Subject: [PATCH 8/9] fix
---
debug_toolbar/panels/profiling.py | 4 +---
tests/panels/test_profiling.py | 2 +-
2 files changed, 2 insertions(+), 4 deletions(-)
diff --git a/debug_toolbar/panels/profiling.py b/debug_toolbar/panels/profiling.py
index 8e7f4098a..138f2041d 100644
--- a/debug_toolbar/panels/profiling.py
+++ b/debug_toolbar/panels/profiling.py
@@ -185,8 +185,6 @@ def generate_stats(self, request, response):
self.stats = Stats(self.profiler)
self.stats.calc_callees()
- self.prof_file_path = None
-
if (
root := dt_settings.get_config()["PROFILER_PROFILE_ROOT"]
) and os.path.exists(root):
@@ -211,6 +209,6 @@ def generate_stats(self, request, response):
self.record_stats(
{
"func_list": [func.serialize() for func in func_list],
- "prof_file_path": self.prof_file_path,
+ "prof_file_path": getattr(self, "prof_file_path", None),
}
)
diff --git a/tests/panels/test_profiling.py b/tests/panels/test_profiling.py
index e76c4a2f0..363c896dc 100644
--- a/tests/panels/test_profiling.py
+++ b/tests/panels/test_profiling.py
@@ -161,7 +161,7 @@ def test_download_invalid_signature(self):
url = reverse("djdt:debug_toolbar_download_prof_file")
# Tamper with the signature
response = self.client.get(url, {"path": self.signed_path + "bad"})
- self.assertEqual(response.status_code, 400)
+ self.assertEqual(response.status_code, 404)
def test_download_missing_file(self):
with override_settings(
From 66e2d50c05cb8fca28dd5edf5d7333da6670e581 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Wed, 17 Dec 2025 20:45:52 +0000
Subject: [PATCH 9/9] [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---
debug_toolbar/views.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/debug_toolbar/views.py b/debug_toolbar/views.py
index 8dba4b198..570386c85 100644
--- a/debug_toolbar/views.py
+++ b/debug_toolbar/views.py
@@ -1,8 +1,7 @@
import pathlib
from django.core import signing
-from django.http import FileResponse, Http404, JsonResponse
-from django.http import HttpRequest, JsonResponse
+from django.http import FileResponse, Http404, HttpRequest, JsonResponse
from django.utils.html import escape
from django.utils.translation import gettext as _
from django.views.decorators.http import require_GET