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 %} +
+ + Download .prof file + +
+{% 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 %} - - - - - - - + {% else %} + + {% endif %} + {{ call.func_std_string|safe }} + + + + + + + + {% endfor %}
-
- {% 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 }}
{{ call.cumtime|floatformat:3 }}{{ call.cumtime_per_call|floatformat:3 }}{{ call.tottime|floatformat:3 }}{{ call.tottime_per_call|floatformat:3 }}{{ call.count }}
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