Skip to content
9 changes: 8 additions & 1 deletion debug_toolbar/_stubs.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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: ...
26 changes: 18 additions & 8 deletions debug_toolbar/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import re
import socket
from functools import cache
from typing import TYPE_CHECKING

from asgiref.sync import (
async_to_sync,
Expand All @@ -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.
"""
Expand All @@ -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.
"""
Expand Down Expand Up @@ -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.
"""
Expand All @@ -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.
Expand All @@ -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)
Expand All @@ -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)

Expand Down Expand Up @@ -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.
"""
Expand Down Expand Up @@ -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():
Expand Down
7 changes: 6 additions & 1 deletion debug_toolbar/panels/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
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

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:
"""
Expand All @@ -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
Expand Down
62 changes: 42 additions & 20 deletions debug_toolbar/toolbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,41 @@
import logging
import re
import uuid
from collections import OrderedDict
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

from debug_toolbar import APP_NAME, settings as dt_settings
from debug_toolbar._stubs import GetResponse
from debug_toolbar.store import get_store

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 = []
Expand All @@ -39,24 +49,31 @@ def __init__(self, request, get_response, request_id=None):
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 = {}
# Use OrderedDict for the _panels attribute so that items can be efficiently
# removed using FIFO order in the DebugToolbar.store() method. The .popitem()
# method of Python's built-in dict only supports LIFO removal.
# type: ignore[var-annotated]
self._panels = OrderedDict()
while panels:
panel = panels.pop()
self._panels[panel.panel_id] = panel
self.stats: dict[str, Any] = {}
self.server_timing_stats: dict[str, Any] = {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like there may have been a merge conflict issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this was due to a merge conflict

self.request_id = request_id
self.init_store()
self._created.send(request, toolbar=self)

# 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.
"""
Expand All @@ -72,15 +89,15 @@ 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":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm likely missing something basic. Why can't we use -> Panel since we have the TYPE_CHECKING import check?

"""
Get the panel with the given id, which is the class name by default.
"""
return self._panels[panel_id]

# Handle rendering the toolbar in HTML

def render_toolbar(self):
def render_toolbar(self) -> str:
"""
Renders the overall Toolbar with panels inside.
"""
Expand All @@ -101,7 +118,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.
Expand All @@ -128,10 +145,10 @@ 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):
def get_panel_classes(cls) -> list[type["Panel"]]:
if cls._panel_classes is None:
# Load panels in a temporary variable for thread safety.
panel_classes = [
Expand All @@ -140,10 +157,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

Expand All @@ -159,7 +176,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.
"""
Expand All @@ -171,7 +188,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
Expand All @@ -185,7 +205,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.
"""
Expand All @@ -200,7 +220,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
Expand All @@ -210,7 +232,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
)
Expand All @@ -226,7 +248,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.

Expand Down
2 changes: 2 additions & 0 deletions docs/panels.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down