From c106c06787b06feb12f63153828494e1154ca947 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 3 Apr 2023 23:13:30 +0200
Subject: [PATCH 01/33] [pre-commit.ci] pre-commit autoupdate (#1755)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/pre-commit/mirrors-eslint: v8.36.0 → v8.37.0](https://github.com/pre-commit/mirrors-eslint/compare/v8.36.0...v8.37.0)
- [github.com/psf/black: 23.1.0 → 23.3.0](https://github.com/psf/black/compare/23.1.0...23.3.0)
- [github.com/abravalheri/validate-pyproject: v0.12.1 → v0.12.2](https://github.com/abravalheri/validate-pyproject/compare/v0.12.1...v0.12.2)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index b5264e4de..85d2b0b31 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -46,7 +46,7 @@ repos:
args:
- --trailing-comma=es5
- repo: https://github.com/pre-commit/mirrors-eslint
- rev: v8.36.0
+ rev: v8.37.0
hooks:
- id: eslint
files: \.js?$
@@ -54,7 +54,7 @@ repos:
args:
- --fix
- repo: https://github.com/psf/black
- rev: 23.1.0
+ rev: 23.3.0
hooks:
- id: black
language_version: python3
@@ -64,6 +64,6 @@ repos:
hooks:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
- rev: v0.12.1
+ rev: v0.12.2
hooks:
- id: validate-pyproject
From 5130f3c809673c28a030259aca489b6b55804249 Mon Sep 17 00:00:00 2001
From: Matthias Kestenholz
Date: Wed, 5 Apr 2023 14:36:48 +0200
Subject: [PATCH 02/33] Fix the release date
---
docs/changes.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/changes.rst b/docs/changes.rst
index bb348a36c..1e6070e84 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -4,10 +4,10 @@ Change log
Pending
-------
-4.0.0 (2023-03-14)
+4.0.0 (2023-04-03)
------------------
-* Added Django 4.2a1 to the CI.
+* Added Django 4.2 to the CI.
* Dropped support for Python 3.7.
* Fixed PostgreSQL raw query with a tuple parameter during on explain.
* Use ``TOOLBAR_LANGUAGE`` setting when rendering individual panels
From 5cd990047071e76d3703379a500fbec1a987a3ac Mon Sep 17 00:00:00 2001
From: Daniel Harding
Date: Tue, 14 Mar 2023 15:58:01 +0300
Subject: [PATCH 03/33] Remove unhelpful comments
---
debug_toolbar/panels/sql/utils.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/debug_toolbar/panels/sql/utils.py b/debug_toolbar/panels/sql/utils.py
index 0fbba3e90..c8be12618 100644
--- a/debug_toolbar/panels/sql/utils.py
+++ b/debug_toolbar/panels/sql/utils.py
@@ -12,7 +12,6 @@ class BoldKeywordFilter:
"""sqlparse filter to bold SQL keywords"""
def process(self, stream):
- """Process the token stream"""
for token_type, value in stream:
is_keyword = token_type in T.Keyword
if is_keyword:
@@ -55,7 +54,7 @@ def get_filter_stack(prettify, aligned_indent):
stack.stmtprocess.append(
sqlparse.filters.AlignedIndentFilter(char=" ", n="
")
)
- stack.preprocess.append(BoldKeywordFilter()) # add our custom filter
+ stack.preprocess.append(BoldKeywordFilter())
stack.postprocess.append(sqlparse.filters.SerializerUnicode()) # tokens -> strings
return stack
From b34f0297b7a76bb3667cff5f707d0e43fd2e4110 Mon Sep 17 00:00:00 2001
From: Daniel Harding
Date: Tue, 14 Mar 2023 18:53:29 +0300
Subject: [PATCH 04/33] Use Python's built-in html.escape()
Because the token values escaped by BoldKeywordFilter are simply
intermediate values and are not directly included in HTML templates,
use Python's html.escape() instead of django.utils.html.escape() to
eliminate the overhead of converting the token values to SafeString.
Also pass quote=False when calling escape() since the token values will
not be used in quoted attributes.
---
debug_toolbar/panels/sql/utils.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/debug_toolbar/panels/sql/utils.py b/debug_toolbar/panels/sql/utils.py
index c8be12618..a17308bb8 100644
--- a/debug_toolbar/panels/sql/utils.py
+++ b/debug_toolbar/panels/sql/utils.py
@@ -1,8 +1,8 @@
import re
from functools import lru_cache
+from html import escape
import sqlparse
-from django.utils.html import escape
from sqlparse import tokens as T
from debug_toolbar import settings as dt_settings
@@ -16,7 +16,7 @@ def process(self, stream):
is_keyword = token_type in T.Keyword
if is_keyword:
yield T.Text, ""
- yield token_type, escape(value)
+ yield token_type, escape(value, quote=False)
if is_keyword:
yield T.Text, ""
From eb269ee1adda90cd92341bf022a04bf162e1c2f3 Mon Sep 17 00:00:00 2001
From: Daniel Harding
Date: Thu, 2 Jun 2022 17:26:51 +0300
Subject: [PATCH 05/33] Replace sqlparse.filters.SerializerUnicode() usage
sqlparse's SerializerUnicode filter does a bunch of fancy whitespace
processing which isn't needed because the resulting string will just be
inserted into HTML. Replace with a simple EscapedStringSerializer that
does nothing but convert the Statement to a properly-escaped string.
In the process stop the escaping within BoldKeywordFilter to have a
cleaner separation of concerns: BoldKeywordFilter now only handles
marking up keywords as bold, while escaping is explicitly handled by the
EscapedStringSerializer.
---
debug_toolbar/panels/sql/utils.py | 25 +++++++++++++++++++++----
1 file changed, 21 insertions(+), 4 deletions(-)
diff --git a/debug_toolbar/panels/sql/utils.py b/debug_toolbar/panels/sql/utils.py
index a17308bb8..cef2330bb 100644
--- a/debug_toolbar/panels/sql/utils.py
+++ b/debug_toolbar/panels/sql/utils.py
@@ -15,10 +15,27 @@ def process(self, stream):
for token_type, value in stream:
is_keyword = token_type in T.Keyword
if is_keyword:
- yield T.Text, ""
- yield token_type, escape(value, quote=False)
+ yield T.Other, ""
+ yield token_type, value
if is_keyword:
- yield T.Text, ""
+ yield T.Other, ""
+
+
+def escaped_value(token):
+ # Don't escape T.Whitespace tokens because AlignedIndentFilter inserts its tokens as
+ # T.Whitesapce, and in our case those tokens are actually HTML.
+ if token.ttype in (T.Other, T.Whitespace):
+ return token.value
+ return escape(token.value, quote=False)
+
+
+class EscapedStringSerializer:
+ """sqlparse post-processor to convert a Statement into a string escaped for
+ inclusion in HTML ."""
+
+ @staticmethod
+ def process(stmt):
+ return "".join(escaped_value(token) for token in stmt.flatten())
def reformat_sql(sql, with_toggle=False):
@@ -55,7 +72,7 @@ def get_filter_stack(prettify, aligned_indent):
sqlparse.filters.AlignedIndentFilter(char=" ", n="
")
)
stack.preprocess.append(BoldKeywordFilter())
- stack.postprocess.append(sqlparse.filters.SerializerUnicode()) # tokens -> strings
+ stack.postprocess.append(EscapedStringSerializer()) # Statement -> str
return stack
From a499f8d2a83940b920a39313afd06955a2e03051 Mon Sep 17 00:00:00 2001
From: Daniel Harding
Date: Thu, 2 Jun 2022 17:28:26 +0300
Subject: [PATCH 06/33] Replace select-list elision implementation
Instead of using a regex to elide the select list in the simplified
representation of an SQL query, use an sqlparse filter to elide the
select list as a preprocessing step. The result ends up being about 10%
faster.
---
debug_toolbar/panels/sql/utils.py | 62 ++++++++++++++++++++++---------
1 file changed, 44 insertions(+), 18 deletions(-)
diff --git a/debug_toolbar/panels/sql/utils.py b/debug_toolbar/panels/sql/utils.py
index cef2330bb..cdba66364 100644
--- a/debug_toolbar/panels/sql/utils.py
+++ b/debug_toolbar/panels/sql/utils.py
@@ -1,4 +1,3 @@
-import re
from functools import lru_cache
from html import escape
@@ -8,6 +7,38 @@
from debug_toolbar import settings as dt_settings
+class ElideSelectListsFilter:
+ """sqlparse filter to elide the select list in SELECT ... FROM clauses"""
+
+ def process(self, stream):
+ for token_type, value in stream:
+ yield token_type, value
+ if token_type in T.Keyword and value.upper() == "SELECT":
+ yield from self.elide_until_from(stream)
+
+ @staticmethod
+ def elide_until_from(stream):
+ select_list_characters = 0
+ select_list_tokens = []
+ for token_type, value in stream:
+ if token_type in T.Keyword and value.upper() == "FROM":
+ # Do not elide a select list of 12 characters or fewer to preserve
+ # SELECT COUNT(*) FROM ...
+ # and
+ # SELECT (1) AS `a` FROM ...
+ # queries.
+ if select_list_characters <= 12:
+ yield from select_list_tokens
+ else:
+ # U+2022: Unicode character 'BULLET'
+ yield T.Other, " \u2022\u2022\u2022 "
+ yield token_type, value
+ break
+ if select_list_characters <= 12:
+ select_list_characters += len(value)
+ select_list_tokens.append((token_type, value))
+
+
class BoldKeywordFilter:
"""sqlparse filter to bold SQL keywords"""
@@ -39,35 +70,37 @@ def process(stmt):
def reformat_sql(sql, with_toggle=False):
- formatted = parse_sql(sql, aligned_indent=True)
+ formatted = parse_sql(sql)
if not with_toggle:
return formatted
- simple = simplify(parse_sql(sql, aligned_indent=False))
- uncollapsed = f'{simple}'
+ simplified = parse_sql(sql, simplify=True)
+ uncollapsed = f'{simplified}'
collapsed = f'{formatted}'
return collapsed + uncollapsed
-def parse_sql(sql, aligned_indent=False):
+def parse_sql(sql, *, simplify=False):
return _parse_sql(
sql,
- dt_settings.get_config()["PRETTIFY_SQL"],
- aligned_indent,
+ prettify=dt_settings.get_config()["PRETTIFY_SQL"],
+ simplify=simplify,
)
@lru_cache(maxsize=128)
-def _parse_sql(sql, pretty, aligned_indent):
- stack = get_filter_stack(pretty, aligned_indent)
+def _parse_sql(sql, *, prettify, simplify):
+ stack = get_filter_stack(prettify=prettify, simplify=simplify)
return "".join(stack.run(sql))
@lru_cache(maxsize=None)
-def get_filter_stack(prettify, aligned_indent):
+def get_filter_stack(*, prettify, simplify):
stack = sqlparse.engine.FilterStack()
if prettify:
stack.enable_grouping()
- if aligned_indent:
+ if simplify:
+ stack.preprocess.append(ElideSelectListsFilter())
+ else:
stack.stmtprocess.append(
sqlparse.filters.AlignedIndentFilter(char=" ", n="
")
)
@@ -76,13 +109,6 @@ def get_filter_stack(prettify, aligned_indent):
return stack
-simplify_re = re.compile(r"SELECT (...........*?) FROM")
-
-
-def simplify(sql):
- return simplify_re.sub(r"SELECT ••• FROM", sql)
-
-
def contrasting_color_generator():
"""
Generate contrasting colors by varying most significant bit of RGB first,
From cec0a0c36db9c4c188b38fe329ae5b5f6ad23f16 Mon Sep 17 00:00:00 2001
From: Daniel Harding
Date: Thu, 30 Mar 2023 13:59:26 +0300
Subject: [PATCH 07/33] Use better heuristic for select list elision
Instead of only eliding select lists longer than 12 characters, now only
elide select lists that contain a dot (from a column expression like
`table_name`.`column_name`). The motivation for this is that as of
Django 1.10, using .count() on a queryset generates
SELECT COUNT(*) AS `__count` FROM ...
instead of
SELECT COUNT(*) FROM ...
queries. This change prevents the new form from being elided.
---
debug_toolbar/panels/sql/utils.py | 21 ++++++++++++---------
tests/panels/test_sql.py | 15 +++++++++++++++
2 files changed, 27 insertions(+), 9 deletions(-)
diff --git a/debug_toolbar/panels/sql/utils.py b/debug_toolbar/panels/sql/utils.py
index cdba66364..3950dafdc 100644
--- a/debug_toolbar/panels/sql/utils.py
+++ b/debug_toolbar/panels/sql/utils.py
@@ -18,25 +18,28 @@ def process(self, stream):
@staticmethod
def elide_until_from(stream):
- select_list_characters = 0
- select_list_tokens = []
+ has_dot = False
+ saved_tokens = []
for token_type, value in stream:
if token_type in T.Keyword and value.upper() == "FROM":
- # Do not elide a select list of 12 characters or fewer to preserve
- # SELECT COUNT(*) FROM ...
+ # Do not elide a select lists that do not contain dots (used to separate
+ # table names from column names) in order to preserve
+ # SELECT COUNT(*) AS `__count` FROM ...
# and
# SELECT (1) AS `a` FROM ...
# queries.
- if select_list_characters <= 12:
- yield from select_list_tokens
+ if not has_dot:
+ yield from saved_tokens
else:
# U+2022: Unicode character 'BULLET'
yield T.Other, " \u2022\u2022\u2022 "
yield token_type, value
break
- if select_list_characters <= 12:
- select_list_characters += len(value)
- select_list_tokens.append((token_type, value))
+ if not has_dot:
+ if token_type in T.Punctuation and value == ".":
+ has_dot = True
+ else:
+ saved_tokens.append((token_type, value))
class BoldKeywordFilter:
diff --git a/tests/panels/test_sql.py b/tests/panels/test_sql.py
index 13e3625ba..6e1a50197 100644
--- a/tests/panels/test_sql.py
+++ b/tests/panels/test_sql.py
@@ -495,6 +495,21 @@ def test_prettify_sql(self):
self.assertEqual(len(self.panel._queries), 1)
self.assertEqual(pretty_sql, self.panel._queries[-1]["sql"])
+ def test_simplification(self):
+ """
+ Test case to validate that select lists for .count() and .exist() queries do not
+ get elided, but other select lists do.
+ """
+ User.objects.count()
+ User.objects.exists()
+ list(User.objects.values_list("id"))
+ response = self.panel.process_request(self.request)
+ self.panel.generate_stats(self.request, response)
+ self.assertEqual(len(self.panel._queries), 3)
+ self.assertNotIn("\u2022", self.panel._queries[0]["sql"])
+ self.assertNotIn("\u2022", self.panel._queries[1]["sql"])
+ self.assertIn("\u2022", self.panel._queries[2]["sql"])
+
@override_settings(
DEBUG=True,
)
From 494b42a76f2ce3f22dc41f4b8f80718df3afd966 Mon Sep 17 00:00:00 2001
From: Daniel Harding
Date: Tue, 14 Mar 2023 22:32:49 +0300
Subject: [PATCH 08/33] Only elide top-level select lists
If a query has subselects in its WHERE clause, do not elide the select
lists in those subselects.
---
debug_toolbar/panels/sql/utils.py | 11 ++++++---
tests/panels/test_sql.py | 38 +++++++++++++++++++++++++++++++
2 files changed, 46 insertions(+), 3 deletions(-)
diff --git a/debug_toolbar/panels/sql/utils.py b/debug_toolbar/panels/sql/utils.py
index 3950dafdc..3c99facf5 100644
--- a/debug_toolbar/panels/sql/utils.py
+++ b/debug_toolbar/panels/sql/utils.py
@@ -8,13 +8,18 @@
class ElideSelectListsFilter:
- """sqlparse filter to elide the select list in SELECT ... FROM clauses"""
+ """sqlparse filter to elide the select list from top-level SELECT ... FROM clauses,
+ if present"""
def process(self, stream):
+ allow_elision = True
for token_type, value in stream:
yield token_type, value
- if token_type in T.Keyword and value.upper() == "SELECT":
- yield from self.elide_until_from(stream)
+ if token_type in T.Keyword:
+ keyword = value.upper()
+ if allow_elision and keyword == "SELECT":
+ yield from self.elide_until_from(stream)
+ allow_elision = keyword in ["EXCEPT", "INTERSECT", "UNION"]
@staticmethod
def elide_until_from(stream):
diff --git a/tests/panels/test_sql.py b/tests/panels/test_sql.py
index 6e1a50197..a597d4c11 100644
--- a/tests/panels/test_sql.py
+++ b/tests/panels/test_sql.py
@@ -510,6 +510,44 @@ def test_simplification(self):
self.assertNotIn("\u2022", self.panel._queries[1]["sql"])
self.assertIn("\u2022", self.panel._queries[2]["sql"])
+ def test_top_level_simplification(self):
+ """
+ Test case to validate that top-level select lists get elided, but other select
+ lists for subselects do not.
+ """
+ list(User.objects.filter(id__in=User.objects.filter(is_staff=True)))
+ list(User.objects.filter(id__lt=20).union(User.objects.filter(id__gt=10)))
+ if connection.vendor != "mysql":
+ list(
+ User.objects.filter(id__lt=20).intersection(
+ User.objects.filter(id__gt=10)
+ )
+ )
+ list(
+ User.objects.filter(id__lt=20).difference(
+ User.objects.filter(id__gt=10)
+ )
+ )
+ response = self.panel.process_request(self.request)
+ self.panel.generate_stats(self.request, response)
+ if connection.vendor != "mysql":
+ self.assertEqual(len(self.panel._queries), 4)
+ else:
+ self.assertEqual(len(self.panel._queries), 2)
+ # WHERE ... IN SELECT ... queries should have only one elided select list
+ self.assertEqual(self.panel._queries[0]["sql"].count("SELECT"), 4)
+ self.assertEqual(self.panel._queries[0]["sql"].count("\u2022"), 3)
+ # UNION queries should have two elidid select lists
+ self.assertEqual(self.panel._queries[1]["sql"].count("SELECT"), 4)
+ self.assertEqual(self.panel._queries[1]["sql"].count("\u2022"), 6)
+ if connection.vendor != "mysql":
+ # INTERSECT queries should have two elidid select lists
+ self.assertEqual(self.panel._queries[2]["sql"].count("SELECT"), 4)
+ self.assertEqual(self.panel._queries[2]["sql"].count("\u2022"), 6)
+ # EXCEPT queries should have two elidid select lists
+ self.assertEqual(self.panel._queries[3]["sql"].count("SELECT"), 4)
+ self.assertEqual(self.panel._queries[3]["sql"].count("\u2022"), 6)
+
@override_settings(
DEBUG=True,
)
From d3ba7d7e8073551f1eda7262422ac5ac98fcc479 Mon Sep 17 00:00:00 2001
From: Daniel Harding
Date: Wed, 29 Mar 2023 16:28:40 +0300
Subject: [PATCH 09/33] Apply BoldKeywordFilter after AlignedIndentFilter
The "" tokens inserted by the BoldKeywordFilter were causing the
AlignedIndentFilter to apply excessive indentation to queries which used
CASE statements. Fix by rewriting BoldIndentFilter as a statement
filter rather than a preprocess filter, and applying after
AlignedIndentFilter.
---
debug_toolbar/panels/sql/utils.py | 25 ++++++++++++++++---------
1 file changed, 16 insertions(+), 9 deletions(-)
diff --git a/debug_toolbar/panels/sql/utils.py b/debug_toolbar/panels/sql/utils.py
index 3c99facf5..cbe275ff3 100644
--- a/debug_toolbar/panels/sql/utils.py
+++ b/debug_toolbar/panels/sql/utils.py
@@ -50,14 +50,21 @@ def elide_until_from(stream):
class BoldKeywordFilter:
"""sqlparse filter to bold SQL keywords"""
- def process(self, stream):
- for token_type, value in stream:
- is_keyword = token_type in T.Keyword
- if is_keyword:
- yield T.Other, ""
- yield token_type, value
- if is_keyword:
- yield T.Other, ""
+ def process(self, stmt):
+ idx = 0
+ while idx < len(stmt.tokens):
+ token = stmt[idx]
+ if token.is_keyword:
+ stmt.insert_before(idx, sqlparse.sql.Token(T.Other, ""))
+ stmt.insert_after(
+ idx + 1,
+ sqlparse.sql.Token(T.Other, ""),
+ skip_ws=False,
+ )
+ idx += 2
+ elif token.is_group:
+ self.process(token)
+ idx += 1
def escaped_value(token):
@@ -112,7 +119,7 @@ def get_filter_stack(*, prettify, simplify):
stack.stmtprocess.append(
sqlparse.filters.AlignedIndentFilter(char=" ", n="
")
)
- stack.preprocess.append(BoldKeywordFilter())
+ stack.stmtprocess.append(BoldKeywordFilter())
stack.postprocess.append(EscapedStringSerializer()) # Statement -> str
return stack
From 2e414a32b0535624874a144a8ec1f11f3c3c7929 Mon Sep 17 00:00:00 2001
From: Daniel Harding
Date: Wed, 29 Mar 2023 16:43:48 +0300
Subject: [PATCH 10/33] Only enable SQL grouping for AlignedIndentFilter
When formatting SQL statements using sqparse, grouping only affects the
output when AlignedIndentFilter is applied.
---
debug_toolbar/panels/sql/utils.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/debug_toolbar/panels/sql/utils.py b/debug_toolbar/panels/sql/utils.py
index cbe275ff3..c47c19142 100644
--- a/debug_toolbar/panels/sql/utils.py
+++ b/debug_toolbar/panels/sql/utils.py
@@ -111,11 +111,11 @@ def _parse_sql(sql, *, prettify, simplify):
@lru_cache(maxsize=None)
def get_filter_stack(*, prettify, simplify):
stack = sqlparse.engine.FilterStack()
- if prettify:
- stack.enable_grouping()
if simplify:
stack.preprocess.append(ElideSelectListsFilter())
else:
+ if prettify:
+ stack.enable_grouping()
stack.stmtprocess.append(
sqlparse.filters.AlignedIndentFilter(char=" ", n="
")
)
From 9d8296c6564091840cd71f3eab14eb38c3c67b2f Mon Sep 17 00:00:00 2001
From: Daniel Harding
Date: Wed, 29 Mar 2023 17:08:04 +0300
Subject: [PATCH 11/33] Eliminate intermediate _parse_sql() method
By using a settings_changed signal receiver to clear the query caching,
the parse_sql() and _parse_sql() functions can be merged and the check
for the "PRETTIFY_SQL" setting can be moved back inside the
get_filter_stack() function.
---
debug_toolbar/panels/sql/utils.py | 25 +++++++++----------
tests/panels/test_sql.py | 40 ++++++++++++++-----------------
2 files changed, 31 insertions(+), 34 deletions(-)
diff --git a/debug_toolbar/panels/sql/utils.py b/debug_toolbar/panels/sql/utils.py
index c47c19142..efd7c1637 100644
--- a/debug_toolbar/panels/sql/utils.py
+++ b/debug_toolbar/panels/sql/utils.py
@@ -2,6 +2,8 @@
from html import escape
import sqlparse
+from django.dispatch import receiver
+from django.test.signals import setting_changed
from sqlparse import tokens as T
from debug_toolbar import settings as dt_settings
@@ -94,27 +96,19 @@ def reformat_sql(sql, with_toggle=False):
return collapsed + uncollapsed
-def parse_sql(sql, *, simplify=False):
- return _parse_sql(
- sql,
- prettify=dt_settings.get_config()["PRETTIFY_SQL"],
- simplify=simplify,
- )
-
-
@lru_cache(maxsize=128)
-def _parse_sql(sql, *, prettify, simplify):
- stack = get_filter_stack(prettify=prettify, simplify=simplify)
+def parse_sql(sql, *, simplify=False):
+ stack = get_filter_stack(simplify=simplify)
return "".join(stack.run(sql))
@lru_cache(maxsize=None)
-def get_filter_stack(*, prettify, simplify):
+def get_filter_stack(*, simplify):
stack = sqlparse.engine.FilterStack()
if simplify:
stack.preprocess.append(ElideSelectListsFilter())
else:
- if prettify:
+ if dt_settings.get_config()["PRETTIFY_SQL"]:
stack.enable_grouping()
stack.stmtprocess.append(
sqlparse.filters.AlignedIndentFilter(char=" ", n="
")
@@ -124,6 +118,13 @@ def get_filter_stack(*, prettify, simplify):
return stack
+@receiver(setting_changed)
+def clear_caches(*, setting, **kwargs):
+ if setting == "DEBUG_TOOLBAR_CONFIG":
+ parse_sql.cache_clear()
+ get_filter_stack.cache_clear()
+
+
def contrasting_color_generator():
"""
Generate contrasting colors by varying most significant bit of RGB first,
diff --git a/tests/panels/test_sql.py b/tests/panels/test_sql.py
index a597d4c11..7b3452935 100644
--- a/tests/panels/test_sql.py
+++ b/tests/panels/test_sql.py
@@ -14,7 +14,6 @@
from django.test.utils import override_settings
import debug_toolbar.panels.sql.tracking as sql_tracking
-from debug_toolbar import settings as dt_settings
try:
import psycopg
@@ -458,42 +457,39 @@ def test_regression_infinite_recursion(self):
# ensure the stacktrace is populated
self.assertTrue(len(query["stacktrace"]) > 0)
- @override_settings(
- DEBUG_TOOLBAR_CONFIG={"PRETTIFY_SQL": True},
- )
def test_prettify_sql(self):
"""
Test case to validate that the PRETTIFY_SQL setting changes the output
of the sql when it's toggled. It does not validate what it does
though.
"""
- list(User.objects.filter(username__istartswith="spam"))
-
- response = self.panel.process_request(self.request)
- self.panel.generate_stats(self.request, response)
- pretty_sql = self.panel._queries[-1]["sql"]
- self.assertEqual(len(self.panel._queries), 1)
+ with override_settings(DEBUG_TOOLBAR_CONFIG={"PRETTIFY_SQL": True}):
+ list(User.objects.filter(username__istartswith="spam"))
+ response = self.panel.process_request(self.request)
+ self.panel.generate_stats(self.request, response)
+ pretty_sql = self.panel._queries[-1]["sql"]
+ self.assertEqual(len(self.panel._queries), 1)
# Reset the queries
self.panel._queries = []
# Run it again, but with prettify off. Verify that it's different.
- dt_settings.get_config()["PRETTIFY_SQL"] = False
- list(User.objects.filter(username__istartswith="spam"))
- response = self.panel.process_request(self.request)
- self.panel.generate_stats(self.request, response)
- self.assertEqual(len(self.panel._queries), 1)
- self.assertNotEqual(pretty_sql, self.panel._queries[-1]["sql"])
+ with override_settings(DEBUG_TOOLBAR_CONFIG={"PRETTIFY_SQL": False}):
+ list(User.objects.filter(username__istartswith="spam"))
+ response = self.panel.process_request(self.request)
+ self.panel.generate_stats(self.request, response)
+ self.assertEqual(len(self.panel._queries), 1)
+ self.assertNotEqual(pretty_sql, self.panel._queries[-1]["sql"])
self.panel._queries = []
# Run it again, but with prettify back on.
# This is so we don't have to check what PRETTIFY_SQL does exactly,
# but we know it's doing something.
- dt_settings.get_config()["PRETTIFY_SQL"] = True
- list(User.objects.filter(username__istartswith="spam"))
- response = self.panel.process_request(self.request)
- self.panel.generate_stats(self.request, response)
- self.assertEqual(len(self.panel._queries), 1)
- self.assertEqual(pretty_sql, self.panel._queries[-1]["sql"])
+ with override_settings(DEBUG_TOOLBAR_CONFIG={"PRETTIFY_SQL": True}):
+ list(User.objects.filter(username__istartswith="spam"))
+ response = self.panel.process_request(self.request)
+ self.panel.generate_stats(self.request, response)
+ self.assertEqual(len(self.panel._queries), 1)
+ self.assertEqual(pretty_sql, self.panel._queries[-1]["sql"])
def test_simplification(self):
"""
From 7b8a6cc82e10a6c6116d494640abe3985fee6067 Mon Sep 17 00:00:00 2001
From: Daniel Harding
Date: Thu, 30 Mar 2023 15:11:05 +0300
Subject: [PATCH 12/33] Amend change log
---
docs/changes.rst | 3 +++
1 file changed, 3 insertions(+)
diff --git a/docs/changes.rst b/docs/changes.rst
index 1e6070e84..e6d5c3dc0 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -23,6 +23,9 @@ Pending
is rendered, so that the correct values will be displayed in the rendered
stack trace, as they may have changed between the time the stack trace was
captured and when it is rendered.
+* Improved SQL statement formatting performance. Additionally, fixed the
+ indentation of ``CASE`` statements and stopped simplifying ``.count()``
+ queries.
3.8.1 (2022-12-03)
------------------
From bbba2f804182b17940139297efcf93a8ba204f9d Mon Sep 17 00:00:00 2001
From: Adam Radwon <3501229+radwon@users.noreply.github.com>
Date: Sun, 9 Apr 2023 13:04:27 +0000
Subject: [PATCH 13/33] Use the new STORAGES setting in Django 4.2
In Django 4.2 the django.core.files.storage.get_storage_class() function is deprecated as well as the STATICFILES_STORAGE setting in favor of STORAGES["staticfiles"].
Use django.core.files.storage.storages to get the configured storage class for static files instead.
For Django versions prior to 4.2 keep using the django.core.files.storage.get_storage_class() function for backwards compatibility.
Fixes #1758
---
debug_toolbar/panels/staticfiles.py | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/debug_toolbar/panels/staticfiles.py b/debug_toolbar/panels/staticfiles.py
index c02194071..bee336249 100644
--- a/debug_toolbar/panels/staticfiles.py
+++ b/debug_toolbar/panels/staticfiles.py
@@ -3,7 +3,6 @@
from django.conf import settings
from django.contrib.staticfiles import finders, storage
from django.core.checks import Warning
-from django.core.files.storage import get_storage_class
from django.utils.functional import LazyObject
from django.utils.translation import gettext_lazy as _, ngettext
@@ -53,7 +52,17 @@ class DebugConfiguredStorage(LazyObject):
"""
def _setup(self):
- configured_storage_cls = get_storage_class(settings.STATICFILES_STORAGE)
+ 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 __init__(self, collector, *args, **kwargs):
From 7ededd293bc83f7094405bf52bb96ff7203d0c26 Mon Sep 17 00:00:00 2001
From: Adam Radwon <3501229+radwon@users.noreply.github.com>
Date: Sun, 9 Apr 2023 18:12:46 +0000
Subject: [PATCH 14/33] Use the new STORAGES setting in Django 4.2
Use the new STORAGES setting in Django 4.2
In Django 4.2 the django.core.files.storage.get_storage_class() function
is deprecated as well as the STATICFILES_STORAGE setting in favor of
STORAGES["staticfiles"].
Use django.core.files.storage.storages to get the configured storage
class for static files instead.
For Django versions prior to 4.2 keep using the
django.core.files.storage.get_storage_class() function for backwards
compatibility.
Fixes #1758
---
docs/changes.rst | 2 ++
1 file changed, 2 insertions(+)
diff --git a/docs/changes.rst b/docs/changes.rst
index e6d5c3dc0..cc88036cb 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -4,6 +4,8 @@ Change log
Pending
-------
+* Added support for the new STORAGES setting in Django 4.2 for static files.
+
4.0.0 (2023-04-03)
------------------
From e45e5b66c398b88369b0ccc3a031005062c86747 Mon Sep 17 00:00:00 2001
From: Matthias Kestenholz
Date: Sun, 9 Apr 2023 20:35:41 +0200
Subject: [PATCH 15/33] Move a changelog entry to the correct place
---
docs/changes.rst | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/changes.rst b/docs/changes.rst
index cc88036cb..014233997 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -4,6 +4,9 @@ Change log
Pending
-------
+* Improved SQL statement formatting performance. Additionally, fixed the
+ indentation of ``CASE`` statements and stopped simplifying ``.count()``
+ queries.
* Added support for the new STORAGES setting in Django 4.2 for static files.
4.0.0 (2023-04-03)
@@ -25,9 +28,6 @@ Pending
is rendered, so that the correct values will be displayed in the rendered
stack trace, as they may have changed between the time the stack trace was
captured and when it is rendered.
-* Improved SQL statement formatting performance. Additionally, fixed the
- indentation of ``CASE`` statements and stopped simplifying ``.count()``
- queries.
3.8.1 (2022-12-03)
------------------
From 7ec5ebf4b831340c36a65a3232b56853e4775762 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 10 Apr 2023 19:39:12 +0000
Subject: [PATCH 16/33] [pre-commit.ci] pre-commit autoupdate
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/pre-commit/mirrors-eslint: v8.37.0 → v8.38.0](https://github.com/pre-commit/mirrors-eslint/compare/v8.37.0...v8.38.0)
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 85d2b0b31..3873ceadb 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -46,7 +46,7 @@ repos:
args:
- --trailing-comma=es5
- repo: https://github.com/pre-commit/mirrors-eslint
- rev: v8.37.0
+ rev: v8.38.0
hooks:
- id: eslint
files: \.js?$
From 75da65ccc17add38d7cab79d69f93db4ae495d31 Mon Sep 17 00:00:00 2001
From: Tim Schilling
Date: Sun, 30 Apr 2023 03:15:12 -0500
Subject: [PATCH 17/33] Switch to sphinx 7's intersphinx_mapping usage. (#1767)
https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#confval-intersphinx_mapping
---
docs/conf.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/docs/conf.py b/docs/conf.py
index 18b02f9f7..f6795a6d3 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -59,8 +59,11 @@
# html_static_path = ['_static']
intersphinx_mapping = {
- "https://docs.python.org/": None,
- "https://docs.djangoproject.com/en/dev/": "https://docs.djangoproject.com/en/dev/_objects/",
+ "python": ("https://docs.python.org/", None),
+ "django": (
+ "https://docs.djangoproject.com/en/dev/",
+ "https://docs.djangoproject.com/en/dev/_objects/",
+ ),
}
# -- Options for Read the Docs -----------------------------------------------
From 14c5d51ff73e80e4ee9c14f460cb2790ebc26877 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 24 Apr 2023 19:42:42 +0000
Subject: [PATCH 18/33] [pre-commit.ci] pre-commit autoupdate
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/pre-commit/mirrors-prettier: v3.0.0-alpha.6 → v3.0.0-alpha.9-for-vscode](https://github.com/pre-commit/mirrors-prettier/compare/v3.0.0-alpha.6...v3.0.0-alpha.9-for-vscode)
- [github.com/pre-commit/mirrors-eslint: v8.38.0 → v8.39.0](https://github.com/pre-commit/mirrors-eslint/compare/v8.38.0...v8.39.0)
- [github.com/tox-dev/pyproject-fmt: 0.9.2 → 0.10.0](https://github.com/tox-dev/pyproject-fmt/compare/0.9.2...0.10.0)
---
.pre-commit-config.yaml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 3873ceadb..54d917d1b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -39,14 +39,14 @@ repos:
- id: rst-backticks
- id: rst-directive-colons
- repo: https://github.com/pre-commit/mirrors-prettier
- rev: v3.0.0-alpha.6
+ rev: v3.0.0-alpha.9-for-vscode
hooks:
- id: prettier
types_or: [javascript, css]
args:
- --trailing-comma=es5
- repo: https://github.com/pre-commit/mirrors-eslint
- rev: v8.38.0
+ rev: v8.39.0
hooks:
- id: eslint
files: \.js?$
@@ -60,7 +60,7 @@ repos:
language_version: python3
entry: black --target-version=py38
- repo: https://github.com/tox-dev/pyproject-fmt
- rev: 0.9.2
+ rev: 0.10.0
hooks:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
From 1b148db48e938aeceedc6aa2d1739f46fa54bc3f Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 24 Apr 2023 19:42:54 +0000
Subject: [PATCH 19/33] [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---
.../static/debug_toolbar/css/toolbar.css | 4 ++-
pyproject.toml | 36 +++++++++----------
2 files changed, 21 insertions(+), 19 deletions(-)
diff --git a/debug_toolbar/static/debug_toolbar/css/toolbar.css b/debug_toolbar/static/debug_toolbar/css/toolbar.css
index 4ad2a6df9..154aaafa5 100644
--- a/debug_toolbar/static/debug_toolbar/css/toolbar.css
+++ b/debug_toolbar/static/debug_toolbar/css/toolbar.css
@@ -111,7 +111,9 @@
#djDebug button:active {
border: 1px solid #aaa;
border-bottom: 1px solid #888;
- box-shadow: inset 0 0 5px 2px #aaa, 0 1px 0 0 #eee;
+ box-shadow:
+ inset 0 0 5px 2px #aaa,
+ 0 1px 0 0 #eee;
}
#djDebug #djDebugToolbar {
diff --git a/pyproject.toml b/pyproject.toml
index 1c820c417..f8ad083b0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -14,24 +14,24 @@ authors = [
]
requires-python = ">=3.8"
classifiers = [
- "Development Status :: 5 - Production/Stable",
- "Environment :: Web Environment",
- "Framework :: Django",
- "Framework :: Django :: 3.2",
- "Framework :: Django :: 4.0",
- "Framework :: Django :: 4.1",
- "Framework :: Django :: 4.2",
- "Intended Audience :: Developers",
- "License :: OSI Approved :: BSD License",
- "Operating System :: OS Independent",
- "Programming Language :: Python",
- "Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
- "Programming Language :: Python :: 3.10",
- "Programming Language :: Python :: 3.11",
- "Topic :: Software Development :: Libraries :: Python Modules",
+ "Development Status :: 5 - Production/Stable",
+ "Environment :: Web Environment",
+ "Framework :: Django",
+ "Framework :: Django :: 3.2",
+ "Framework :: Django :: 4.0",
+ "Framework :: Django :: 4.1",
+ "Framework :: Django :: 4.2",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: BSD License",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Topic :: Software Development :: Libraries :: Python Modules",
]
dynamic = [
"version",
From 5429e5b16bb7c3084044c749d9243c3953b78f3e Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 8 May 2023 22:40:47 +0200
Subject: [PATCH 20/33] [pre-commit.ci] pre-commit autoupdate (#1768)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v3.3.1 → v3.4.0](https://github.com/asottile/pyupgrade/compare/v3.3.1...v3.4.0)
- [github.com/pre-commit/mirrors-eslint: v8.39.0 → v8.40.0](https://github.com/pre-commit/mirrors-eslint/compare/v8.39.0...v8.40.0)
- [github.com/tox-dev/pyproject-fmt: 0.10.0 → 0.11.1](https://github.com/tox-dev/pyproject-fmt/compare/0.10.0...0.11.1)
* [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
---
.pre-commit-config.yaml | 6 +++---
pyproject.toml | 1 -
2 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 54d917d1b..7b60c7f71 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -16,7 +16,7 @@ repos:
hooks:
- id: doc8
- repo: https://github.com/asottile/pyupgrade
- rev: v3.3.1
+ rev: v3.4.0
hooks:
- id: pyupgrade
args: [--py38-plus]
@@ -46,7 +46,7 @@ repos:
args:
- --trailing-comma=es5
- repo: https://github.com/pre-commit/mirrors-eslint
- rev: v8.39.0
+ rev: v8.40.0
hooks:
- id: eslint
files: \.js?$
@@ -60,7 +60,7 @@ repos:
language_version: python3
entry: black --target-version=py38
- repo: https://github.com/tox-dev/pyproject-fmt
- rev: 0.10.0
+ rev: 0.11.1
hooks:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
diff --git a/pyproject.toml b/pyproject.toml
index f8ad083b0..b1ccc67f2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -25,7 +25,6 @@ classifiers = [
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
- "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
From a3090b423e1ba7093084e7a65cab3af032ee20e5 Mon Sep 17 00:00:00 2001
From: Paolo Melchiorre
Date: Tue, 9 May 2023 11:37:21 +0200
Subject: [PATCH 21/33] Fix #1711 Improve installation page formats (#1772)
---
docs/installation.rst | 16 +++++++++++-----
1 file changed, 11 insertions(+), 5 deletions(-)
diff --git a/docs/installation.rst b/docs/installation.rst
index acc017601..3b65ff8e2 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -10,7 +10,9 @@ fully functional.
1. Install the Package
^^^^^^^^^^^^^^^^^^^^^^
-The recommended way to install the Debug Toolbar is via pip_::
+The recommended way to install the Debug Toolbar is via pip_:
+
+.. code-block:: console
$ python -m pip install django-debug-toolbar
@@ -20,9 +22,11 @@ If you aren't familiar with pip, you may also obtain a copy of the
.. _pip: https://pip.pypa.io/
To test an upcoming release, you can install the in-development version
-instead with the following command::
+instead with the following command:
+
+.. code-block:: console
- $ python -m pip install -e git+https://github.com/jazzband/django-debug-toolbar.git#egg=django-debug-toolbar
+ $ python -m pip install -e git+https://github.com/jazzband/django-debug-toolbar.git#egg=django-debug-toolbar
If you're upgrading from a previous version, you should review the
:doc:`change log ` and look for specific upgrade instructions.
@@ -64,7 +68,9 @@ Second, ensure that your ``TEMPLATES`` setting contains a
3. Install the App
^^^^^^^^^^^^^^^^^^
-Add ``"debug_toolbar"`` to your ``INSTALLED_APPS`` setting::
+Add ``"debug_toolbar"`` to your ``INSTALLED_APPS`` setting:
+
+.. code-block:: python
INSTALLED_APPS = [
# ...
@@ -83,7 +89,7 @@ Add django-debug-toolbar's URLs to your project's URLconf:
urlpatterns = [
# ...
- path('__debug__/', include('debug_toolbar.urls')),
+ path("__debug__/", include("debug_toolbar.urls")),
]
This example uses the ``__debug__`` prefix, but you can use any prefix that
From a6b65a7c509589a635d5c3889ee8ea7a1e10b707 Mon Sep 17 00:00:00 2001
From: Daniel Harding
Date: Tue, 9 May 2023 16:15:08 +0300
Subject: [PATCH 22/33] Don't try to undo cache method monkey patching (#1770)
Trying to undo the monkey patch of cache methods in the
CachePanel.disable_instrumentation() method is fragile in the presence
of other code which may also monkey patch the same methods (such as
Sentry's Django integration), and there are theoretically situations
where it is actually impossible to do correctly. Thus once a cache has
been monkey-patched, leave it that way, and instead rely on checking in
the patched methods to see if recording needs to happen. This is done
via a _djdt_panel attribute which is set to the current panel in the
enable_instrumentation() method and then set to None in the
disable_instrumentation() method.
---
debug_toolbar/panels/cache.py | 82 ++++++++++++++++-------------------
docs/changes.rst | 3 ++
2 files changed, 40 insertions(+), 45 deletions(-)
diff --git a/debug_toolbar/panels/cache.py b/debug_toolbar/panels/cache.py
index f5ceea513..69a899ea1 100644
--- a/debug_toolbar/panels/cache.py
+++ b/debug_toolbar/panels/cache.py
@@ -30,6 +30,27 @@
]
+def _monkey_patch_method(cache, name):
+ original_method = getattr(cache, name)
+
+ @functools.wraps(original_method)
+ def wrapper(*args, **kwargs):
+ panel = cache._djdt_panel
+ if panel is None:
+ return original_method(*args, **kwargs)
+ else:
+ return panel._record_call(cache, name, original_method, args, kwargs)
+
+ setattr(cache, name, wrapper)
+
+
+def _monkey_patch_cache(cache):
+ if not hasattr(cache, "_djdt_patched"):
+ for name in WRAPPED_CACHE_METHODS:
+ _monkey_patch_method(cache, name)
+ cache._djdt_patched = True
+
+
class CachePanel(Panel):
"""
Panel that displays the cache statistics.
@@ -72,7 +93,8 @@ def wrapper(self, alias):
cache = original_method(self, alias)
panel = cls.current_instance()
if panel is not None:
- panel._monkey_patch_cache(cache)
+ _monkey_patch_cache(cache)
+ cache._djdt_panel = panel
return cache
CacheHandler.create_connection = wrapper
@@ -120,14 +142,17 @@ def _store_call_info(
def _record_call(self, cache, name, original_method, args, kwargs):
# Some cache backends implement certain cache methods in terms of other cache
# methods (e.g. get_or_set() in terms of get() and add()). In order to only
- # record the calls made directly by the user code, set the _djdt_recording flag
- # here to cause the monkey patched cache methods to skip recording additional
- # calls made during the course of this call.
- cache._djdt_recording = True
- t = time.time()
- value = original_method(*args, **kwargs)
- t = time.time() - t
- cache._djdt_recording = False
+ # record the calls made directly by the user code, set the cache's _djdt_panel
+ # attribute to None before invoking the original method, which will cause the
+ # monkey-patched cache methods to skip recording additional calls made during
+ # the course of this call, and then reset it back afterward.
+ cache._djdt_panel = None
+ try:
+ t = time.time()
+ value = original_method(*args, **kwargs)
+ t = time.time() - t
+ finally:
+ cache._djdt_panel = self
self._store_call_info(
name=name,
@@ -141,40 +166,6 @@ def _record_call(self, cache, name, original_method, args, kwargs):
)
return value
- def _monkey_patch_method(self, cache, name):
- original_method = getattr(cache, name)
-
- @functools.wraps(original_method)
- def wrapper(*args, **kwargs):
- # If this call is being made as part of the implementation of another cache
- # method, don't record it.
- if cache._djdt_recording:
- return original_method(*args, **kwargs)
- else:
- return self._record_call(cache, name, original_method, args, kwargs)
-
- wrapper._djdt_wrapped = original_method
- setattr(cache, name, wrapper)
-
- def _monkey_patch_cache(self, cache):
- if not hasattr(cache, "_djdt_patched"):
- for name in WRAPPED_CACHE_METHODS:
- self._monkey_patch_method(cache, name)
- cache._djdt_patched = True
- cache._djdt_recording = False
-
- @staticmethod
- def _unmonkey_patch_cache(cache):
- if hasattr(cache, "_djdt_patched"):
- for name in WRAPPED_CACHE_METHODS:
- original_method = getattr(cache, name)._djdt_wrapped
- if original_method.__func__ == getattr(cache.__class__, name):
- delattr(cache, name)
- else:
- setattr(cache, name, original_method)
- del cache._djdt_patched
- del cache._djdt_recording
-
# Implement the Panel API
nav_title = _("Cache")
@@ -204,7 +195,8 @@ def enable_instrumentation(self):
# the .ready() method will ensure that any new cache connections that get opened
# during this request will also be monkey patched.
for cache in caches.all(initialized_only=True):
- self._monkey_patch_cache(cache)
+ _monkey_patch_cache(cache)
+ cache._djdt_panel = self
# Mark this panel instance as the current one for the active thread/async task
# context. This will be used by the CacheHander.create_connection() monkey
# patch.
@@ -214,7 +206,7 @@ def disable_instrumentation(self):
if hasattr(self._context_locals, "current_instance"):
del self._context_locals.current_instance
for cache in caches.all(initialized_only=True):
- self._unmonkey_patch_cache(cache)
+ cache._djdt_panel = None
def generate_stats(self, request, response):
self.record_stats(
diff --git a/docs/changes.rst b/docs/changes.rst
index 014233997..6844b5ce8 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -8,6 +8,9 @@ Pending
indentation of ``CASE`` statements and stopped simplifying ``.count()``
queries.
* Added support for the new STORAGES setting in Django 4.2 for static files.
+* Reworked the cache panel instrumentation code to no longer attempt to undo
+ monkey patching of cache methods, as that turned out to be fragile in the
+ presence of other code which also monkey patches those methods.
4.0.0 (2023-04-03)
------------------
From 29f795f7150cca8ee20386a9ee903426ffa41a48 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Gergely=20Kalm=C3=A1r?=
Date: Tue, 9 May 2023 16:45:56 +0200
Subject: [PATCH 23/33] Add theming support (#1760)
---
.../static/debug_toolbar/css/toolbar.css | 26 ++++++++++++-------
docs/changes.rst | 1 +
docs/configuration.rst | 23 ++++++++++++++++
docs/spelling_wordlist.txt | 2 ++
4 files changed, 43 insertions(+), 9 deletions(-)
diff --git a/debug_toolbar/static/debug_toolbar/css/toolbar.css b/debug_toolbar/static/debug_toolbar/css/toolbar.css
index 154aaafa5..4adb0abb5 100644
--- a/debug_toolbar/static/debug_toolbar/css/toolbar.css
+++ b/debug_toolbar/static/debug_toolbar/css/toolbar.css
@@ -1,3 +1,17 @@
+/* Variable definitions */
+:root {
+ /* Font families are the same as in Django admin/css/base.css */
+ --djdt-font-family-primary: -apple-system, BlinkMacSystemFont, "Segoe UI",
+ system-ui, Roboto, "Helvetica Neue", Arial, sans-serif,
+ "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
+ "Noto Color Emoji";
+ --djdt-font-family-monospace: ui-monospace, Menlo, Monaco, "Cascadia Mono",
+ "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace",
+ "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New",
+ monospace, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
+ "Noto Color Emoji";
+}
+
/* Debug Toolbar CSS Reset, adapted from Eric Meyer's CSS Reset */
#djDebug {
color: #000;
@@ -77,9 +91,7 @@
color: #000;
vertical-align: baseline;
background-color: transparent;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui,
- Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji",
- "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ font-family: var(--djdt-font-family-primary);
text-align: left;
text-shadow: none;
white-space: normal;
@@ -181,7 +193,7 @@
#djDebug #djDebugToolbar li.djdt-active:before {
content: "▶";
- font-family: sans-serif;
+ font-family: var(--djdt-font-family-primary);
position: absolute;
left: 0;
top: 50%;
@@ -246,11 +258,7 @@
#djDebug pre,
#djDebug code {
display: block;
- font-family: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono",
- "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro",
- "Fira Mono", "Droid Sans Mono", "Courier New", monospace,
- "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
- "Noto Color Emoji";
+ font-family: var(--djdt-font-family-monospace);
overflow: auto;
}
diff --git a/docs/changes.rst b/docs/changes.rst
index 6844b5ce8..2dc3f2528 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -8,6 +8,7 @@ Pending
indentation of ``CASE`` statements and stopped simplifying ``.count()``
queries.
* Added support for the new STORAGES setting in Django 4.2 for static files.
+* Added support for theme overrides.
* Reworked the cache panel instrumentation code to no longer attempt to undo
monkey patching of cache methods, as that turned out to be fragile in the
presence of other code which also monkey patches those methods.
diff --git a/docs/configuration.rst b/docs/configuration.rst
index 86cb65ce4..887608c6e 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -338,3 +338,26 @@ Here's what a slightly customized toolbar configuration might look like::
# Panel options
'SQL_WARNING_THRESHOLD': 100, # milliseconds
}
+
+Theming support
+---------------
+The debug toolbar uses CSS variables to define fonts. This allows changing
+fonts without having to override many individual CSS rules. For example, if
+you preferred Roboto instead of the default list of fonts you could add a
+**debug_toolbar/base.html** template override to your project:
+
+.. code-block:: django
+
+ {% extends 'debug_toolbar/base.html' %}
+
+ {% block css %}{{ block.super }}
+
+ {% endblock %}
+
+The list of CSS variables are defined at
+`debug_toolbar/static/debug_toolbar/css/toolbar.css
+`_
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index 1741b405b..d5aa73afe 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -40,12 +40,14 @@ Pympler
querysets
refactoring
resizing
+Roboto
spellchecking
spooler
stacktrace
stacktraces
startup
timeline
+theming
tox
Transifex
unhashable
From 267de8f684b6e3eb044fa0550f47fa478ba1a894 Mon Sep 17 00:00:00 2001
From: Daniel Harding
Date: Wed, 10 May 2023 15:44:26 +0300
Subject: [PATCH 24/33] Replace deprecated GitHub Actions set-output commands
(#1774)
See https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/
for more information.
---
.github/workflows/test.yml | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 8187eee52..cc4b9a456 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -40,7 +40,7 @@ jobs:
- name: Get pip cache dir
id: pip-cache
run: |
- echo "::set-output name=dir::$(pip cache dir)"
+ echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v3
@@ -112,7 +112,7 @@ jobs:
- name: Get pip cache dir
id: pip-cache
run: |
- echo "::set-output name=dir::$(pip cache dir)"
+ echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v3
@@ -165,7 +165,7 @@ jobs:
- name: Get pip cache dir
id: pip-cache
run: |
- echo "::set-output name=dir::$(pip cache dir)"
+ echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v3
@@ -240,7 +240,7 @@ jobs:
- name: Get pip cache dir
id: pip-cache
run: |
- echo "::set-output name=dir::$(pip cache dir)"
+ echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
- name: Cache
uses: actions/cache@v3
From df67c89c3be189d02301dd88416ec4671d8055ca Mon Sep 17 00:00:00 2001
From: Daniel Harding
Date: Tue, 9 May 2023 15:24:44 +0300
Subject: [PATCH 25/33] Remove unnecessary return statement
---
debug_toolbar/panels/sql/tracking.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/debug_toolbar/panels/sql/tracking.py b/debug_toolbar/panels/sql/tracking.py
index 565d9244b..a3ec02a3e 100644
--- a/debug_toolbar/panels/sql/tracking.py
+++ b/debug_toolbar/panels/sql/tracking.py
@@ -63,7 +63,6 @@ def chunked_cursor(*args, **kwargs):
connection.cursor = cursor
connection.chunked_cursor = chunked_cursor
- return cursor
def unwrap_cursor(connection):
From 217238bced43f9e3045b3197ffc94d5985967c65 Mon Sep 17 00:00:00 2001
From: Daniel Harding
Date: Wed, 10 May 2023 14:36:33 +0300
Subject: [PATCH 26/33] Don't try to undo DatabaseWrapper monkey patching
Prior to this commit, the SQLPanel would monkey patch the .cursor() and
.chunked_cursor() methods of each DatabaseWrapper connection in
.enable_instrumentation(), and then undo the monkey patch in
.disable_instrumentation(). However, for the same reasons as
a6b65a7c509589a635d5c3889ee8ea7a1e10b707, stop trying to undo the monkey
patching in .disable_instrumentation() and simply use a .djdt_logger
attribute on the DatabaseWrapper to determine if the cursors should be
wrapped.
---
debug_toolbar/panels/sql/panel.py | 7 ++---
debug_toolbar/panels/sql/tracking.py | 39 +++++++++++-----------------
2 files changed, 19 insertions(+), 27 deletions(-)
diff --git a/debug_toolbar/panels/sql/panel.py b/debug_toolbar/panels/sql/panel.py
index 90e2ba812..c8576e16f 100644
--- a/debug_toolbar/panels/sql/panel.py
+++ b/debug_toolbar/panels/sql/panel.py
@@ -10,7 +10,7 @@
from debug_toolbar.panels import Panel
from debug_toolbar.panels.sql import views
from debug_toolbar.panels.sql.forms import SQLSelectForm
-from debug_toolbar.panels.sql.tracking import unwrap_cursor, wrap_cursor
+from debug_toolbar.panels.sql.tracking import wrap_cursor
from debug_toolbar.panels.sql.utils import contrasting_color_generator, reformat_sql
from debug_toolbar.utils import render_stacktrace
@@ -190,11 +190,12 @@ def get_urls(cls):
def enable_instrumentation(self):
# This is thread-safe because database connections are thread-local.
for connection in connections.all():
- wrap_cursor(connection, self)
+ wrap_cursor(connection)
+ connection._djdt_logger = self
def disable_instrumentation(self):
for connection in connections.all():
- unwrap_cursor(connection)
+ connection._djdt_logger = None
def generate_stats(self, request, response):
colors = contrasting_color_generator()
diff --git a/debug_toolbar/panels/sql/tracking.py b/debug_toolbar/panels/sql/tracking.py
index a3ec02a3e..e1740dada 100644
--- a/debug_toolbar/panels/sql/tracking.py
+++ b/debug_toolbar/panels/sql/tracking.py
@@ -3,6 +3,7 @@
import json
from time import time
+import django.test.testcases
from django.utils.encoding import force_str
from debug_toolbar import settings as dt_settings
@@ -31,10 +32,15 @@ class SQLQueryTriggered(Exception):
"""Thrown when template panel triggers a query"""
-def wrap_cursor(connection, panel):
+def wrap_cursor(connection):
+ # If running a Django SimpleTestCase, which isn't allowed to access the database,
+ # don't perform any monkey patching.
+ if isinstance(connection.cursor, django.test.testcases._DatabaseFailure):
+ return
if not hasattr(connection, "_djdt_cursor"):
connection._djdt_cursor = connection.cursor
connection._djdt_chunked_cursor = connection.chunked_cursor
+ connection._djdt_logger = None
def cursor(*args, **kwargs):
# Per the DB API cursor() does not accept any arguments. There's
@@ -43,48 +49,33 @@ def cursor(*args, **kwargs):
# See:
# https://github.com/jazzband/django-debug-toolbar/pull/615
# https://github.com/jazzband/django-debug-toolbar/pull/896
+ logger = connection._djdt_logger
+ cursor = connection._djdt_cursor(*args, **kwargs)
+ if logger is None:
+ return cursor
if allow_sql.get():
wrapper = NormalCursorWrapper
else:
wrapper = ExceptionCursorWrapper
- return wrapper(connection._djdt_cursor(*args, **kwargs), connection, panel)
+ return wrapper(cursor, connection, logger)
def chunked_cursor(*args, **kwargs):
# prevent double wrapping
# solves https://github.com/jazzband/django-debug-toolbar/issues/1239
+ logger = connection._djdt_logger
cursor = connection._djdt_chunked_cursor(*args, **kwargs)
- if not isinstance(cursor, BaseCursorWrapper):
+ if logger is not None and not isinstance(cursor, BaseCursorWrapper):
if allow_sql.get():
wrapper = NormalCursorWrapper
else:
wrapper = ExceptionCursorWrapper
- return wrapper(cursor, connection, panel)
+ return wrapper(cursor, connection, logger)
return cursor
connection.cursor = cursor
connection.chunked_cursor = chunked_cursor
-def unwrap_cursor(connection):
- if hasattr(connection, "_djdt_cursor"):
- # Sometimes the cursor()/chunked_cursor() methods of the DatabaseWrapper
- # instance are already monkey patched before wrap_cursor() is called. (In
- # particular, Django's SimpleTestCase monkey patches those methods for any
- # disallowed databases to raise an exception if they are accessed.) Thus only
- # delete our monkey patch if the method we saved is the same as the class
- # method. Otherwise, restore the prior monkey patch from our saved method.
- if connection._djdt_cursor == connection.__class__.cursor:
- del connection.cursor
- else:
- connection.cursor = connection._djdt_cursor
- del connection._djdt_cursor
- if connection._djdt_chunked_cursor == connection.__class__.chunked_cursor:
- del connection.chunked_cursor
- else:
- connection.chunked_cursor = connection._djdt_chunked_cursor
- del connection._djdt_chunked_cursor
-
-
class BaseCursorWrapper:
pass
From 32ab3630e642ec4482f386ec2d241214ee1be07f Mon Sep 17 00:00:00 2001
From: Daniel Harding
Date: Tue, 9 May 2023 15:32:20 +0300
Subject: [PATCH 27/33] Fix psycopg3 tests
Several tests (such as SQLPanelTestCase.test_cursor_wrapper_singleton)
are written to ensure that only a single cursor wrapper is instantiated
during the test. However, this fails when using Django's psycopg3
backend, since the .last_executed_query() call in
NormalCursorWrapper._record() ends up creating an additional cursor (via
[1]). To avoid this wrapping this additional cursor, set the
DatabaseWrapper's ._djdt_logger attribute to None before calling
.last_executed_query() and restore it when finished. This will cause
the monkey-patched DatabaseWrapper .cursor() and .chunked_cursor()
methods to return the original cursor without wrapping during that call.
[1] https://github.com/django/django/blob/4.2.1/django/db/backends/postgresql/psycopg_any.py#L21
---
debug_toolbar/panels/sql/tracking.py | 23 +++++++++++++++++++----
1 file changed, 19 insertions(+), 4 deletions(-)
diff --git a/debug_toolbar/panels/sql/tracking.py b/debug_toolbar/panels/sql/tracking.py
index e1740dada..4add1fce7 100644
--- a/debug_toolbar/panels/sql/tracking.py
+++ b/debug_toolbar/panels/sql/tracking.py
@@ -144,6 +144,21 @@ def _decode(self, param):
except UnicodeDecodeError:
return "(encoded string)"
+ def _last_executed_query(self, sql, params):
+ """Get the last executed query from the connection."""
+ # Django's psycopg3 backend creates a new cursor in its implementation of the
+ # .last_executed_query() method. To avoid wrapping that cursor, temporarily set
+ # the DatabaseWrapper's ._djdt_logger attribute to None. This will cause the
+ # monkey-patched .cursor() and .chunked_cursor() methods to skip the wrapping
+ # process during the .last_executed_query() call.
+ self.db._djdt_logger = None
+ try:
+ return self.db.ops.last_executed_query(
+ self.cursor, sql, self._quote_params(params)
+ )
+ finally:
+ self.db._djdt_logger = self.logger
+
def _record(self, method, sql, params):
alias = self.db.alias
vendor = self.db.vendor
@@ -176,9 +191,7 @@ def _record(self, method, sql, params):
params = {
"vendor": vendor,
"alias": alias,
- "sql": self.db.ops.last_executed_query(
- self.cursor, sql, self._quote_params(params)
- ),
+ "sql": self._last_executed_query(sql, params),
"duration": duration,
"raw_sql": sql,
"params": _params,
@@ -186,7 +199,9 @@ def _record(self, method, sql, params):
"stacktrace": get_stack_trace(skip=2),
"start_time": start_time,
"stop_time": stop_time,
- "is_slow": duration > dt_settings.get_config()["SQL_WARNING_THRESHOLD"],
+ "is_slow": (
+ duration > dt_settings.get_config()["SQL_WARNING_THRESHOLD"]
+ ),
"is_select": sql.lower().strip().startswith("select"),
"template_info": template_info,
}
From e7575e87dc9e2d2560b87d6fd5a123b9398cbd34 Mon Sep 17 00:00:00 2001
From: Tim Schilling
Date: Tue, 9 May 2023 20:28:58 -0500
Subject: [PATCH 28/33] Inherit from django.db.backends.utils.CursorWrapper
This switches the Debug Toolbar cursor wrappers to inherit from the
Django class django.db.backends.utils.CursorWrapper. This reduces some
of the code we need.
---
debug_toolbar/panels/sql/tracking.py | 46 +++++++++-------------------
1 file changed, 14 insertions(+), 32 deletions(-)
diff --git a/debug_toolbar/panels/sql/tracking.py b/debug_toolbar/panels/sql/tracking.py
index 4add1fce7..425e4e5cc 100644
--- a/debug_toolbar/panels/sql/tracking.py
+++ b/debug_toolbar/panels/sql/tracking.py
@@ -4,6 +4,7 @@
from time import time
import django.test.testcases
+from django.db.backends.utils import CursorWrapper
from django.utils.encoding import force_str
from debug_toolbar import settings as dt_settings
@@ -57,54 +58,47 @@ def cursor(*args, **kwargs):
wrapper = NormalCursorWrapper
else:
wrapper = ExceptionCursorWrapper
- return wrapper(cursor, connection, logger)
+ return wrapper(cursor.cursor, connection, logger)
def chunked_cursor(*args, **kwargs):
# prevent double wrapping
# solves https://github.com/jazzband/django-debug-toolbar/issues/1239
logger = connection._djdt_logger
cursor = connection._djdt_chunked_cursor(*args, **kwargs)
- if logger is not None and not isinstance(cursor, BaseCursorWrapper):
+ if logger is not None and not isinstance(cursor, DjDTCursorWrapper):
if allow_sql.get():
wrapper = NormalCursorWrapper
else:
wrapper = ExceptionCursorWrapper
- return wrapper(cursor, connection, logger)
+ return wrapper(cursor.cursor, connection, logger)
return cursor
connection.cursor = cursor
connection.chunked_cursor = chunked_cursor
-class BaseCursorWrapper:
- pass
+class DjDTCursorWrapper(CursorWrapper):
+ def __init__(self, cursor, db, logger):
+ super().__init__(cursor, db)
+ # logger must implement a ``record`` method
+ self.logger = logger
-class ExceptionCursorWrapper(BaseCursorWrapper):
+class ExceptionCursorWrapper(DjDTCursorWrapper):
"""
Wraps a cursor and raises an exception on any operation.
Used in Templates panel.
"""
- def __init__(self, cursor, db, logger):
- pass
-
def __getattr__(self, attr):
raise SQLQueryTriggered()
-class NormalCursorWrapper(BaseCursorWrapper):
+class NormalCursorWrapper(DjDTCursorWrapper):
"""
Wraps a cursor and logs queries.
"""
- def __init__(self, cursor, db, logger):
- self.cursor = cursor
- # Instance of a BaseDatabaseWrapper subclass
- self.db = db
- # logger must implement a ``record`` method
- self.logger = logger
-
def _quote_expr(self, element):
if isinstance(element, str):
return "'%s'" % element.replace("'", "''")
@@ -246,22 +240,10 @@ def _record(self, method, sql, params):
self.logger.record(**params)
def callproc(self, procname, params=None):
- return self._record(self.cursor.callproc, procname, params)
+ return self._record(super().callproc, procname, params)
def execute(self, sql, params=None):
- return self._record(self.cursor.execute, sql, params)
+ return self._record(super().execute, sql, params)
def executemany(self, sql, param_list):
- return self._record(self.cursor.executemany, sql, param_list)
-
- def __getattr__(self, attr):
- return getattr(self.cursor, attr)
-
- def __iter__(self):
- return iter(self.cursor)
-
- def __enter__(self):
- return self
-
- def __exit__(self, type, value, traceback):
- self.close()
+ return self._record(super().executemany, sql, param_list)
From 43a87f7d52b1cb55fac0e19c246aa66cd2eb3795 Mon Sep 17 00:00:00 2001
From: Daniel Harding
Date: Mon, 15 May 2023 15:39:05 +0300
Subject: [PATCH 29/33] Replace time.time() with time.perf_counter() (#1777)
time.perf_counter() is guaranteed to use the highest-resolution clock
available, and is monotonically increasing, neither of which are true
for time.time(). Thanks @matthiask for the suggestion!
---
debug_toolbar/management/commands/debugsqlshell.py | 6 +++---
debug_toolbar/panels/cache.py | 6 +++---
debug_toolbar/panels/sql/tracking.py | 6 +++---
debug_toolbar/panels/timer.py | 6 +++---
docs/changes.rst | 2 ++
5 files changed, 14 insertions(+), 12 deletions(-)
diff --git a/debug_toolbar/management/commands/debugsqlshell.py b/debug_toolbar/management/commands/debugsqlshell.py
index 93514d121..b80577232 100644
--- a/debug_toolbar/management/commands/debugsqlshell.py
+++ b/debug_toolbar/management/commands/debugsqlshell.py
@@ -1,4 +1,4 @@
-from time import time
+from time import perf_counter
import sqlparse
from django.core.management.commands.shell import Command
@@ -19,12 +19,12 @@
class PrintQueryWrapper(base_module.CursorDebugWrapper):
def execute(self, sql, params=()):
- start_time = time()
+ start_time = perf_counter()
try:
return self.cursor.execute(sql, params)
finally:
raw_sql = self.db.ops.last_executed_query(self.cursor, sql, params)
- end_time = time()
+ end_time = perf_counter()
duration = (end_time - start_time) * 1000
formatted_sql = sqlparse.format(raw_sql, reindent=True)
print(f"{formatted_sql} [{duration:.2f}ms]")
diff --git a/debug_toolbar/panels/cache.py b/debug_toolbar/panels/cache.py
index 69a899ea1..31ce70988 100644
--- a/debug_toolbar/panels/cache.py
+++ b/debug_toolbar/panels/cache.py
@@ -1,5 +1,5 @@
import functools
-import time
+from time import perf_counter
from asgiref.local import Local
from django.conf import settings
@@ -148,9 +148,9 @@ def _record_call(self, cache, name, original_method, args, kwargs):
# the course of this call, and then reset it back afterward.
cache._djdt_panel = None
try:
- t = time.time()
+ start_time = perf_counter()
value = original_method(*args, **kwargs)
- t = time.time() - t
+ t = perf_counter() - start_time
finally:
cache._djdt_panel = self
diff --git a/debug_toolbar/panels/sql/tracking.py b/debug_toolbar/panels/sql/tracking.py
index 425e4e5cc..a85ac51ad 100644
--- a/debug_toolbar/panels/sql/tracking.py
+++ b/debug_toolbar/panels/sql/tracking.py
@@ -1,7 +1,7 @@
import contextvars
import datetime
import json
-from time import time
+from time import perf_counter
import django.test.testcases
from django.db.backends.utils import CursorWrapper
@@ -162,11 +162,11 @@ def _record(self, method, sql, params):
conn = self.db.connection
initial_conn_status = conn.info.transaction_status
- start_time = time()
+ start_time = perf_counter()
try:
return method(sql, params)
finally:
- stop_time = time()
+ stop_time = perf_counter()
duration = (stop_time - start_time) * 1000
_params = ""
try:
diff --git a/debug_toolbar/panels/timer.py b/debug_toolbar/panels/timer.py
index 801c9c6fd..554798e7d 100644
--- a/debug_toolbar/panels/timer.py
+++ b/debug_toolbar/panels/timer.py
@@ -1,4 +1,4 @@
-import time
+from time import perf_counter
from django.template.loader import render_to_string
from django.templatetags.static import static
@@ -59,7 +59,7 @@ def scripts(self):
return scripts
def process_request(self, request):
- self._start_time = time.time()
+ self._start_time = perf_counter()
if self.has_content:
self._start_rusage = resource.getrusage(resource.RUSAGE_SELF)
return super().process_request(request)
@@ -67,7 +67,7 @@ def process_request(self, request):
def generate_stats(self, request, response):
stats = {}
if hasattr(self, "_start_time"):
- stats["total_time"] = (time.time() - self._start_time) * 1000
+ stats["total_time"] = (perf_counter() - self._start_time) * 1000
if hasattr(self, "_start_rusage"):
self._end_rusage = resource.getrusage(resource.RUSAGE_SELF)
stats["utime"] = 1000 * self._elapsed_ru("ru_utime")
diff --git a/docs/changes.rst b/docs/changes.rst
index 2dc3f2528..7c0338385 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -12,6 +12,8 @@ Pending
* Reworked the cache panel instrumentation code to no longer attempt to undo
monkey patching of cache methods, as that turned out to be fragile in the
presence of other code which also monkey patches those methods.
+* Update all timing code that used :py:func:`time.time()` to use
+ :py:func:`time.perf_counter()` instead.
4.0.0 (2023-04-03)
------------------
From 1960ca38c91c2dde1a66ca4769c6fc7aac176c7e Mon Sep 17 00:00:00 2001
From: Daniel Harding
Date: Mon, 15 May 2023 15:48:24 +0300
Subject: [PATCH 30/33] Bump coverage percentage to 94% (#1776)
---
README.rst | 2 +-
pyproject.toml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.rst b/README.rst
index e3a40b8a1..29e4e1b0e 100644
--- a/README.rst
+++ b/README.rst
@@ -16,7 +16,7 @@ Django Debug Toolbar |latest-version|
:target: https://github.com/jazzband/django-debug-toolbar/actions
:alt: Build Status
-.. |coverage| image:: https://img.shields.io/badge/Coverage-93%25-green
+.. |coverage| image:: https://img.shields.io/badge/Coverage-94%25-green
:target: https://github.com/jazzband/django-debug-toolbar/actions/workflows/test.yml?query=branch%3Amain
:alt: Test coverage status
diff --git a/pyproject.toml b/pyproject.toml
index b1ccc67f2..8729cb911 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -73,5 +73,5 @@ source = ["src", ".tox/*/site-packages"]
[tool.coverage.report]
# Update coverage badge link in README.rst when fail_under changes
-fail_under = 93
+fail_under = 94
show_missing = true
From af7d15f7d1dad69b039fbc6257331a4002c76903 Mon Sep 17 00:00:00 2001
From: Matthieu <2222826+d9pouces@users.noreply.github.com>
Date: Tue, 16 May 2023 03:44:10 +0200
Subject: [PATCH 31/33] If wsgi.multiprocess isn't set, render panels on each
request.
* Avoid an exception with Django-channels
If you apply Django middlewares on a HttpRequest without "wsgi.multiprocess" in META, an exception is raised by DJT.
This simple patch avoids this bug.
* If wsgi.multiprocess isn't set, render panels on each request.
The likely cause of this is that the application is using ASGI since
wsgi.multiprocess is a required key for a WSGI application. Since the
toolbar currently doesn't support async applications, it's pretty likely
that it won't work for this request.
If you're a developer reading this and you think this is wrong, you can set
the RENDER_PANELS setting to forcibly control this setting.
---------
Co-authored-by: tschilling
---
debug_toolbar/toolbar.py | 10 +++++++---
docs/changes.rst | 6 ++++++
tests/test_integration.py | 29 +++++++++++++++++++++++++++++
3 files changed, 42 insertions(+), 3 deletions(-)
diff --git a/debug_toolbar/toolbar.py b/debug_toolbar/toolbar.py
index 40e758107..31010f47f 100644
--- a/debug_toolbar/toolbar.py
+++ b/debug_toolbar/toolbar.py
@@ -95,9 +95,13 @@ def should_render_panels(self):
If False, the panels will be loaded via Ajax.
"""
- render_panels = self.config["RENDER_PANELS"]
- if render_panels is None:
- render_panels = self.request.META["wsgi.multiprocess"]
+ if (render_panels := self.config["RENDER_PANELS"]) is None:
+ # If wsgi.multiprocess isn't in the headers, then it's likely
+ # being served by ASGI. This type of set up is most likely
+ # incompatible with the toolbar until
+ # https://github.com/jazzband/django-debug-toolbar/issues/1430
+ # is resolved.
+ render_panels = self.request.META.get("wsgi.multiprocess", True)
return render_panels
# Handle storing toolbars in memory and fetching them later on
diff --git a/docs/changes.rst b/docs/changes.rst
index 7c0338385..75b95c562 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -14,6 +14,12 @@ Pending
presence of other code which also monkey patches those methods.
* Update all timing code that used :py:func:`time.time()` to use
:py:func:`time.perf_counter()` instead.
+* Made the check on ``request.META["wsgi.multiprocess"]`` optional, but
+ defaults to forcing the toolbar to render the panels on each request. This
+ is because it's likely an ASGI application that's serving the responses
+ and that's more likely to be an incompatible setup. If you find that this
+ is incorrect for you in particular, you can use the ``RENDER_PANELS``
+ setting to forcibly control this logic.
4.0.0 (2023-04-03)
------------------
diff --git a/tests/test_integration.py b/tests/test_integration.py
index b292dcbf0..71340709a 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -65,6 +65,35 @@ def test_show_toolbar_INTERNAL_IPS(self):
with self.settings(INTERNAL_IPS=[]):
self.assertFalse(show_toolbar(self.request))
+ def test_should_render_panels_RENDER_PANELS(self):
+ """
+ The toolbar should force rendering panels on each request
+ based on the RENDER_PANELS setting.
+ """
+ toolbar = DebugToolbar(self.request, self.get_response)
+ self.assertFalse(toolbar.should_render_panels())
+ toolbar.config["RENDER_PANELS"] = True
+ self.assertTrue(toolbar.should_render_panels())
+ toolbar.config["RENDER_PANELS"] = None
+ self.assertTrue(toolbar.should_render_panels())
+
+ def test_should_render_panels_multiprocess(self):
+ """
+ The toolbar should render the panels on each request when wsgi.multiprocess
+ is True or missing.
+ """
+ request = rf.get("/")
+ request.META["wsgi.multiprocess"] = True
+ toolbar = DebugToolbar(request, self.get_response)
+ toolbar.config["RENDER_PANELS"] = None
+ self.assertTrue(toolbar.should_render_panels())
+
+ request.META["wsgi.multiprocess"] = False
+ self.assertFalse(toolbar.should_render_panels())
+
+ request.META.pop("wsgi.multiprocess")
+ self.assertTrue(toolbar.should_render_panels())
+
def _resolve_stats(self, path):
# takes stats from Request panel
self.request.path = path
From e9fff24ef07286eacb4b5ea7b641d8d317cfee16 Mon Sep 17 00:00:00 2001
From: "pre-commit-ci[bot]"
<66853113+pre-commit-ci[bot]@users.noreply.github.com>
Date: Mon, 15 May 2023 19:35:45 +0000
Subject: [PATCH 32/33] [pre-commit.ci] pre-commit autoupdate
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
updates:
- [github.com/tox-dev/pyproject-fmt: 0.11.1 → 0.11.2](https://github.com/tox-dev/pyproject-fmt/compare/0.11.1...0.11.2)
---
.pre-commit-config.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7b60c7f71..124892d78 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -60,7 +60,7 @@ repos:
language_version: python3
entry: black --target-version=py38
- repo: https://github.com/tox-dev/pyproject-fmt
- rev: 0.11.1
+ rev: 0.11.2
hooks:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
From de7e19c5ef08445ca592c75dfa3dad22c04992a1 Mon Sep 17 00:00:00 2001
From: Tim Schilling
Date: Tue, 16 May 2023 01:31:49 -0500
Subject: [PATCH 33/33] Version 4.1.0 (#1779)
---
README.rst | 2 +-
debug_toolbar/__init__.py | 2 +-
docs/changes.rst | 3 +++
docs/conf.py | 2 +-
4 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/README.rst b/README.rst
index 29e4e1b0e..b10a9ad91 100644
--- a/README.rst
+++ b/README.rst
@@ -44,7 +44,7 @@ Here's a screenshot of the toolbar in action:
In addition to the built-in panels, a number of third-party panels are
contributed by the community.
-The current stable version of the Debug Toolbar is 4.0.0. It works on
+The current stable version of the Debug Toolbar is 4.1.0. It works on
Django ≥ 3.2.4.
Documentation, including installation and configuration instructions, is
diff --git a/debug_toolbar/__init__.py b/debug_toolbar/__init__.py
index 109d7d4d7..1a9cf7c93 100644
--- a/debug_toolbar/__init__.py
+++ b/debug_toolbar/__init__.py
@@ -4,7 +4,7 @@
# Do not use pkg_resources to find the version but set it here directly!
# see issue #1446
-VERSION = "4.0.0"
+VERSION = "4.1.0"
# Code that discovers files or modules in INSTALLED_APPS imports this module.
urls = "debug_toolbar.urls", APP_NAME
diff --git a/docs/changes.rst b/docs/changes.rst
index 75b95c562..ad6607e34 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -4,6 +4,9 @@ Change log
Pending
-------
+4.1.0 (2023-05-15)
+------------------
+
* Improved SQL statement formatting performance. Additionally, fixed the
indentation of ``CASE`` statements and stopped simplifying ``.count()``
queries.
diff --git a/docs/conf.py b/docs/conf.py
index f6795a6d3..2e4886c9c 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -25,7 +25,7 @@
copyright = copyright.format(datetime.date.today().year)
# The full version, including alpha/beta/rc tags
-release = "4.0.0"
+release = "4.1.0"
# -- General configuration ---------------------------------------------------