Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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] = {}
Comment on lines +52 to +61
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.

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