Skip to content

Restructure the Panel class to execute more like the MIDDLEWARE #1140

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 21, 2019
Merged
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
20 changes: 7 additions & 13 deletions debug_toolbar/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,8 @@ def check_middleware(app_configs, **kwargs):
gzip_index = None
debug_toolbar_indexes = []

setting = getattr(settings, "MIDDLEWARE", None)
setting_name = "MIDDLEWARE"
if setting is None:
setting = settings.MIDDLEWARE_CLASSES
setting_name = "MIDDLEWARE_CLASSES"

# Determine the indexes which gzip and/or the toolbar are installed at
for i, middleware in enumerate(setting):
for i, middleware in enumerate(settings.MIDDLEWARE):
if is_middleware_class(GZipMiddleware, middleware):
gzip_index = i
elif is_middleware_class(DebugToolbarMiddleware, middleware):
Expand All @@ -41,9 +35,9 @@ def check_middleware(app_configs, **kwargs):
errors.append(
Warning(
"debug_toolbar.middleware.DebugToolbarMiddleware is missing "
"from %s." % setting_name,
"from MIDDLEWARE.",
hint="Add debug_toolbar.middleware.DebugToolbarMiddleware to "
"%s." % setting_name,
"MIDDLEWARE.",
id="debug_toolbar.W001",
)
)
Expand All @@ -52,9 +46,9 @@ def check_middleware(app_configs, **kwargs):
errors.append(
Warning(
"debug_toolbar.middleware.DebugToolbarMiddleware occurs "
"multiple times in %s." % setting_name,
"multiple times in MIDDLEWARE.",
hint="Load debug_toolbar.middleware.DebugToolbarMiddleware only "
"once in %s." % setting_name,
"once in MIDDLEWARE.",
id="debug_toolbar.W002",
)
)
Expand All @@ -63,9 +57,9 @@ def check_middleware(app_configs, **kwargs):
errors.append(
Warning(
"debug_toolbar.middleware.DebugToolbarMiddleware occurs before "
"django.middleware.gzip.GZipMiddleware in %s." % setting_name,
"django.middleware.gzip.GZipMiddleware in MIDDLEWARE.",
hint="Move debug_toolbar.middleware.DebugToolbarMiddleware to "
"after django.middleware.gzip.GZipMiddleware in %s." % setting_name,
"after django.middleware.gzip.GZipMiddleware in MIDDLEWARE.",
id="debug_toolbar.W003",
)
)
Expand Down
71 changes: 17 additions & 54 deletions debug_toolbar/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@
from __future__ import absolute_import, unicode_literals

import re
import threading

from django.conf import settings
from django.utils import six
from django.utils.deprecation import MiddlewareMixin
from django.utils.encoding import force_text
from django.utils.lru_cache import lru_cache
from django.utils.module_loading import import_string
Expand Down Expand Up @@ -41,70 +39,35 @@ def get_show_toolbar():
return func_or_path


class DebugToolbarMiddleware(MiddlewareMixin):
class DebugToolbarMiddleware(object):
"""
Middleware to set up Debug Toolbar on incoming request and render toolbar
on outgoing response.
"""

debug_toolbars = {}
def __init__(self, get_response):
self.get_response = get_response

def process_request(self, request):
# Decide whether the toolbar is active for this request.
def __call__(self, request):
# Decide whether the toolbar is active for this request. Don't render
# the toolbar during AJAX requests.
show_toolbar = get_show_toolbar()
if not show_toolbar(request):
return
if not show_toolbar(request) or request.is_ajax():
return self.get_response(request)

# Don't render the toolbar during AJAX requests.
if request.is_ajax():
return

toolbar = DebugToolbar(request)
self.__class__.debug_toolbars[threading.current_thread().ident] = toolbar
toolbar = DebugToolbar(request, self.get_response)

# Activate instrumentation ie. monkey-patch.
for panel in toolbar.enabled_panels:
panel.enable_instrumentation()

# Run process_request methods of panels like Django middleware.
response = None
for panel in toolbar.enabled_panels:
response = panel.process_request(request)
if response:
break
return response

def process_view(self, request, view_func, view_args, view_kwargs):
toolbar = self.__class__.debug_toolbars.get(threading.current_thread().ident)
if not toolbar:
return

# Run process_view methods of panels like Django middleware.
response = None
for panel in toolbar.enabled_panels:
response = panel.process_view(request, view_func, view_args, view_kwargs)
if response:
break
return response

def process_response(self, request, response):
toolbar = self.__class__.debug_toolbars.pop(
threading.current_thread().ident, None
)
if not toolbar:
return response

# Run process_response methods of panels like Django middleware.
for panel in reversed(toolbar.enabled_panels):
new_response = panel.process_response(request, response)
if new_response:
response = new_response

# Deactivate instrumentation ie. monkey-unpatch. This must run
# regardless of the response. Keep 'return' clauses below.
# (NB: Django's model for middleware doesn't guarantee anything.)
for panel in reversed(toolbar.enabled_panels):
panel.disable_instrumentation()
try:
# Run panels like Django middleware.
response = toolbar.process_request(request)
finally:
# Deactivate instrumentation ie. monkey-unpatch. This must run
# regardless of the response. Keep 'return' clauses below.
for panel in reversed(toolbar.enabled_panels):
panel.disable_instrumentation()

# Check for responses where the toolbar can't be inserted.
content_encoding = response.get("Content-Encoding", "")
Expand Down
39 changes: 10 additions & 29 deletions debug_toolbar/panels/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ class Panel(object):
Base class for panels.
"""

def __init__(self, toolbar):
def __init__(self, toolbar, get_response):
self.toolbar = toolbar
self.get_response = get_response

# Private panel properties

Expand Down Expand Up @@ -129,8 +130,7 @@ def disable_instrumentation(self):
This is the opposite of :meth:`enable_instrumentation`.

Unless the toolbar or this panel is disabled, this method will be
called late in :class:`DebugToolbarMiddleware.process_response`. It
should be idempotent.
called late in the middleware. It should be idempotent.
"""

# Store and retrieve stats (shared between panels for no good reason)
Expand Down Expand Up @@ -168,40 +168,21 @@ def get_server_timing_stats(self):

def process_request(self, request):
"""
Like process_request in Django's middleware.
Like __call__ in Django's middleware.

Write panel logic related to the request there. Save data with
:meth:`record_stats`.
"""

def process_view(self, request, view_func, view_args, view_kwargs):
"""
Like process_view in Django's middleware.

Write panel logic related to the view there. Save data with
:meth:`record_stats`.
"""

def process_response(self, request, response):
"""
Like process_response in Django's middleware. This is similar to
:meth:`generate_stats <debug_toolbar.panels.Panel.generate_stats>`,
but will be executed on every request. It should be used when either
the logic needs to be executed on every request or it needs to change
the response entirely, such as :class:`RedirectsPanel`.

Write panel logic related to the response there. Post-process data
gathered while the view executed. Save data with :meth:`record_stats`.

Return a response to overwrite the existing response.
Return the existing response or overwrite it.
"""
return self.get_response(request)

def generate_stats(self, request, response):
"""
Similar to :meth:`process_response
<debug_toolbar.panels.Panel.process_response>`,
but may not be executed on every request. This will only be called if
the toolbar will be inserted into the request.
Called after :meth:`process_request
<debug_toolbar.panels.Panel.process_request>`, but may not be executed
on every request. This will only be called if the toolbar will be
inserted into the request.

Write panel logic related to the response there. Post-process data
gathered while the view executed. Save data with :meth:`record_stats`.
Expand Down
1 change: 1 addition & 0 deletions debug_toolbar/panels/headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def process_request(self, request):
self.record_stats(
{"request_headers": self.request_headers, "environ": self.environ}
)
return super(HeadersPanel, self).process_request(request)

def generate_stats(self, request, response):
self.response_headers = OrderedDict(sorted(response.items()))
Expand Down
1 change: 1 addition & 0 deletions debug_toolbar/panels/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def nav_subtitle(self):

def process_request(self, request):
collector.clear_collection()
return super(LoggingPanel, self).process_request(request)

def generate_stats(self, request, response):
records = collector.get_collection()
Expand Down
7 changes: 4 additions & 3 deletions debug_toolbar/panels/profiling.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,11 @@ class ProfilingPanel(Panel):

template = "debug_toolbar/panels/profiling.html"

def process_view(self, request, view_func, view_args, view_kwargs):
def process_request(self, request):
self.profiler = cProfile.Profile()
args = (request,) + view_args
return self.profiler.runcall(view_func, *args, **view_kwargs)
return self.profiler.runcall(
super(ProfilingPanel, self).process_request, request
)

def add_node(self, func_list, func, max_depth, cum_time=0.1):
func_list.append(func)
Expand Down
3 changes: 2 additions & 1 deletion debug_toolbar/panels/redirects.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ class RedirectsPanel(Panel):

nav_title = _("Intercept redirects")

def process_response(self, request, response):
def process_request(self, request):
response = super(RedirectsPanel, self).process_request(request)
if 300 <= int(response.status_code) < 400:
redirect_to = response.get("Location", None)
if redirect_to:
Expand Down
1 change: 1 addition & 0 deletions debug_toolbar/panels/staticfiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ def nav_subtitle(self):

def process_request(self, request):
collector.clear_collection()
return super(StaticFilesPanel, self).process_request(request)

def generate_stats(self, request, response):
used_paths = collector.get_collection()
Expand Down
1 change: 1 addition & 0 deletions debug_toolbar/panels/timer.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def process_request(self, request):
self._start_time = time.time()
if self.has_content:
self._start_rusage = resource.getrusage(resource.RUSAGE_SELF)
return super(TimerPanel, self).process_request(request)

def generate_stats(self, request, response):
stats = {}
Expand Down
15 changes: 11 additions & 4 deletions debug_toolbar/toolbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,20 @@


class DebugToolbar(object):
def __init__(self, request):
def __init__(self, request, get_response):
self.request = request
self.config = dt_settings.get_config().copy()
panels = []
for panel_class in reversed(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 = OrderedDict()
for panel_class in self.get_panel_classes():
panel_instance = panel_class(self)
self._panels[panel_instance.panel_id] = panel_instance
while panels:
panel = panels.pop()
self._panels[panel.panel_id] = panel
self.stats = {}
self.server_timing_stats = {}
self.store_id = None
Expand Down
13 changes: 13 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ UNRELEASED
* Updated ``StaticFilesPanel`` to be compatible with Django 3.0.
* The ``ProfilingPanel`` is now enabled but inactive by default.
* Fixed toggling of table rows in the profiling panel UI.
* The ``ProfilingPanel`` no longer skips remaining panels or middlewares.

**Backwards incompatible changes**
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Removed support for Django's deprecated ``MIDDLEWARE_CLASSES`` setting.
* Restructured ``Panel`` to execute more like the new-style Django MIDDLEWARE.
The ``Panel.__init__()`` method is now passed ``get_response`` as the first
positional argument. The ``Panel.process_request()`` method must now always
return a response. Usually this is the response returned by
``get_response()`` but the panel may also return a different response as is
the case in the ``RedirectsPanel``. Third party panels must adjust to this
new architecture. ``Panel.process_response()`` and ``Panel.process_view()``
have been removed as a result of this change.

1.11 (2018-12-03)
-----------------
Expand Down
15 changes: 3 additions & 12 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,20 +75,11 @@ settings module as follows::
# ...
]

Old-style middleware::

MIDDLEWARE_CLASSES = [
# ...
'debug_toolbar.middleware.DebugToolbarMiddleware',
# ...
]

.. warning::

The order of ``MIDDLEWARE`` and ``MIDDLEWARE_CLASSES`` is important. You
should include the Debug Toolbar middleware as early as possible in the
list. However, it must come after any other middleware that encodes the
response's content, such as
The order of ``MIDDLEWARE`` is important. You should include the Debug
Toolbar middleware as early as possible in the list. However, it must come
after any other middleware that encodes the response's content, such as
:class:`~django.middleware.gzip.GZipMiddleware`.

Configuring Internal IPs
Expand Down
15 changes: 0 additions & 15 deletions docs/panels.rst
Original file line number Diff line number Diff line change
Expand Up @@ -117,17 +117,6 @@ Profiling information for the processing of the request.
This panel is included but inactive by default. You can activate it by default
with the ``DISABLE_PANELS`` configuration option.

If the ``debug_toolbar.middleware.DebugToolbarMiddleware`` is first in
``MIDDLEWARE`` then the other middlewares' ``process_view`` methods will not be
executed. This is because ``ProfilingPanel.process_view`` will return a
``HttpResponse`` which causes the other middlewares' ``process_view`` methods
to be skipped.

If you run into this issues, then you should either deactivate the
``ProfilingPanel`` or move ``DebugToolbarMiddleware`` to the end of
``MIDDLEWARE``. If you do the latter, then the debug toolbar won't track the
execution of other middleware.

Third-party panels
------------------

Expand Down Expand Up @@ -342,10 +331,6 @@ unauthorized access. There is no public CSS API at this time.

.. automethod:: debug_toolbar.panels.Panel.process_request

.. automethod:: debug_toolbar.panels.Panel.process_view

.. automethod:: debug_toolbar.panels.Panel.process_response

.. automethod:: debug_toolbar.panels.Panel.generate_stats

.. _javascript-api:
Expand Down
10 changes: 0 additions & 10 deletions docs/tips.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,6 @@ Browsers have become more aggressive with caching static assets, such as
JavaScript and CSS files. Check your browser's development console, and if
you see errors, try a hard browser refresh or clearing your cache.

Middleware isn't working correctly
----------------------------------

Using the Debug Toolbar in its default configuration with the profiling panel
active will cause middlewares after
``debug_toolbar.middleware.DebugToolbarMiddleware`` to not execute their
``process_view`` functions. This can be resolved by disabling the profiling
panel or moving the ``DebugToolbarMiddleware`` to the end of ``MIDDLEWARE``.
Read more about it at :ref:`ProfilingPanel <profiling-panel>`

Performance considerations
--------------------------

Expand Down
Loading