diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 608371cea..25efec746 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,19 +32,19 @@ repos: args: - --trailing-comma=es5 - repo: https://github.com/pre-commit/mirrors-eslint - rev: v9.0.0-beta.0 + rev: v9.0.0-beta.1 hooks: - id: eslint additional_dependencies: - - "eslint@v9.0.0-beta.0" - - "@eslint/js@v9.0.0-beta.0" + - "eslint@v9.0.0-beta.1" + - "@eslint/js@v9.0.0-beta.1" - "globals" files: \.js?$ types: [file] args: - --fix - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.2.2' + rev: 'v0.3.0' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/debug_toolbar/panels/staticfiles.py b/debug_toolbar/panels/staticfiles.py index 5f9efb5c3..2eed2efa0 100644 --- a/debug_toolbar/panels/staticfiles.py +++ b/debug_toolbar/panels/staticfiles.py @@ -4,7 +4,6 @@ from django.conf import settings from django.contrib.staticfiles import finders, storage -from django.core.checks import Warning from django.utils.functional import LazyObject from django.utils.translation import gettext_lazy as _, ngettext @@ -178,27 +177,3 @@ def get_staticfiles_apps(self): if app not in apps: apps.append(app) return apps - - @classmethod - def run_checks(cls): - """ - Check that the integration is configured correctly for the panel. - - Specifically look for static files that haven't been collected yet. - - Return a list of :class: `django.core.checks.CheckMessage` instances. - """ - errors = [] - for finder in finders.get_finders(): - try: - for path, finder_storage in finder.list([]): - finder_storage.path(path) - except OSError: - errors.append( - Warning( - "debug_toolbar requires the STATICFILES_DIRS directories to exist.", - hint="Running manage.py collectstatic may help uncover the issue.", - id="debug_toolbar.staticfiles.W001", - ) - ) - return errors diff --git a/debug_toolbar/panels/templates/panel.py b/debug_toolbar/panels/templates/panel.py index f8c9242ca..c0c6246b2 100644 --- a/debug_toolbar/panels/templates/panel.py +++ b/debug_toolbar/panels/templates/panel.py @@ -154,9 +154,9 @@ def process_context_list(self, context_layers): # QuerySet would trigger the database: user can run the # query from SQL Panel elif isinstance(value, (QuerySet, RawQuerySet)): - temp_layer[ - key - ] = f"<<{value.__class__.__name__.lower()} of {value.model._meta.label}>>" + temp_layer[key] = ( + f"<<{value.__class__.__name__.lower()} of {value.model._meta.label}>>" + ) else: token = allow_sql.set(False) # noqa: FBT003 try: diff --git a/docs/architecture.rst b/docs/architecture.rst new file mode 100644 index 000000000..7be5ac78d --- /dev/null +++ b/docs/architecture.rst @@ -0,0 +1,84 @@ +Architecture +============ + +The Django Debug Toolbar is designed to be flexible and extensible for +developers and third-party panel creators. + +Core Components +--------------- + +While there are several components, the majority of logic and complexity +lives within the following: + +- ``debug_toolbar.middleware.DebugToolbarMiddleware`` +- ``debug_toolbar.toolbar.DebugToolbar`` +- ``debug_toolbar.panels`` + +^^^^^^^^^^^^^^^^^^^^^^ +DebugToolbarMiddleware +^^^^^^^^^^^^^^^^^^^^^^ + +The middleware is how the toolbar integrates with Django projects. +It determines if the toolbar should instrument the request, which +panels to use, facilitates the processing of the request and augmenting +the response with the toolbar. Most logic for how the toolbar interacts +with the user's Django project belongs here. + +^^^^^^^^^^^^ +DebugToolbar +^^^^^^^^^^^^ + +The ``DebugToolbar`` class orchestrates the processing of a request +for each of the panels. It contains the logic that needs to be aware +of all the panels, but doesn't need to interact with the user's Django +project. + +^^^^^^ +Panels +^^^^^^ + +The majority of the complex logic lives within the panels themselves. This +is because the panels are responsible for collecting the various metrics. +Some of the metrics are collected via +`monkey-patching `_, such as +``TemplatesPanel``. Others, such as ``SettingsPanel`` don't need to collect +anything and include the data directly in the response. + +Some panels such as ``SQLPanel`` have additional functionality. This tends +to involve a user clicking on something, and the toolbar presenting a new +page with additional data. That additional data is handled in views defined +in the panels package (for example, ``debug_toolbar.panels.sql.views``). + +Logic Flow +---------- + +When a request comes in, the toolbar first interacts with it in the +middleware. If the middleware determines the request should be instrumented, +it will instantiate the toolbar and pass the request for processing. The +toolbar will use the enabled panels to collect information on the request +and/or response. When the toolbar has completed collecting its metrics on +both the request and response, the middleware will collect the results +from the toolbar. It will inject the HTML and JavaScript to render the +toolbar as well as any headers into the response. + +After the browser renders the panel and the user interacts with it, the +toolbar's JavaScript will send requests to the server. If the view handling +the request needs to fetch data from the toolbar, the request must supply +the store ID. This is so that the toolbar can load the collected metrics +for that particular request. + +The history panel allows a user to view the metrics for any request since +the application was started. The toolbar maintains its state entirely in +memory for the process running ``runserver``. If the application is +restarted the toolbar will lose its state. + +Problematic Parts +----------------- + +- ``debug.panels.templates.panel``: This monkey-patches template rendering + when the panel module is loaded +- ``debug.panels.sql``: This package is particularly complex, but provides + the main benefit of the toolbar +- Support for async and multi-threading: This is currently unsupported, but + is being implemented as per the + `Async compatible toolbar project `_. diff --git a/docs/changes.rst b/docs/changes.rst index 26f76e77b..5c126a0c8 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -10,6 +10,13 @@ Pending * Display a better error message when the toolbar's requests return invalid json. * Render forms with ``as_div`` to silence Django 5.0 deprecation warnings. +* Stayed on top of pre-commit hook updates. +* Added :doc:`architecture documentation ` to help + on-board new contributors. +* Removed the static file path validation check in + :class:`StaticFilesPanel ` + since that check is made redundant by a similar check in Django 4.0 and + later. 4.3.0 (2024-02-01) ------------------ diff --git a/docs/contributing.rst b/docs/contributing.rst index 5e11ee603..55d9a5ca7 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -48,6 +48,12 @@ For convenience, there's an alias for the second command:: Look at ``example/settings.py`` for running the example with another database than SQLite. +Architecture +------------ + +There is high-level information on how the Django Debug Toolbar is structured +in the :doc:`architecture documentation `. + Tests ----- diff --git a/docs/index.rst b/docs/index.rst index e53703d4f..e72037045 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,3 +12,4 @@ Django Debug Toolbar commands changes contributing + architecture diff --git a/tests/panels/test_staticfiles.py b/tests/panels/test_staticfiles.py index 32ed7ea61..0736d86ed 100644 --- a/tests/panels/test_staticfiles.py +++ b/tests/panels/test_staticfiles.py @@ -1,15 +1,8 @@ -import os -import unittest - -import django from django.conf import settings from django.contrib.staticfiles import finders -from django.test.utils import override_settings from ..base import BaseTestCase -PATH_DOES_NOT_EXIST = os.path.join(settings.BASE_DIR, "tests", "invalid_static") - class StaticFilesPanelTestCase(BaseTestCase): panel_id = "StaticFilesPanel" @@ -52,33 +45,3 @@ def test_insert_content(self): "django.contrib.staticfiles.finders.AppDirectoriesFinder", content ) self.assertValidHTML(content) - - @unittest.skipIf(django.VERSION >= (4,), "Django>=4 handles missing dirs itself.") - @override_settings( - STATICFILES_DIRS=[PATH_DOES_NOT_EXIST] + settings.STATICFILES_DIRS, - STATIC_ROOT=PATH_DOES_NOT_EXIST, - ) - def test_finder_directory_does_not_exist(self): - """Misconfigure the static files settings and verify the toolbar runs. - - The test case is that the STATIC_ROOT is in STATICFILES_DIRS and that - the directory of STATIC_ROOT does not exist. - """ - response = self.panel.process_request(self.request) - self.panel.generate_stats(self.request, response) - content = self.panel.content - self.assertIn( - "django.contrib.staticfiles.finders.AppDirectoriesFinder", content - ) - self.assertNotIn( - "django.contrib.staticfiles.finders.FileSystemFinder (2 files)", content - ) - self.assertEqual(self.panel.num_used, 0) - self.assertNotEqual(self.panel.num_found, 0) - expected_apps = ["django.contrib.admin", "debug_toolbar"] - if settings.USE_GIS: - expected_apps = ["django.contrib.gis"] + expected_apps - self.assertEqual(self.panel.get_staticfiles_apps(), expected_apps) - self.assertEqual( - self.panel.get_staticfiles_dirs(), finders.FileSystemFinder().locations - ) diff --git a/tests/sync.py b/tests/sync.py index d71298089..d7a9872fd 100644 --- a/tests/sync.py +++ b/tests/sync.py @@ -1,6 +1,7 @@ """ Taken from channels.db """ + from asgiref.sync import SyncToAsync from django.db import close_old_connections diff --git a/tests/test_checks.py b/tests/test_checks.py index ab8ab199c..3d2ba21ad 100644 --- a/tests/test_checks.py +++ b/tests/test_checks.py @@ -1,7 +1,4 @@ -import os -import unittest from unittest.mock import patch - import django from django.conf import settings from django.core.checks import Error, Warning, run_checks @@ -13,6 +10,7 @@ PATH_DOES_NOT_EXIST = os.path.join(settings.BASE_DIR, "tests", "invalid_static") + class ChecksTestCase(SimpleTestCase): @override_settings( MIDDLEWARE=[ @@ -95,23 +93,6 @@ def test_check_middleware_classes_error(self): messages, ) - @unittest.skipIf(django.VERSION >= (4,), "Django>=4 handles missing dirs itself.") - @override_settings( - STATICFILES_DIRS=[PATH_DOES_NOT_EXIST], - ) - def test_panel_check_errors(self): - messages = run_checks() - self.assertEqual( - messages, - [ - Warning( - "debug_toolbar requires the STATICFILES_DIRS directories to exist.", - hint="Running manage.py collectstatic may help uncover the issue.", - id="debug_toolbar.staticfiles.W001", - ) - ], - ) - @override_settings(DEBUG_TOOLBAR_PANELS=[]) def test_panels_is_empty(self): errors = run_checks() @@ -272,38 +253,6 @@ def test_check_w007_invalid(self, mocked_guess_type): ], ) - # def test_debug_toolbar_installed_when_running_tests(self, mocked_get_config): - # with self.settings(DEBUG=False): - # dt_settings.get_config()["IS_RUNNING_TESTS"] = False - # errors = debug_toolbar_installed_when_running_tests_check(None) - # self.assertEqual(len(errors), 0) - # with self.settings(DEBUG=True): - # dt_settings.get_config()["IS_RUNNING_TESTS"] = True - # errors = debug_toolbar_installed_when_running_tests_check(None) - # self.assertEqual(len(errors), 0) - # with self.settings(DEBUG=True): - # dt_settings.get_config()["IS_RUNNING_TESTS"] = False - # errors = debug_toolbar_installed_when_running_tests_check(None) - # self.assertEqual(len(errors), 0) - # with self.settings(DEBUG=False): - # dt_settings.get_config()["IS_RUNNING_TESTS"] = True - # errors = debug_toolbar_installed_when_running_tests_check(None) - # self.assertEqual( - # errors, - # [ - # Error( - # "The Django Debug Toolbar can't be used with tests", - # hint="Django changes the DEBUG setting to False when running " - # "tests. By default the Django Debug Toolbar is installed because " - # "DEBUG is set to True. For most cases, you need to avoid installing " - # "the toolbar when running tests. If you feel this check is in error, " - # "you can set `DEBUG_TOOLBAR_CONFIG['IS_RUNNING_TESTS'] = False` to " - # "bypass this check.", - # id="debug_toolbar.W008", - # ) - # ], - - # ) def test_debug_toolbar_installed_when_running_tests(self): with self.settings(DEBUG=False): diff --git a/tests/urls_invalid.py b/tests/urls_invalid.py index ccadb6735..a2a56699c 100644 --- a/tests/urls_invalid.py +++ b/tests/urls_invalid.py @@ -1,2 +1,3 @@ """Invalid urls.py file for testing""" + urlpatterns = [] diff --git a/tests/urls_use_package_urls.py b/tests/urls_use_package_urls.py index 50f7dfd69..0a3a91ab3 100644 --- a/tests/urls_use_package_urls.py +++ b/tests/urls_use_package_urls.py @@ -1,4 +1,5 @@ """urls.py to test using debug_toolbar.urls in include""" + from django.urls import include, path import debug_toolbar diff --git a/tox.ini b/tox.ini index 67436888f..4910a9f6b 100644 --- a/tox.ini +++ b/tox.ini @@ -30,6 +30,8 @@ passenv= DB_PASSWORD DB_HOST DB_PORT + DISPLAY + DJANGO_SELENIUM_TESTS GITHUB_* setenv = PYTHONPATH = {toxinidir}