diff --git a/debug_toolbar/panels/templates/panel.py b/debug_toolbar/panels/templates/panel.py index 72565f016..e63417fa9 100644 --- a/debug_toolbar/panels/templates/panel.py +++ b/debug_toolbar/panels/templates/panel.py @@ -83,58 +83,11 @@ def _store_template_info(self, sender, **kwargs): if is_debug_toolbar_template: return - context_list = [] - for context_layer in context.dicts: - if hasattr(context_layer, "items") and context_layer: - # Check if the layer is in the cache. - pformatted = None - for key_values, _pformatted in self.pformat_layers: - if key_values == context_layer: - pformatted = _pformatted - break - - if pformatted is None: - temp_layer = {} - for key, value in context_layer.items(): - # Replace any request elements - they have a large - # Unicode representation and the request data is - # already made available from the Request panel. - if isinstance(value, http.HttpRequest): - temp_layer[key] = "<>" - # Replace the debugging sql_queries element. The SQL - # data is already made available from the SQL panel. - elif key == "sql_queries" and isinstance(value, list): - temp_layer[key] = "<>" - # Replace LANGUAGES, which is available in i18n context - # processor - elif key == "LANGUAGES" and isinstance(value, tuple): - temp_layer[key] = "<>" - # QuerySet would trigger the database: user can run the - # query from SQL Panel - elif isinstance(value, (QuerySet, RawQuerySet)): - temp_layer[key] = "<<{} of {}>>".format( - value.__class__.__name__.lower(), - value.model._meta.label, - ) - else: - token = allow_sql.set(False) # noqa: FBT003 - try: - saferepr(value) # this MAY trigger a db query - except SQLQueryTriggered: - temp_layer[key] = "<>" - except UnicodeEncodeError: - temp_layer[key] = "<>" - except Exception: - temp_layer[key] = "<>" - else: - temp_layer[key] = value - finally: - allow_sql.reset(token) - pformatted = pformat(temp_layer) - self.pformat_layers.append((context_layer, pformatted)) - context_list.append(pformatted) - - kwargs["context"] = context_list + kwargs["context"] = [ + context_layer + for context_layer in context.dicts + if hasattr(context_layer, "items") and context_layer + ] kwargs["context_processors"] = getattr(context, "context_processors", None) self.templates.append(kwargs) @@ -167,6 +120,64 @@ def enable_instrumentation(self): def disable_instrumentation(self): template_rendered.disconnect(self._store_template_info) + def process_context_list(self, context_layers): + context_list = [] + for context_layer in context_layers: + # Check if the layer is in the cache. + pformatted = None + for key_values, _pformatted in self.pformat_layers: + if key_values == context_layer: + pformatted = _pformatted + break + + if pformatted is None: + temp_layer = {} + for key, value in context_layer.items(): + # Do not force evaluating LazyObject + if hasattr(value, "_wrapped"): + # SimpleLazyObject has __repr__ which includes actual value + # if it has been already evaluated + temp_layer[key] = repr(value) + # Replace any request elements - they have a large + # Unicode representation and the request data is + # already made available from the Request panel. + elif isinstance(value, http.HttpRequest): + temp_layer[key] = "<>" + # Replace the debugging sql_queries element. The SQL + # data is already made available from the SQL panel. + elif key == "sql_queries" and isinstance(value, list): + temp_layer[key] = "<>" + # Replace LANGUAGES, which is available in i18n context + # processor + elif key == "LANGUAGES" and isinstance(value, tuple): + temp_layer[key] = "<>" + # QuerySet would trigger the database: user can run the + # query from SQL Panel + elif isinstance(value, (QuerySet, RawQuerySet)): + temp_layer[key] = "<<{} of {}>>".format( + value.__class__.__name__.lower(), + value.model._meta.label, + ) + else: + token = allow_sql.set(False) # noqa: FBT003 + try: + saferepr(value) # this MAY trigger a db query + except SQLQueryTriggered: + temp_layer[key] = "<>" + except UnicodeEncodeError: + temp_layer[key] = "<>" + except Exception: + temp_layer[key] = "<>" + else: + temp_layer[key] = value + finally: + allow_sql.reset(token) + pformatted = pformat(temp_layer) + self.pformat_layers.append((context_layer, pformatted)) + context_list.append(pformatted) + + return context_list + def generate_stats(self, request, response): template_context = [] for template_data in self.templates: @@ -182,8 +193,11 @@ def generate_stats(self, request, response): info["template"] = template # Clean up context for better readability if self.toolbar.config["SHOW_TEMPLATE_CONTEXT"]: - context_list = template_data.get("context", []) - info["context"] = "\n".join(context_list) + if "context_list" not in template_data: + template_data["context_list"] = self.process_context_list( + template_data.get("context", []) + ) + info["context"] = "\n".join(template_data["context_list"]) template_context.append(info) # Fetch context_processors/template_dirs from any template diff --git a/docs/changes.rst b/docs/changes.rst index 24679bfd1..ab774136d 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -6,6 +6,10 @@ Pending * Removed outdated third-party panels from the list. * Avoided the unnecessary work of recursively quoting SQL parameters. +* Postponed context process in templates panel to include lazy evaluated + content. +* Fixed template panel to avoid evaluating ``LazyObject`` when not already + evaluated. 4.2.0 (2023-08-10) ------------------ diff --git a/tests/panels/test_template.py b/tests/panels/test_template.py index 37e70cfa5..eb23cde31 100644 --- a/tests/panels/test_template.py +++ b/tests/panels/test_template.py @@ -2,6 +2,7 @@ from django.contrib.auth.models import User from django.template import Context, RequestContext, Template from django.test import override_settings +from django.utils.functional import SimpleLazyObject from ..base import BaseTestCase, IntegrationTestCase from ..forms import TemplateReprForm @@ -21,6 +22,7 @@ def tearDown(self): super().tearDown() def test_queryset_hook(self): + response = self.panel.process_request(self.request) t = Template("No context variables here!") c = Context( { @@ -29,12 +31,13 @@ def test_queryset_hook(self): } ) t.render(c) + self.panel.generate_stats(self.request, response) # ensure the query was NOT logged self.assertEqual(len(self.sql_panel._queries), 0) self.assertEqual( - self.panel.templates[0]["context"], + self.panel.templates[0]["context_list"], [ "{'False': False, 'None': None, 'True': True}", "{'deep_queryset': '<>',\n" @@ -99,16 +102,34 @@ def test_disabled(self): self.assertFalse(self.panel.enabled) def test_empty_context(self): + response = self.panel.process_request(self.request) t = Template("") c = Context({}) t.render(c) + self.panel.generate_stats(self.request, response) # Includes the builtin context but not the empty one. self.assertEqual( - self.panel.templates[0]["context"], + self.panel.templates[0]["context_list"], ["{'False': False, 'None': None, 'True': True}"], ) + def test_lazyobject(self): + response = self.panel.process_request(self.request) + t = Template("") + c = Context({"lazy": SimpleLazyObject(lambda: "lazy_value")}) + t.render(c) + self.panel.generate_stats(self.request, response) + self.assertNotIn("lazy_value", self.panel.content) + + def test_lazyobject_eval(self): + response = self.panel.process_request(self.request) + t = Template("{{lazy}}") + c = Context({"lazy": SimpleLazyObject(lambda: "lazy_value")}) + self.assertEqual(t.render(c), "lazy_value") + self.panel.generate_stats(self.request, response) + self.assertIn("lazy_value", self.panel.content) + @override_settings( DEBUG=True, DEBUG_TOOLBAR_PANELS=["debug_toolbar.panels.templates.TemplatesPanel"]