diff --git a/debug_toolbar/_stubs.py b/debug_toolbar/_stubs.py index c536a0fe7..98d5dd33e 100644 --- a/debug_toolbar/_stubs.py +++ b/debug_toolbar/_stubs.py @@ -1,8 +1,7 @@ -from __future__ import annotations - -from typing import Any, NamedTuple, Optional +from typing import Any, NamedTuple, Optional, Protocol from django import template as dj_template +from django.http import HttpRequest, HttpResponse class InspectStack(NamedTuple): @@ -24,3 +23,7 @@ class RenderContext(dj_template.context.RenderContext): class RequestContext(dj_template.RequestContext): template: dj_template.Template render_context: RenderContext + + +class GetResponse(Protocol): + def __call__(self, request: HttpRequest) -> HttpResponse: ... diff --git a/debug_toolbar/middleware.py b/debug_toolbar/middleware.py index 2292bde8b..ffd688665 100644 --- a/debug_toolbar/middleware.py +++ b/debug_toolbar/middleware.py @@ -13,14 +13,19 @@ sync_to_async, ) from django.conf import settings +from django.http import HttpRequest from django.utils.module_loading import import_string from debug_toolbar import settings as dt_settings from debug_toolbar.toolbar import DebugToolbar from debug_toolbar.utils import clear_stack_trace_caches, is_processable_html_response +from ._stubs import GetResponse -def show_toolbar(request): +_HTML_TYPES = ("text/html", "application/xhtml+xml") + + +def show_toolbar(request: HttpRequest): """ Default function to determine whether to show the toolbar on a given page. """ @@ -108,7 +113,7 @@ class DebugToolbarMiddleware: sync_capable = True async_capable = True - def __init__(self, get_response): + def __init__(self, get_response: GetResponse): self.get_response = get_response # If get_response is a coroutine function, turns us into async mode so # a thread is not consumed during a whole request. diff --git a/debug_toolbar/panels/__init__.py b/debug_toolbar/panels/__init__.py index a53ba6652..65e82d073 100644 --- a/debug_toolbar/panels/__init__.py +++ b/debug_toolbar/panels/__init__.py @@ -3,6 +3,7 @@ from django.utils.functional import classproperty from debug_toolbar import settings as dt_settings +from debug_toolbar._stubs import GetResponse from debug_toolbar.utils import get_name_from_obj @@ -13,7 +14,7 @@ class Panel: is_async = False - def __init__(self, toolbar, get_response): + def __init__(self, toolbar, get_response: GetResponse): self.toolbar = toolbar self.get_response = get_response self.from_store = False diff --git a/debug_toolbar/toolbar.py b/debug_toolbar/toolbar.py index 38f1a3803..af5529dbd 100644 --- a/debug_toolbar/toolbar.py +++ b/debug_toolbar/toolbar.py @@ -6,14 +6,16 @@ import re import uuid from functools import cache +from typing import TYPE_CHECKING, Any, Optional from django.apps import apps from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.dispatch import Signal +from django.http import HttpRequest from django.template import TemplateSyntaxError from django.template.loader import render_to_string -from django.urls import include, path, re_path, resolve +from django.urls import URLPattern, include, path, re_path, resolve from django.urls.exceptions import Resolver404 from django.utils.module_loading import import_string from django.utils.translation import get_language, override as lang_override @@ -21,27 +23,35 @@ from debug_toolbar import APP_NAME, settings as dt_settings from debug_toolbar.store import get_store +from ._stubs import GetResponse + logger = logging.getLogger(__name__) +if TYPE_CHECKING: + from .panels import Panel + + class DebugToolbar: # for internal testing use only _created = Signal() store = None - def __init__(self, request, get_response, request_id=None): + def __init__( + self, request: HttpRequest, get_response: GetResponse, request_id=None + ): self.request = request self.config = dt_settings.get_config().copy() panels = [] - for panel_class in reversed(self.get_panel_classes()): + for panel_class in reversed(list(self.get_panel_classes())): panel = panel_class(self, get_response) panels.append(panel) if panel.enabled: get_response = panel.process_request self.process_request = get_response - self._panels = {panel.panel_id: panel for panel in reversed(panels)} - self.stats = {} - self.server_timing_stats = {} + self._panels = {panel.panel_id: panel for panel in panels} # type: ignore[var-annotated] + self.stats: dict[str, Any] = {} + self.server_timing_stats: dict[str, Any] = {} self.request_id = request_id self.init_store() self._created.send(request, toolbar=self) @@ -49,14 +59,14 @@ def __init__(self, request, get_response, request_id=None): # Manage panels @property - def panels(self): + def panels(self) -> list["Panel"]: """ Get a list of all available panels. """ return list(self._panels.values()) @property - def enabled_panels(self): + def enabled_panels(self) -> list["Panel"]: """ Get a list of panels enabled for the current request. """ @@ -72,7 +82,7 @@ def csp_nonce(self): """ return getattr(self.request, "csp_nonce", None) - def get_panel_by_id(self, panel_id): + def get_panel_by_id(self, panel_id: str) -> "Panel": """ Get the panel with the given id, which is the class name by default. """ @@ -80,7 +90,7 @@ def get_panel_by_id(self, panel_id): # Handle rendering the toolbar in HTML - def render_toolbar(self): + def render_toolbar(self) -> str: """ Renders the overall Toolbar with panels inside. """ @@ -101,7 +111,7 @@ def render_toolbar(self): else: raise - def should_render_panels(self): + def should_render_panels(self) -> bool: """Determine whether the panels should be rendered during the request If False, the panels will be loaded via Ajax. @@ -128,7 +138,7 @@ def fetch(cls, request_id, panel_id=None): # Manually implement class-level caching of panel classes and url patterns # because it's more obvious than going through an abstraction. - _panel_classes = None + _panel_classes: Optional[list[type["Panel"]]] = None @classmethod def get_panel_classes(cls): @@ -140,10 +150,10 @@ def get_panel_classes(cls): cls._panel_classes = panel_classes return cls._panel_classes - _urlpatterns = None + _urlpatterns: Optional[list[URLPattern]] = None @classmethod - def get_urls(cls): + def get_urls(cls) -> list[URLPattern]: if cls._urlpatterns is None: from . import views @@ -153,13 +163,13 @@ def get_urls(cls): path("render_panel/", views.render_panel, name="render_panel"), ] # Per-panel URLs - for panel_class in cls.get_panel_classes(): + for panel_class in list(cls.get_panel_classes()): urlpatterns += panel_class.get_urls() cls._urlpatterns = urlpatterns return cls._urlpatterns @classmethod - def is_toolbar_request(cls, request): + def is_toolbar_request(cls, request) -> bool: """ Determine if the request is for a DebugToolbar view. """ @@ -171,7 +181,10 @@ def is_toolbar_request(cls, request): ) except Resolver404: return False - return resolver_match.namespaces and resolver_match.namespaces[-1] == APP_NAME + return ( + bool(resolver_match.namespaces) + and resolver_match.namespaces[-1] == APP_NAME + ) @staticmethod @cache @@ -185,7 +198,7 @@ def get_observe_request(): return func_or_path -def observe_request(request): +def observe_request(request: HttpRequest): """ Determine whether to update the toolbar from a client side request. """ @@ -216,7 +229,7 @@ def from_store(cls, request_id, panel_id=None): ) toolbar._panels = {} - for panel_class in reversed(cls.get_panel_classes()): + for panel_class in reversed(list(cls.get_panel_classes())): panel = panel_class(toolbar, from_store_get_response) if panel_id and panel.panel_id != panel_id: continue