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