diff --git a/.flake8 b/.flake8
deleted file mode 100644
index 7d44b7eca..000000000
--- a/.flake8
+++ /dev/null
@@ -1,4 +0,0 @@
-# TODO: move this to pyproject.toml when supported, see https://github.com/PyCQA/flake8/issues/234
-
-[flake8]
-extend-ignore = E203, E501
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index cc4b9a456..9dece68ef 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -20,9 +20,9 @@ jobs:
mariadb:
image: mariadb
env:
- MYSQL_ROOT_PASSWORD: debug_toolbar
+ MARIADB_ROOT_PASSWORD: debug_toolbar
options: >-
- --health-cmd "mysqladmin ping"
+ --health-cmd "mariadb-admin ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 124892d78..001b07e34 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -7,63 +7,53 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- id: mixed-line-ending
-- repo: https://github.com/pycqa/flake8
- rev: 6.0.0
- hooks:
- - id: flake8
+ - id: file-contents-sorter
+ files: docs/spelling_wordlist.txt
- repo: https://github.com/pycqa/doc8
rev: v1.1.1
hooks:
- id: doc8
-- repo: https://github.com/asottile/pyupgrade
- rev: v3.4.0
- hooks:
- - id: pyupgrade
- args: [--py38-plus]
- repo: https://github.com/adamchainz/django-upgrade
- rev: 1.13.0
+ rev: 1.14.0
hooks:
- id: django-upgrade
args: [--target-version, "3.2"]
-- repo: https://github.com/pycqa/isort
- rev: 5.12.0
- hooks:
- - id: isort
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
hooks:
- - id: python-check-blanket-noqa
- - id: python-check-mock-methods
- - id: python-no-eval
- - id: python-no-log-warn
- id: rst-backticks
- id: rst-directive-colons
- repo: https://github.com/pre-commit/mirrors-prettier
- rev: v3.0.0-alpha.9-for-vscode
+ rev: v3.0.1
hooks:
- id: prettier
types_or: [javascript, css]
args:
- --trailing-comma=es5
- repo: https://github.com/pre-commit/mirrors-eslint
- rev: v8.40.0
+ rev: v8.46.0
hooks:
- id: eslint
files: \.js?$
types: [file]
args:
- --fix
+- repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: 'v0.0.282'
+ hooks:
+ - id: ruff
+ args: [--fix, --exit-non-zero-on-fix]
- repo: https://github.com/psf/black
- rev: 23.3.0
+ rev: 23.7.0
hooks:
- id: black
language_version: python3
entry: black --target-version=py38
- repo: https://github.com/tox-dev/pyproject-fmt
- rev: 0.11.2
+ rev: 0.13.0
hooks:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
- rev: v0.12.2
+ rev: v0.13
hooks:
- id: validate-pyproject
diff --git a/debug_toolbar/__init__.py b/debug_toolbar/__init__.py
index 1a9cf7c93..dbe08451f 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.1.0"
+VERSION = "4.2.0"
# Code that discovers files or modules in INSTALLED_APPS imports this module.
urls = "debug_toolbar.urls", APP_NAME
diff --git a/debug_toolbar/apps.py b/debug_toolbar/apps.py
index c55b75392..0a10a4b08 100644
--- a/debug_toolbar/apps.py
+++ b/debug_toolbar/apps.py
@@ -1,4 +1,5 @@
import inspect
+import mimetypes
from django.apps import AppConfig
from django.conf import settings
@@ -32,8 +33,26 @@ def check_template_config(config):
included in the loaders.
If custom loaders are specified, then APP_DIRS must be True.
"""
+
+ def flat_loaders(loaders):
+ """
+ Recursively flatten the settings list of template loaders.
+
+ Check for (loader, [child_loaders]) tuples.
+ Django's default cached loader uses this pattern.
+ """
+ for loader in loaders:
+ if isinstance(loader, tuple):
+ yield loader[0]
+ yield from flat_loaders(loader[1])
+ else:
+ yield loader
+
app_dirs = config.get("APP_DIRS", False)
loaders = config.get("OPTIONS", {}).get("loaders", None)
+ if loaders:
+ loaders = list(flat_loaders(loaders))
+
# By default the app loader is included.
has_app_loaders = (
loaders is None or "django.template.loaders.app_directories.Loader" in loaders
@@ -156,3 +175,34 @@ def check_panels(app_configs, **kwargs):
)
)
return errors
+
+
+@register()
+def js_mimetype_check(app_configs, **kwargs):
+ """
+ Check that JavaScript files are resolving to the correct content type.
+ """
+ # Ideally application/javascript is returned, but text/javascript is
+ # acceptable.
+ javascript_types = {"application/javascript", "text/javascript"}
+ check_failed = not set(mimetypes.guess_type("toolbar.js")).intersection(
+ javascript_types
+ )
+ if check_failed:
+ return [
+ Warning(
+ "JavaScript files are resolving to the wrong content type.",
+ hint="The Django Debug Toolbar may not load properly while mimetypes are misconfigured. "
+ "See the Django documentation for an explanation of why this occurs.\n"
+ "https://docs.djangoproject.com/en/stable/ref/contrib/staticfiles/#static-file-development-view\n"
+ "\n"
+ "This typically occurs on Windows machines. The suggested solution is to modify "
+ "HKEY_CLASSES_ROOT in the registry to specify the content type for JavaScript "
+ "files.\n"
+ "\n"
+ "[HKEY_CLASSES_ROOT\\.js]\n"
+ '"Content Type"="application/javascript"',
+ id="debug_toolbar.W007",
+ )
+ ]
+ return []
diff --git a/debug_toolbar/forms.py b/debug_toolbar/forms.py
index 1263c3aff..61444b43c 100644
--- a/debug_toolbar/forms.py
+++ b/debug_toolbar/forms.py
@@ -38,8 +38,8 @@ def clean_signed(self):
signing.Signer(salt=self.salt).unsign(self.cleaned_data["signed"])
)
return verified
- except signing.BadSignature:
- raise ValidationError("Bad signature")
+ except signing.BadSignature as exc:
+ raise ValidationError("Bad signature") from exc
def verified_data(self):
return self.is_valid() and self.cleaned_data["signed"]
diff --git a/debug_toolbar/panels/__init__.py b/debug_toolbar/panels/__init__.py
index 6d9491b1f..57f385a5e 100644
--- a/debug_toolbar/panels/__init__.py
+++ b/debug_toolbar/panels/__init__.py
@@ -124,7 +124,6 @@ def ready(cls):
be done unconditionally for the panel regardless of whether it is
enabled for a particular request. It should be idempotent.
"""
- pass
# URLs for panel-specific views
diff --git a/debug_toolbar/panels/cache.py b/debug_toolbar/panels/cache.py
index 31ce70988..4c7bf5af7 100644
--- a/debug_toolbar/panels/cache.py
+++ b/debug_toolbar/panels/cache.py
@@ -117,10 +117,7 @@ def _store_call_info(
else:
self.hits += 1
elif name == "get_many":
- if "keys" in kwargs:
- keys = kwargs["keys"]
- else:
- keys = args[0]
+ keys = kwargs["keys"] if "keys" in kwargs else args[0]
self.hits += len(return_value)
self.misses += len(keys) - len(return_value)
time_taken *= 1000
diff --git a/debug_toolbar/panels/headers.py b/debug_toolbar/panels/headers.py
index ed20d6178..e1ea6da1e 100644
--- a/debug_toolbar/panels/headers.py
+++ b/debug_toolbar/panels/headers.py
@@ -33,7 +33,7 @@ class HeadersPanel(Panel):
template = "debug_toolbar/panels/headers.html"
def process_request(self, request):
- wsgi_env = list(sorted(request.META.items()))
+ wsgi_env = sorted(request.META.items())
self.request_headers = {
unmangle(k): v for (k, v) in wsgi_env if is_http_header(k)
}
diff --git a/debug_toolbar/panels/history/panel.py b/debug_toolbar/panels/history/panel.py
index 8bd0e8f65..508a60577 100644
--- a/debug_toolbar/panels/history/panel.py
+++ b/debug_toolbar/panels/history/panel.py
@@ -1,3 +1,4 @@
+import contextlib
import json
from django.http.request import RawPostDataException
@@ -22,7 +23,7 @@ class HistoryPanel(Panel):
def get_headers(self, request):
headers = super().get_headers(request)
observe_request = self.toolbar.get_observe_request()
- store_id = getattr(self.toolbar, "store_id")
+ store_id = self.toolbar.store_id
if store_id and observe_request(request):
headers["djdt-store-id"] = store_id
return headers
@@ -62,10 +63,9 @@ def generate_stats(self, request, response):
and request.body
and request.headers.get("content-type") == "application/json"
):
- try:
+ with contextlib.suppress(ValueError):
data = json.loads(request.body)
- except ValueError:
- pass
+
except RawPostDataException:
# It is not guaranteed that we may read the request data (again).
data = None
diff --git a/debug_toolbar/panels/profiling.py b/debug_toolbar/panels/profiling.py
index ca32b98c2..9d10229ad 100644
--- a/debug_toolbar/panels/profiling.py
+++ b/debug_toolbar/panels/profiling.py
@@ -13,7 +13,7 @@
class FunctionCall:
def __init__(
- self, statobj, func, depth=0, stats=None, id=0, parent_ids=[], hsv=(0, 0.5, 1)
+ self, statobj, func, depth=0, stats=None, id=0, parent_ids=None, hsv=(0, 0.5, 1)
):
self.statobj = statobj
self.func = func
@@ -23,7 +23,7 @@ def __init__(
self.stats = statobj.stats[func][:4]
self.depth = depth
self.id = id
- self.parent_ids = parent_ids
+ self.parent_ids = parent_ids or []
self.hsv = hsv
def parent_classes(self):
@@ -88,10 +88,7 @@ def subfuncs(self):
for func, stats in self.statobj.all_callees[self.func].items():
i += 1
h1 = h + (i / count) / (self.depth + 1)
- if stats[3] == 0:
- s1 = 0
- else:
- s1 = s * (stats[3] / self.stats[3])
+ s1 = 0 if stats[3] == 0 else s * (stats[3] / self.stats[3])
yield FunctionCall(
self.statobj,
func,
diff --git a/debug_toolbar/panels/redirects.py b/debug_toolbar/panels/redirects.py
index f6a00b574..195d0cf11 100644
--- a/debug_toolbar/panels/redirects.py
+++ b/debug_toolbar/panels/redirects.py
@@ -18,9 +18,7 @@ def process_request(self, request):
if 300 <= response.status_code < 400:
redirect_to = response.get("Location")
if redirect_to:
- status_line = "{} {}".format(
- response.status_code, response.reason_phrase
- )
+ status_line = f"{response.status_code} {response.reason_phrase}"
cookies = response.cookies
context = {"redirect_to": redirect_to, "status_line": status_line}
# Using SimpleTemplateResponse avoids running global context processors.
diff --git a/debug_toolbar/panels/request.py b/debug_toolbar/panels/request.py
index bfb485ae7..a936eba6b 100644
--- a/debug_toolbar/panels/request.py
+++ b/debug_toolbar/panels/request.py
@@ -64,7 +64,5 @@ def generate_stats(self, request, response):
(k, request.session.get(k)) for k in sorted(request.session.keys())
]
except TypeError:
- session_list = [
- (k, request.session.get(k)) for k in request.session.keys()
- ]
+ session_list = [(k, request.session.get(k)) for k in request.session]
self.record_stats({"session": {"list": session_list}})
diff --git a/debug_toolbar/panels/sql/forms.py b/debug_toolbar/panels/sql/forms.py
index 8d2709777..0515c5c8e 100644
--- a/debug_toolbar/panels/sql/forms.py
+++ b/debug_toolbar/panels/sql/forms.py
@@ -37,8 +37,8 @@ def clean_params(self):
try:
return json.loads(value)
- except ValueError:
- raise ValidationError("Is not valid JSON")
+ except ValueError as exc:
+ raise ValidationError("Is not valid JSON") from exc
def clean_alias(self):
value = self.cleaned_data["alias"]
diff --git a/debug_toolbar/panels/sql/panel.py b/debug_toolbar/panels/sql/panel.py
index c8576e16f..58c1c2738 100644
--- a/debug_toolbar/panels/sql/panel.py
+++ b/debug_toolbar/panels/sql/panel.py
@@ -6,6 +6,7 @@
from django.urls import path
from django.utils.translation import gettext_lazy as _, ngettext
+from debug_toolbar import settings as dt_settings
from debug_toolbar.forms import SignedDataForm
from debug_toolbar.panels import Panel
from debug_toolbar.panels.sql import views
@@ -89,7 +90,7 @@ def _duplicate_query_key(query):
def _process_query_groups(query_groups, databases, colors, name):
counts = defaultdict(int)
- for (alias, key), query_group in query_groups.items():
+ for (alias, _key), query_group in query_groups.items():
count = len(query_group)
# Queries are similar / duplicates only if there are at least 2 of them.
if count > 1:
@@ -110,9 +111,7 @@ class SQLPanel(Panel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self._offset = {k: len(connections[k].queries) for k in connections}
self._sql_time = 0
- self._num_queries = 0
self._queries = []
self._databases = {}
# synthetic transaction IDs, keyed by DB alias
@@ -151,7 +150,6 @@ def record(self, **kwargs):
self._databases[alias]["time_spent"] += kwargs["duration"]
self._databases[alias]["num_queries"] += 1
self._sql_time += kwargs["duration"]
- self._num_queries += 1
# Implement the Panel API
@@ -159,12 +157,13 @@ def record(self, **kwargs):
@property
def nav_subtitle(self):
+ query_count = len(self._queries)
return ngettext(
"%(query_count)d query in %(sql_time).2fms",
"%(query_count)d queries in %(sql_time).2fms",
- self._num_queries,
+ query_count,
) % {
- "query_count": self._num_queries,
+ "query_count": query_count,
"sql_time": self._sql_time,
}
@@ -204,6 +203,8 @@ def generate_stats(self, request, response):
duplicate_query_groups = defaultdict(list)
if self._queries:
+ sql_warning_threshold = dt_settings.get_config()["SQL_WARNING_THRESHOLD"]
+
width_ratio_tally = 0
factor = int(256.0 / (len(self._databases) * 2.5))
for n, db in enumerate(self._databases.values()):
@@ -261,6 +262,12 @@ def generate_stats(self, request, response):
if query["sql"]:
query["sql"] = reformat_sql(query["sql"], with_toggle=True)
+
+ query["is_slow"] = query["duration"] > sql_warning_threshold
+ query["is_select"] = (
+ query["raw_sql"].lower().lstrip().startswith("select")
+ )
+
query["rgb_color"] = self._databases[alias]["rgb_color"]
try:
query["width_ratio"] = (query["duration"] / self._sql_time) * 100
diff --git a/debug_toolbar/panels/sql/tracking.py b/debug_toolbar/panels/sql/tracking.py
index a85ac51ad..0c53dc2c5 100644
--- a/debug_toolbar/panels/sql/tracking.py
+++ b/debug_toolbar/panels/sql/tracking.py
@@ -1,13 +1,12 @@
+import contextlib
import contextvars
import datetime
import json
from time import perf_counter
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
from debug_toolbar.utils import get_stack_trace, get_template_info
try:
@@ -34,8 +33,14 @@ class SQLQueryTriggered(Exception):
def wrap_cursor(connection):
- # If running a Django SimpleTestCase, which isn't allowed to access the database,
- # don't perform any monkey patching.
+ # When running a SimpleTestCase, Django monkey patches some DatabaseWrapper
+ # methods, including .cursor() and .chunked_cursor(), to raise an exception
+ # if the test code tries to access the database, and then undoes the monkey
+ # patching when the test case is finished. If we monkey patch those methods
+ # also, Django's process of undoing those monkey patches will fail. To
+ # avoid this failure, and because database access is not allowed during a
+ # SimpleTestCase anyway, skip applying our instrumentation monkey patches if
+ # we detect that Django has already monkey patched DatabaseWrapper.cursor().
if isinstance(connection.cursor, django.test.testcases._DatabaseFailure):
return
if not hasattr(connection, "_djdt_cursor"):
@@ -54,37 +59,42 @@ def cursor(*args, **kwargs):
cursor = connection._djdt_cursor(*args, **kwargs)
if logger is None:
return cursor
- if allow_sql.get():
- wrapper = NormalCursorWrapper
- else:
- wrapper = ExceptionCursorWrapper
- return wrapper(cursor.cursor, connection, logger)
+ mixin = NormalCursorMixin if allow_sql.get() else ExceptionCursorMixin
+ return patch_cursor_wrapper_with_mixin(cursor.__class__, mixin)(
+ 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, DjDTCursorWrapper):
- if allow_sql.get():
- wrapper = NormalCursorWrapper
- else:
- wrapper = ExceptionCursorWrapper
- return wrapper(cursor.cursor, connection, logger)
+ if logger is not None and not isinstance(cursor, DjDTCursorWrapperMixin):
+ mixin = NormalCursorMixin if allow_sql.get() else ExceptionCursorMixin
+ return patch_cursor_wrapper_with_mixin(cursor.__class__, mixin)(
+ cursor.cursor, connection, logger
+ )
return cursor
connection.cursor = cursor
connection.chunked_cursor = chunked_cursor
-class DjDTCursorWrapper(CursorWrapper):
+def patch_cursor_wrapper_with_mixin(base_wrapper, mixin):
+ class DjDTCursorWrapper(mixin, base_wrapper):
+ pass
+
+ return DjDTCursorWrapper
+
+
+class DjDTCursorWrapperMixin:
def __init__(self, cursor, db, logger):
super().__init__(cursor, db)
# logger must implement a ``record`` method
self.logger = logger
-class ExceptionCursorWrapper(DjDTCursorWrapper):
+class ExceptionCursorMixin(DjDTCursorWrapperMixin):
"""
Wraps a cursor and raises an exception on any operation.
Used in Templates panel.
@@ -94,7 +104,7 @@ def __getattr__(self, attr):
raise SQLQueryTriggered()
-class NormalCursorWrapper(DjDTCursorWrapper):
+class NormalCursorMixin(DjDTCursorWrapperMixin):
"""
Wraps a cursor and logs queries.
"""
@@ -169,20 +179,22 @@ def _record(self, method, sql, params):
stop_time = perf_counter()
duration = (stop_time - start_time) * 1000
_params = ""
- try:
+ with contextlib.suppress(TypeError):
+ # object JSON serializable?
_params = json.dumps(self._decode(params))
- except TypeError:
- pass # object not JSON serializable
template_info = get_template_info()
# Sql might be an object (such as psycopg Composed).
# For logging purposes, make sure it's str.
if vendor == "postgresql" and not isinstance(sql, str):
- sql = sql.as_string(conn)
+ if isinstance(sql, bytes):
+ sql = sql.decode("utf-8")
+ else:
+ sql = sql.as_string(conn)
else:
sql = str(sql)
- params = {
+ kwargs = {
"vendor": vendor,
"alias": alias,
"sql": self._last_executed_query(sql, params),
@@ -191,12 +203,6 @@ def _record(self, method, sql, params):
"params": _params,
"raw_params": 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_select": sql.lower().strip().startswith("select"),
"template_info": template_info,
}
@@ -228,7 +234,7 @@ def _record(self, method, sql, params):
else:
trans_id = None
- params.update(
+ kwargs.update(
{
"trans_id": trans_id,
"trans_status": conn.info.transaction_status,
@@ -237,7 +243,7 @@ def _record(self, method, sql, params):
)
# We keep `sql` to maintain backwards compatibility
- self.logger.record(**params)
+ self.logger.record(**kwargs)
def callproc(self, procname, params=None):
return self._record(super().callproc, procname, params)
diff --git a/debug_toolbar/panels/sql/utils.py b/debug_toolbar/panels/sql/utils.py
index efd7c1637..cb4eda348 100644
--- a/debug_toolbar/panels/sql/utils.py
+++ b/debug_toolbar/panels/sql/utils.py
@@ -86,7 +86,7 @@ def process(stmt):
return "".join(escaped_value(token) for token in stmt.flatten())
-def reformat_sql(sql, with_toggle=False):
+def reformat_sql(sql, *, with_toggle=False):
formatted = parse_sql(sql)
if not with_toggle:
return formatted
@@ -132,7 +132,7 @@ def contrasting_color_generator():
"""
def rgb_to_hex(rgb):
- return "#%02x%02x%02x" % tuple(rgb)
+ return "#{:02x}{:02x}{:02x}".format(*tuple(rgb))
triples = [
(1, 0, 0),
diff --git a/debug_toolbar/panels/staticfiles.py b/debug_toolbar/panels/staticfiles.py
index bee336249..5f9efb5c3 100644
--- a/debug_toolbar/panels/staticfiles.py
+++ b/debug_toolbar/panels/staticfiles.py
@@ -1,3 +1,5 @@
+import contextlib
+from contextvars import ContextVar
from os.path import join, normpath
from django.conf import settings
@@ -7,12 +9,6 @@
from django.utils.translation import gettext_lazy as _, ngettext
from debug_toolbar import panels
-from debug_toolbar.utils import ThreadCollector
-
-try:
- import threading
-except ImportError:
- threading = None
class StaticFile:
@@ -33,15 +29,8 @@ def url(self):
return storage.staticfiles_storage.url(self.path)
-class FileCollector(ThreadCollector):
- def collect(self, path, thread=None):
- # handle the case of {% static "admin/" %}
- if path.endswith("/"):
- return
- super().collect(StaticFile(path), thread)
-
-
-collector = FileCollector()
+# This will collect the StaticFile instances across threads.
+used_static_files = ContextVar("djdt_static_used_static_files")
class DebugConfiguredStorage(LazyObject):
@@ -65,15 +54,16 @@ def _setup(self):
configured_storage_cls = get_storage_class(settings.STATICFILES_STORAGE)
class DebugStaticFilesStorage(configured_storage_cls):
- def __init__(self, collector, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.collector = collector
-
def url(self, path):
- self.collector.collect(path)
+ with contextlib.suppress(LookupError):
+ # For LookupError:
+ # The ContextVar wasn't set yet. Since the toolbar wasn't properly
+ # configured to handle this request, we don't need to capture
+ # the static file.
+ used_static_files.get().append(StaticFile(path))
return super().url(path)
- self._wrapped = DebugStaticFilesStorage(collector)
+ self._wrapped = DebugStaticFilesStorage()
_original_storage = storage.staticfiles_storage
@@ -97,7 +87,7 @@ def title(self):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.num_found = 0
- self._paths = {}
+ self.used_paths = []
def enable_instrumentation(self):
storage.staticfiles_storage = DebugConfiguredStorage()
@@ -120,18 +110,22 @@ def nav_subtitle(self):
) % {"num_used": num_used}
def process_request(self, request):
- collector.clear_collection()
- return super().process_request(request)
+ reset_token = used_static_files.set([])
+ response = super().process_request(request)
+ # Make a copy of the used paths so that when the
+ # ContextVar is reset, our panel still has the data.
+ self.used_paths = used_static_files.get().copy()
+ # Reset the ContextVar to be empty again, removing the reference
+ # to the list of used files.
+ used_static_files.reset(reset_token)
+ return response
def generate_stats(self, request, response):
- used_paths = collector.get_collection()
- self._paths[threading.current_thread()] = used_paths
-
self.record_stats(
{
"num_found": self.num_found,
- "num_used": len(used_paths),
- "staticfiles": used_paths,
+ "num_used": len(self.used_paths),
+ "staticfiles": self.used_paths,
"staticfiles_apps": self.get_staticfiles_apps(),
"staticfiles_dirs": self.get_staticfiles_dirs(),
"staticfiles_finders": self.get_staticfiles_finders(),
diff --git a/debug_toolbar/panels/templates/panel.py b/debug_toolbar/panels/templates/panel.py
index 35d5b5191..72565f016 100644
--- a/debug_toolbar/panels/templates/panel.py
+++ b/debug_toolbar/panels/templates/panel.py
@@ -117,7 +117,7 @@ def _store_template_info(self, sender, **kwargs):
value.model._meta.label,
)
else:
- token = allow_sql.set(False)
+ token = allow_sql.set(False) # noqa: FBT003
try:
saferepr(value) # this MAY trigger a db query
except SQLQueryTriggered:
@@ -192,9 +192,7 @@ def generate_stats(self, request, response):
template = self.templates[0]["template"]
# django templates have the 'engine' attribute, while jinja
# templates use 'backend'
- engine_backend = getattr(template, "engine", None) or getattr(
- template, "backend"
- )
+ engine_backend = getattr(template, "engine", None) or template.backend
template_dirs = engine_backend.dirs
else:
context_processors = None
diff --git a/debug_toolbar/settings.py b/debug_toolbar/settings.py
index bf534a7da..eb6b59209 100644
--- a/debug_toolbar/settings.py
+++ b/debug_toolbar/settings.py
@@ -83,6 +83,7 @@ def get_panels():
warnings.warn(
f"Please remove {logging_panel} from your DEBUG_TOOLBAR_PANELS setting.",
DeprecationWarning,
+ stacklevel=1,
)
return PANELS
diff --git a/debug_toolbar/static/debug_toolbar/css/toolbar.css b/debug_toolbar/static/debug_toolbar/css/toolbar.css
index 4adb0abb5..a35286a1f 100644
--- a/debug_toolbar/static/debug_toolbar/css/toolbar.css
+++ b/debug_toolbar/static/debug_toolbar/css/toolbar.css
@@ -1,10 +1,9 @@
/* 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-primary: "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",
diff --git a/debug_toolbar/static/debug_toolbar/js/toolbar.js b/debug_toolbar/static/debug_toolbar/js/toolbar.js
index ef2e617f9..9546ef27e 100644
--- a/debug_toolbar/static/debug_toolbar/js/toolbar.js
+++ b/debug_toolbar/static/debug_toolbar/js/toolbar.js
@@ -341,9 +341,9 @@ const djdt = {
options.path ? "; path=" + options.path : "",
options.domain ? "; domain=" + options.domain : "",
options.secure ? "; secure" : "",
- "sameSite" in options
- ? "; sameSite=" + options.samesite
- : "; sameSite=Lax",
+ "samesite" in options
+ ? "; samesite=" + options.samesite
+ : "; samesite=lax",
].join("");
return value;
diff --git a/debug_toolbar/toolbar.py b/debug_toolbar/toolbar.py
index 31010f47f..11f8a1daa 100644
--- a/debug_toolbar/toolbar.py
+++ b/debug_toolbar/toolbar.py
@@ -86,7 +86,7 @@ def render_toolbar(self):
"The debug toolbar requires the staticfiles contrib app. "
"Add 'django.contrib.staticfiles' to INSTALLED_APPS and "
"define STATIC_URL in your settings."
- )
+ ) from None
else:
raise
diff --git a/debug_toolbar/utils.py b/debug_toolbar/utils.py
index 4a7e9b2c3..7234f1f77 100644
--- a/debug_toolbar/utils.py
+++ b/debug_toolbar/utils.py
@@ -14,12 +14,6 @@
from debug_toolbar import _stubs as stubs, settings as dt_settings
-try:
- import threading
-except ImportError:
- threading = None
-
-
_local_data = Local()
@@ -168,10 +162,7 @@ def get_template_source_from_exception_info(
def get_name_from_obj(obj: Any) -> str:
- if hasattr(obj, "__name__"):
- name = obj.__name__
- else:
- name = obj.__class__.__name__
+ name = obj.__name__ if hasattr(obj, "__name__") else obj.__class__.__name__
if hasattr(obj, "__module__"):
module = obj.__module__
@@ -360,33 +351,3 @@ def get_stack_trace(*, skip=0):
def clear_stack_trace_caches():
if hasattr(_local_data, "stack_trace_recorder"):
del _local_data.stack_trace_recorder
-
-
-class ThreadCollector:
- def __init__(self):
- if threading is None:
- raise NotImplementedError(
- "threading module is not available, "
- "this panel cannot be used without it"
- )
- self.collections = {} # a dictionary that maps threads to collections
-
- def get_collection(self, thread=None):
- """
- Returns a list of collected items for the provided thread, of if none
- is provided, returns a list for the current thread.
- """
- if thread is None:
- thread = threading.current_thread()
- if thread not in self.collections:
- self.collections[thread] = []
- return self.collections[thread]
-
- def clear_collection(self, thread=None):
- if thread is None:
- thread = threading.current_thread()
- if thread in self.collections:
- del self.collections[thread]
-
- def collect(self, item, thread=None):
- self.get_collection(thread).append(item)
diff --git a/docs/changes.rst b/docs/changes.rst
index ad6607e34..89f5bdc7e 100644
--- a/docs/changes.rst
+++ b/docs/changes.rst
@@ -4,6 +4,23 @@ Change log
Pending
-------
+4.2.0 (2023-08-10)
+------------------
+
+* Adjusted app directories system check to allow for nested template loaders.
+* Switched from flake8, isort and pyupgrade to `ruff
+