diff --git a/debug_toolbar/_stubs.py b/debug_toolbar/_stubs.py index c536a0fe7..eeeaaaadd 100644 --- a/debug_toolbar/_stubs.py +++ b/debug_toolbar/_stubs.py @@ -1,9 +1,12 @@ from __future__ import annotations -from typing import Any, NamedTuple, Optional +from typing import TYPE_CHECKING, Any, NamedTuple, Optional, Protocol from django import template as dj_template +if TYPE_CHECKING: + from django.http import HttpRequest, HttpResponse + class InspectStack(NamedTuple): frame: Any @@ -24,3 +27,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..6cd855351 100644 --- a/debug_toolbar/middleware.py +++ b/debug_toolbar/middleware.py @@ -5,6 +5,7 @@ import re import socket from functools import cache +from typing import TYPE_CHECKING from asgiref.sync import ( async_to_sync, @@ -13,14 +14,21 @@ sync_to_async, ) from django.conf import settings +from django.http import HttpRequest, HttpResponse from django.utils.module_loading import import_string from debug_toolbar import settings as dt_settings +from debug_toolbar.panels import Panel from debug_toolbar.toolbar import DebugToolbar from debug_toolbar.utils import clear_stack_trace_caches, is_processable_html_response +if TYPE_CHECKING: + from debug_toolbar._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. """ @@ -35,7 +43,7 @@ def show_toolbar(request): return False -def show_toolbar_with_docker(request): +def show_toolbar_with_docker(request: HttpRequest): """ Default function to determine whether to show the toolbar on a given page. """ @@ -86,7 +94,7 @@ def get_show_toolbar(async_mode): """ Get the callback function to show the toolbar. - Will wrap the function with sync_to_async or + Will wrap the function with sync_to_async or async_to_sync depending on the status of async_mode and whether the underlying function is a coroutine. """ @@ -108,7 +116,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. @@ -119,7 +127,7 @@ def __init__(self, get_response): # __call__ to avoid swapping out dunder methods. markcoroutinefunction(self) - def __call__(self, request): + def __call__(self, request: HttpRequest) -> HttpResponse: # Decide whether the toolbar is active for this request. if self.async_mode: return self.__acall__(request) @@ -144,7 +152,7 @@ def __call__(self, request): return self._postprocess(request, response, toolbar) - async def __acall__(self, request): + async def __acall__(self, request: HttpRequest) -> HttpResponse: # Decide whether the toolbar is active for this request. show_toolbar = get_show_toolbar(async_mode=self.async_mode) @@ -172,7 +180,9 @@ async def __acall__(self, request): return self._postprocess(request, response, toolbar) - def _postprocess(self, request, response, toolbar): + def _postprocess( + self, request: HttpRequest, response: HttpResponse, toolbar: DebugToolbar + ) -> HttpResponse: """ Post-process the response. """ @@ -206,7 +216,7 @@ def _postprocess(self, request, response, toolbar): return response @staticmethod - def get_headers(request, panels): + def get_headers(request: HttpRequest, panels: list["Panel"]) -> dict[str, str]: headers = {} for panel in panels: for header, value in panel.get_headers(request).items(): diff --git a/debug_toolbar/panels/__init__.py b/debug_toolbar/panels/__init__.py index a53ba6652..11db12fd4 100644 --- a/debug_toolbar/panels/__init__.py +++ b/debug_toolbar/panels/__init__.py @@ -1,3 +1,5 @@ +from typing import TYPE_CHECKING + from django.core.handlers.asgi import ASGIRequest from django.template.loader import render_to_string from django.utils.functional import classproperty @@ -5,6 +7,9 @@ from debug_toolbar import settings as dt_settings from debug_toolbar.utils import get_name_from_obj +if TYPE_CHECKING: + from debug_toolbar._stubs import GetResponse + class Panel: """ @@ -13,7 +18,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..8a418f572 100644 --- a/debug_toolbar/toolbar.py +++ b/debug_toolbar/toolbar.py @@ -6,21 +6,26 @@ import re import uuid from functools import cache +from typing import 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 from debug_toolbar import APP_NAME, settings as dt_settings +from debug_toolbar._stubs import GetResponse from debug_toolbar.store import get_store +from .panels import Panel + logger = logging.getLogger(__name__) @@ -29,7 +34,9 @@ class DebugToolbar: _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 = [] @@ -49,14 +56,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 +79,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 +87,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 +108,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. @@ -125,13 +132,10 @@ def fetch(cls, request_id, panel_id=None): if get_store().exists(request_id): return StoredDebugToolbar.from_store(request_id, panel_id=panel_id) - # 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[Panel]] = None @classmethod - def get_panel_classes(cls): + def get_panel_classes(cls) -> list[Panel]: if cls._panel_classes is None: # Load panels in a temporary variable for thread safety. panel_classes = [ @@ -140,10 +144,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 @@ -159,7 +163,7 @@ def get_urls(cls): return cls._urlpatterns @classmethod - def is_toolbar_request(cls, request): + def is_toolbar_request(cls, request: HttpRequest) -> bool: """ Determine if the request is for a DebugToolbar view. """ @@ -171,7 +175,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 +192,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. """ @@ -200,7 +207,9 @@ def from_store_get_response(request): class StoredDebugToolbar(DebugToolbar): - def __init__(self, request, get_response, request_id=None): + def __init__( + self, request: HttpRequest, get_response: "GetResponse", request_id=None + ): self.request = None self.config = dt_settings.get_config().copy() self.process_request = get_response @@ -210,7 +219,7 @@ def __init__(self, request, get_response, request_id=None): self.init_store() @classmethod - def from_store(cls, request_id, panel_id=None): + def from_store(cls, request_id, panel_id=None) -> "StoredDebugToolbar": toolbar = StoredDebugToolbar( None, from_store_get_response, request_id=request_id ) @@ -226,7 +235,7 @@ def from_store(cls, request_id, panel_id=None): return toolbar -def debug_toolbar_urls(prefix="__debug__"): +def debug_toolbar_urls(prefix="__debug__") -> list[URLPattern]: """ Return a URL pattern for serving toolbar in debug mode. diff --git a/docs/panels.rst b/docs/panels.rst index f2364ea7c..0edeb7f09 100644 --- a/docs/panels.rst +++ b/docs/panels.rst @@ -379,6 +379,8 @@ There is no public CSS API at this time. .. automethod:: debug_toolbar.panels.Panel.run_checks +.. autoclass:: debug_toolbar._stubs.GetResponse + .. _javascript-api: JavaScript API