diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index bb1cd93f0..3d76ba424 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -14,6 +14,16 @@ repos:
rev: 0.10.1
hooks:
- id: doc8
+- repo: https://github.com/asottile/pyupgrade
+ rev: v2.29.1
+ hooks:
+ - id: pyupgrade
+ args: [--py36-plus]
+- repo: https://github.com/adamchainz/django-upgrade
+ rev: 1.4.0
+ hooks:
+ - id: django-upgrade
+ args: [--target-version, "3.2"]
- repo: https://github.com/pycqa/isort
rev: 5.10.1
hooks:
diff --git a/README.rst b/README.rst
index c147abf4f..725bb4fe8 100644
--- a/README.rst
+++ b/README.rst
@@ -45,7 +45,7 @@ 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 3.2.4. It works on
-Django ≥ 2.2.
+Django ≥ 3.2.
Documentation, including installation and configuration instructions, is
available at https://django-debug-toolbar.readthedocs.io/.
diff --git a/debug_toolbar/__init__.py b/debug_toolbar/__init__.py
index 401de16c1..7dbb53bd5 100644
--- a/debug_toolbar/__init__.py
+++ b/debug_toolbar/__init__.py
@@ -1,5 +1,3 @@
-import django
-
from debug_toolbar.urls import app_name
__all__ = ["VERSION"]
@@ -12,6 +10,3 @@
# Code that discovers files or modules in INSTALLED_APPS imports this module.
urls = "debug_toolbar.urls", app_name
-
-if django.VERSION < (3, 2):
- default_app_config = "debug_toolbar.apps.DebugToolbarConfig"
diff --git a/debug_toolbar/management/commands/debugsqlshell.py b/debug_toolbar/management/commands/debugsqlshell.py
index ba0f32463..93514d121 100644
--- a/debug_toolbar/management/commands/debugsqlshell.py
+++ b/debug_toolbar/management/commands/debugsqlshell.py
@@ -1,11 +1,10 @@
from time import time
-import django
import sqlparse
from django.core.management.commands.shell import Command
from django.db import connection
-if connection.vendor == "postgresql" and django.VERSION >= (3, 0, 0):
+if connection.vendor == "postgresql":
from django.db.backends.postgresql import base as base_module
else:
from django.db.backends import utils as base_module
@@ -28,7 +27,7 @@ def execute(self, sql, params=()):
end_time = time()
duration = (end_time - start_time) * 1000
formatted_sql = sqlparse.format(raw_sql, reindent=True)
- print("{} [{:.2f}ms]".format(formatted_sql, duration))
+ print(f"{formatted_sql} [{duration:.2f}ms]")
base_module.CursorDebugWrapper = PrintQueryWrapper
diff --git a/debug_toolbar/panels/cache.py b/debug_toolbar/panels/cache.py
index 9f998b18f..617c6e1ef 100644
--- a/debug_toolbar/panels/cache.py
+++ b/debug_toolbar/panels/cache.py
@@ -8,15 +8,9 @@
except ImportError:
ConnectionProxy = None
-import django
from django.conf import settings
from django.core import cache
-from django.core.cache import (
- DEFAULT_CACHE_ALIAS,
- CacheHandler,
- cache as original_cache,
- caches as original_caches,
-)
+from django.core.cache import DEFAULT_CACHE_ALIAS, CacheHandler
from django.core.cache.backends.base import BaseCache
from django.dispatch import Signal
from django.middleware import cache as middleware_cache
@@ -69,7 +63,7 @@ def __init__(self, cache):
self.cache = cache
def __repr__(self):
- return str("") % repr(self.cache)
+ return "" % repr(self.cache)
def _get_func_info(self):
frame = sys._getframe(3)
@@ -141,26 +135,17 @@ def decr_version(self, *args, **kwargs):
return self.cache.decr_version(*args, **kwargs)
-if django.VERSION < (3, 2):
+class CacheHandlerPatch(CacheHandler):
+ def __init__(self, settings=None):
+ self._djdt_wrap = True
+ super().__init__(settings=settings)
- class CacheHandlerPatch(CacheHandler):
- def __getitem__(self, alias):
- actual_cache = super().__getitem__(alias)
+ def create_connection(self, alias):
+ actual_cache = super().create_connection(alias)
+ if self._djdt_wrap:
return CacheStatTracker(actual_cache)
-
-else:
-
- class CacheHandlerPatch(CacheHandler):
- def __init__(self, settings=None):
- self._djdt_wrap = True
- super().__init__(settings=settings)
-
- def create_connection(self, alias):
- actual_cache = super().create_connection(alias)
- if self._djdt_wrap:
- return CacheStatTracker(actual_cache)
- else:
- return actual_cache
+ else:
+ return actual_cache
middleware_cache.caches = CacheHandlerPatch()
@@ -268,40 +253,26 @@ def title(self):
)
def enable_instrumentation(self):
- if django.VERSION < (3, 2):
- if isinstance(middleware_cache.caches, CacheHandlerPatch):
- cache.caches = middleware_cache.caches
- else:
- cache.caches = CacheHandlerPatch()
- else:
- for alias in cache.caches:
- if not isinstance(cache.caches[alias], CacheStatTracker):
- cache.caches[alias] = CacheStatTracker(cache.caches[alias])
+ for alias in cache.caches:
+ if not isinstance(cache.caches[alias], CacheStatTracker):
+ cache.caches[alias] = CacheStatTracker(cache.caches[alias])
- if not isinstance(middleware_cache.caches, CacheHandlerPatch):
- middleware_cache.caches = cache.caches
+ if not isinstance(middleware_cache.caches, CacheHandlerPatch):
+ middleware_cache.caches = cache.caches
# Wrap the patched cache inside Django's ConnectionProxy
if ConnectionProxy:
cache.cache = ConnectionProxy(cache.caches, DEFAULT_CACHE_ALIAS)
def disable_instrumentation(self):
- if django.VERSION < (3, 2):
- cache.caches = original_caches
- cache.cache = original_cache
- # While it can be restored to the original, any views that were
- # wrapped with the cache_page decorator will continue to use a
- # monkey patched cache.
- middleware_cache.caches = original_caches
- else:
- for alias in cache.caches:
- if isinstance(cache.caches[alias], CacheStatTracker):
- cache.caches[alias] = cache.caches[alias].cache
- if ConnectionProxy:
- cache.cache = ConnectionProxy(cache.caches, DEFAULT_CACHE_ALIAS)
- # While it can be restored to the original, any views that were
- # wrapped with the cache_page decorator will continue to use a
- # monkey patched cache.
+ for alias in cache.caches:
+ if isinstance(cache.caches[alias], CacheStatTracker):
+ cache.caches[alias] = cache.caches[alias].cache
+ if ConnectionProxy:
+ cache.cache = ConnectionProxy(cache.caches, DEFAULT_CACHE_ALIAS)
+ # While it can be restored to the original, any views that were
+ # wrapped with the cache_page decorator will continue to use a
+ # monkey patched cache.
def generate_stats(self, request, response):
self.record_stats(
diff --git a/debug_toolbar/panels/profiling.py b/debug_toolbar/panels/profiling.py
index 3acd5fabe..5fd5b3c84 100644
--- a/debug_toolbar/panels/profiling.py
+++ b/debug_toolbar/panels/profiling.py
@@ -30,7 +30,7 @@ def parent_classes(self):
def background(self):
r, g, b = hsv_to_rgb(*self.hsv)
- return "rgb({:f}%,{:f}%,{:f}%)".format(r * 100, g * 100, b * 100)
+ return f"rgb({r * 100:f}%,{g * 100:f}%,{b * 100:f}%)"
def func_std_string(self): # match what old profile produced
func_name = self.func
diff --git a/debug_toolbar/panels/settings.py b/debug_toolbar/panels/settings.py
index 85a92788e..37bba8727 100644
--- a/debug_toolbar/panels/settings.py
+++ b/debug_toolbar/panels/settings.py
@@ -1,17 +1,12 @@
from collections import OrderedDict
-import django
from django.conf import settings
from django.utils.translation import gettext_lazy as _
+from django.views.debug import get_default_exception_reporter_filter
from debug_toolbar.panels import Panel
-if django.VERSION >= (3, 1):
- from django.views.debug import get_default_exception_reporter_filter
-
- get_safe_settings = get_default_exception_reporter_filter().get_safe_settings
-else:
- from django.views.debug import get_safe_settings
+get_safe_settings = get_default_exception_reporter_filter().get_safe_settings
class SettingsPanel(Panel):
diff --git a/debug_toolbar/panels/signals.py b/debug_toolbar/panels/signals.py
index 3535bd143..5e81fa497 100644
--- a/debug_toolbar/panels/signals.py
+++ b/debug_toolbar/panels/signals.py
@@ -97,7 +97,7 @@ def generate_stats(self, request, response):
receiver_class_name = getattr(
receiver.__self__, "__class__", type
).__name__
- text = "{}.{}".format(receiver_class_name, receiver_name)
+ text = f"{receiver_class_name}.{receiver_name}"
else:
text = receiver_name
receivers.append(text)
diff --git a/debug_toolbar/panels/sql/tracking.py b/debug_toolbar/panels/sql/tracking.py
index 2ed691344..c6e4148bd 100644
--- a/debug_toolbar/panels/sql/tracking.py
+++ b/debug_toolbar/panels/sql/tracking.py
@@ -17,8 +17,6 @@
class SQLQueryTriggered(Exception):
"""Thrown when template panel triggers a query"""
- pass
-
class ThreadLocalState(local):
def __init__(self):
diff --git a/debug_toolbar/panels/sql/utils.py b/debug_toolbar/panels/sql/utils.py
index 16b7a3ea4..7b04f0346 100644
--- a/debug_toolbar/panels/sql/utils.py
+++ b/debug_toolbar/panels/sql/utils.py
@@ -26,8 +26,8 @@ def reformat_sql(sql, with_toggle=False):
if not with_toggle:
return formatted
simple = simplify(parse_sql(sql, aligned_indent=False))
- uncollapsed = '{}'.format(simple)
- collapsed = '{}'.format(formatted)
+ uncollapsed = f'{simple}'
+ collapsed = f'{formatted}'
return collapsed + uncollapsed
diff --git a/debug_toolbar/panels/sql/views.py b/debug_toolbar/panels/sql/views.py
index 3b6516b49..49ffee515 100644
--- a/debug_toolbar/panels/sql/views.py
+++ b/debug_toolbar/panels/sql/views.py
@@ -49,11 +49,11 @@ def sql_explain(request, verified_data):
# SQLite's EXPLAIN dumps the low-level opcodes generated for a query;
# EXPLAIN QUERY PLAN dumps a more human-readable summary
# See https://www.sqlite.org/lang_explain.html for details
- cursor.execute("EXPLAIN QUERY PLAN {}".format(sql), params)
+ cursor.execute(f"EXPLAIN QUERY PLAN {sql}", params)
elif vendor == "postgresql":
- cursor.execute("EXPLAIN ANALYZE {}".format(sql), params)
+ cursor.execute(f"EXPLAIN ANALYZE {sql}", params)
else:
- cursor.execute("EXPLAIN {}".format(sql), params)
+ cursor.execute(f"EXPLAIN {sql}", params)
headers = [d[0] for d in cursor.description]
result = cursor.fetchall()
diff --git a/debug_toolbar/panels/templates/panel.py b/debug_toolbar/panels/templates/panel.py
index 8ff06e27d..66f6c60fb 100644
--- a/debug_toolbar/panels/templates/panel.py
+++ b/debug_toolbar/panels/templates/panel.py
@@ -42,7 +42,7 @@ def _request_context_bind_template(self, template):
self.context_processors = OrderedDict()
updates = {}
for processor in processors:
- name = "{}.{}".format(processor.__module__, processor.__name__)
+ name = f"{processor.__module__}.{processor.__name__}"
context = processor(self.request)
self.context_processors[name] = context
updates.update(context)
diff --git a/debug_toolbar/panels/templates/views.py b/debug_toolbar/panels/templates/views.py
index 9e74ec6d5..8d6d634d3 100644
--- a/debug_toolbar/panels/templates/views.py
+++ b/debug_toolbar/panels/templates/views.py
@@ -44,7 +44,7 @@ def template_source(request):
except TemplateDoesNotExist:
pass
else:
- source = "Template Does Not Exist: {}".format(template_origin_name)
+ source = f"Template Does Not Exist: {template_origin_name}"
try:
from pygments import highlight
diff --git a/debug_toolbar/utils.py b/debug_toolbar/utils.py
index f8fb68538..c662ab113 100644
--- a/debug_toolbar/utils.py
+++ b/debug_toolbar/utils.py
@@ -27,7 +27,7 @@ def get_module_path(module_name):
try:
module = import_module(module_name)
except ImportError as e:
- raise ImproperlyConfigured("Error importing HIDE_IN_STACKTRACES: {}".format(e))
+ raise ImproperlyConfigured(f"Error importing HIDE_IN_STACKTRACES: {e}")
else:
source_path = inspect.getsourcefile(module)
if source_path.endswith("__init__.py"):
@@ -157,7 +157,7 @@ def get_name_from_obj(obj):
if hasattr(obj, "__module__"):
module = obj.__module__
- name = "{}.{}".format(module, name)
+ name = f"{module}.{name}"
return name
diff --git a/tests/commands/test_debugsqlshell.py b/tests/commands/test_debugsqlshell.py
index 9520d0dd8..9939c5ca9 100644
--- a/tests/commands/test_debugsqlshell.py
+++ b/tests/commands/test_debugsqlshell.py
@@ -1,14 +1,13 @@
import io
import sys
-import django
from django.contrib.auth.models import User
from django.core import management
from django.db import connection
from django.test import TestCase
from django.test.utils import override_settings
-if connection.vendor == "postgresql" and django.VERSION >= (3, 0, 0):
+if connection.vendor == "postgresql":
from django.db.backends.postgresql import base as base_module
else:
from django.db.backends import utils as base_module
diff --git a/tests/models.py b/tests/models.py
index 0c070a7f2..70d9d5d18 100644
--- a/tests/models.py
+++ b/tests/models.py
@@ -11,19 +11,8 @@ class Binary(models.Model):
field = models.BinaryField()
-try:
- from django.db.models import JSONField
-except ImportError: # Django<3.1
- try:
- from django.contrib.postgres.fields import JSONField
- except ImportError: # psycopg2 not installed
- JSONField = None
-
-
-if JSONField:
-
- class PostgresJSON(models.Model):
- field = JSONField()
+class JSON(models.Model):
+ field = models.JSONField()
if settings.USE_GIS:
diff --git a/tests/panels/test_sql.py b/tests/panels/test_sql.py
index 9358a2f70..874e6b515 100644
--- a/tests/panels/test_sql.py
+++ b/tests/panels/test_sql.py
@@ -15,16 +15,7 @@
from debug_toolbar import settings as dt_settings
from ..base import BaseTestCase
-
-try:
- from psycopg2._json import Json as PostgresJson
-except ImportError:
- PostgresJson = None
-
-if connection.vendor == "postgresql":
- from ..models import PostgresJSON as PostgresJSONModel
-else:
- PostgresJSONModel = None
+from ..models import JSON
class SQLPanelTestCase(BaseTestCase):
@@ -145,22 +136,19 @@ def test_param_conversion(self):
and not (django.VERSION >= (4, 1) and connection.vendor == "mysql")
):
self.assertEqual(
- tuple([q[1]["params"] for q in self.panel._queries]),
+ tuple(q[1]["params"] for q in self.panel._queries),
('["Foo"]', "[10, 1]", '["2017-12-22 16:07:01"]'),
)
else:
self.assertEqual(
- tuple([q[1]["params"] for q in self.panel._queries]),
+ tuple(q[1]["params"] for q in self.panel._queries),
('["Foo", true, false]', "[10, 1]", '["2017-12-22 16:07:01"]'),
)
- @unittest.skipUnless(
- connection.vendor == "postgresql", "Test valid only on PostgreSQL"
- )
def test_json_param_conversion(self):
self.assertEqual(len(self.panel._queries), 0)
- list(PostgresJSONModel.objects.filter(field__contains={"foo": "bar"}))
+ list(JSON.objects.filter(field__contains={"foo": "bar"}))
response = self.panel.process_request(self.request)
self.panel.generate_stats(self.request, response)
@@ -171,11 +159,6 @@ def test_json_param_conversion(self):
self.panel._queries[0][1]["params"],
'["{\\"foo\\": \\"bar\\"}"]',
)
- if django.VERSION < (3, 1):
- self.assertIsInstance(
- self.panel._queries[0][1]["raw_params"][0],
- PostgresJson,
- )
def test_binary_param_force_text(self):
self.assertEqual(len(self.panel._queries), 0)
@@ -244,7 +227,7 @@ def test_raw_query_param_conversion(self):
self.assertEqual(len(self.panel._queries), 2)
self.assertEqual(
- tuple([q[1]["params"] for q in self.panel._queries]),
+ tuple(q[1]["params"] for q in self.panel._queries),
(
'["Foo", true, false, "2017-12-22 16:07:01"]',
" ".join(
@@ -263,7 +246,7 @@ def test_insert_content(self):
Test that the panel only inserts content after generate_stats and
not the process_request.
"""
- list(User.objects.filter(username="café".encode("utf-8")))
+ list(User.objects.filter(username="café".encode()))
response = self.panel.process_request(self.request)
# ensure the panel does not have content yet.
self.assertNotIn("café", self.panel.content)
@@ -279,7 +262,7 @@ def test_insert_locals(self):
Test that the panel inserts locals() content.
"""
local_var = "" # noqa: F841
- list(User.objects.filter(username="café".encode("utf-8")))
+ list(User.objects.filter(username="café".encode()))
response = self.panel.process_request(self.request)
self.panel.generate_stats(self.request, response)
self.assertIn("local_var", self.panel.content)
@@ -293,7 +276,7 @@ def test_not_insert_locals(self):
"""
Test that the panel does not insert locals() content.
"""
- list(User.objects.filter(username="café".encode("utf-8")))
+ list(User.objects.filter(username="café".encode()))
response = self.panel.process_request(self.request)
self.panel.generate_stats(self.request, response)
self.assertNotIn("djdt-locals", self.panel.content)
diff --git a/tests/test_forms.py b/tests/test_forms.py
index 73d820fd8..3820a392f 100644
--- a/tests/test_forms.py
+++ b/tests/test_forms.py
@@ -1,18 +1,11 @@
from datetime import datetime
-import django
from django import forms
from django.test import TestCase
from debug_toolbar.forms import SignedDataForm
-# Django 3.1 uses sha256 by default.
-SIGNATURE = (
- "v02QBcJplEET6QXHNWejnRcmSENWlw6_RjxLTR7QG9g"
- if django.VERSION >= (3, 1)
- else "ukcAFUqYhUUnqT-LupnYoo-KvFg"
-)
-
+SIGNATURE = "v02QBcJplEET6QXHNWejnRcmSENWlw6_RjxLTR7QG9g"
DATA = {"value": "foo", "date": datetime(2020, 1, 1)}
SIGNED_DATA = f'{{"date": "2020-01-01 00:00:00", "value": "foo"}}:{SIGNATURE}'
diff --git a/tests/test_integration.py b/tests/test_integration.py
index ff6d32d9f..b76ebf875 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -2,7 +2,6 @@
import re
import unittest
-import django
import html5lib
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from django.core import signing
@@ -444,10 +443,7 @@ def test_auth_login_view_without_redirect(self):
self.assertEqual(response.status_code, 200)
# The key None (without quotes) exists in the list of template
# variables.
- if django.VERSION < (3, 0):
- self.assertIn("None: ''", response.json()["content"])
- else:
- self.assertIn("None: ''", response.json()["content"])
+ self.assertIn("None: ''", response.json()["content"])
@unittest.skipIf(webdriver is None, "selenium isn't installed")
diff --git a/tox.ini b/tox.ini
index 65fea4b36..054af6534 100644
--- a/tox.ini
+++ b/tox.ini
@@ -2,13 +2,12 @@
envlist =
docs
packaging
- py{36,37}-dj{22,32}-{sqlite,postgresql,postgis,mysql}
- py{38,39}-dj{22,32,40,main}-{sqlite,postgresql,postgis,mysql}
+ py{36,37}-dj{32}-{sqlite,postgresql,postgis,mysql}
+ py{38,39}-dj{32,40,main}-{sqlite,postgresql,postgis,mysql}
py{310}-dj{32,40,main}-{sqlite,postgresql,postgis,mysql}
[testenv]
deps =
- dj22: django~=2.2.17
dj32: django~=3.2.9
dj40: django~=4.0.0
sqlite: mock
@@ -42,25 +41,25 @@ whitelist_externals = make
pip_pre = True
commands = make coverage TEST_ARGS='{posargs:tests}'
-[testenv:py{36,37,38,39,310}-dj{22,32,40,main}-postgresql]
+[testenv:py{36,37,38,39,310}-dj{32,40,main}-postgresql]
setenv =
{[testenv]setenv}
DB_BACKEND = postgresql
DB_PORT = {env:DB_PORT:5432}
-[testenv:py{36,37,38,39,310}-dj{22,32,40,main}-postgis]
+[testenv:py{36,37,38,39,310}-dj{32,40,main}-postgis]
setenv =
{[testenv]setenv}
DB_BACKEND = postgis
DB_PORT = {env:DB_PORT:5432}
-[testenv:py{36,37,38,39,310}-dj{22,32,40,main}-mysql]
+[testenv:py{36,37,38,39,310}-dj{32,40,main}-mysql]
setenv =
{[testenv]setenv}
DB_BACKEND = mysql
DB_PORT = {env:DB_PORT:3306}
-[testenv:py{36,37,38,39,310}-dj{22,32,40,main}-sqlite]
+[testenv:py{36,37,38,39,310}-dj{32,40,main}-sqlite]
setenv =
{[testenv]setenv}
DB_BACKEND = sqlite3