Skip to content

Commit 3ceb965

Browse files
authored
panels(templates): avoid evaluating LazyObject (#1833)
* panels(templates): postpone context processing - this makes it show evaluated querysets which were used in the template - removes completely context processing when SHOW_TEMPLATE_CONTEXT is disabled * panels(templates): avoid evaluating LazyObject LazyObject is typically used for something expensive to evaluate, so avoid evaluating it just for showing it in the debug toolbar.
1 parent 06c42d9 commit 3ceb965

File tree

3 files changed

+95
-56
lines changed

3 files changed

+95
-56
lines changed

debug_toolbar/panels/templates/panel.py

Lines changed: 68 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -83,58 +83,11 @@ def _store_template_info(self, sender, **kwargs):
8383
if is_debug_toolbar_template:
8484
return
8585

86-
context_list = []
87-
for context_layer in context.dicts:
88-
if hasattr(context_layer, "items") and context_layer:
89-
# Check if the layer is in the cache.
90-
pformatted = None
91-
for key_values, _pformatted in self.pformat_layers:
92-
if key_values == context_layer:
93-
pformatted = _pformatted
94-
break
95-
96-
if pformatted is None:
97-
temp_layer = {}
98-
for key, value in context_layer.items():
99-
# Replace any request elements - they have a large
100-
# Unicode representation and the request data is
101-
# already made available from the Request panel.
102-
if isinstance(value, http.HttpRequest):
103-
temp_layer[key] = "<<request>>"
104-
# Replace the debugging sql_queries element. The SQL
105-
# data is already made available from the SQL panel.
106-
elif key == "sql_queries" and isinstance(value, list):
107-
temp_layer[key] = "<<sql_queries>>"
108-
# Replace LANGUAGES, which is available in i18n context
109-
# processor
110-
elif key == "LANGUAGES" and isinstance(value, tuple):
111-
temp_layer[key] = "<<languages>>"
112-
# QuerySet would trigger the database: user can run the
113-
# query from SQL Panel
114-
elif isinstance(value, (QuerySet, RawQuerySet)):
115-
temp_layer[key] = "<<{} of {}>>".format(
116-
value.__class__.__name__.lower(),
117-
value.model._meta.label,
118-
)
119-
else:
120-
token = allow_sql.set(False) # noqa: FBT003
121-
try:
122-
saferepr(value) # this MAY trigger a db query
123-
except SQLQueryTriggered:
124-
temp_layer[key] = "<<triggers database query>>"
125-
except UnicodeEncodeError:
126-
temp_layer[key] = "<<Unicode encode error>>"
127-
except Exception:
128-
temp_layer[key] = "<<unhandled exception>>"
129-
else:
130-
temp_layer[key] = value
131-
finally:
132-
allow_sql.reset(token)
133-
pformatted = pformat(temp_layer)
134-
self.pformat_layers.append((context_layer, pformatted))
135-
context_list.append(pformatted)
136-
137-
kwargs["context"] = context_list
86+
kwargs["context"] = [
87+
context_layer
88+
for context_layer in context.dicts
89+
if hasattr(context_layer, "items") and context_layer
90+
]
13891
kwargs["context_processors"] = getattr(context, "context_processors", None)
13992
self.templates.append(kwargs)
14093

@@ -167,6 +120,64 @@ def enable_instrumentation(self):
167120
def disable_instrumentation(self):
168121
template_rendered.disconnect(self._store_template_info)
169122

123+
def process_context_list(self, context_layers):
124+
context_list = []
125+
for context_layer in context_layers:
126+
# Check if the layer is in the cache.
127+
pformatted = None
128+
for key_values, _pformatted in self.pformat_layers:
129+
if key_values == context_layer:
130+
pformatted = _pformatted
131+
break
132+
133+
if pformatted is None:
134+
temp_layer = {}
135+
for key, value in context_layer.items():
136+
# Do not force evaluating LazyObject
137+
if hasattr(value, "_wrapped"):
138+
# SimpleLazyObject has __repr__ which includes actual value
139+
# if it has been already evaluated
140+
temp_layer[key] = repr(value)
141+
# Replace any request elements - they have a large
142+
# Unicode representation and the request data is
143+
# already made available from the Request panel.
144+
elif isinstance(value, http.HttpRequest):
145+
temp_layer[key] = "<<request>>"
146+
# Replace the debugging sql_queries element. The SQL
147+
# data is already made available from the SQL panel.
148+
elif key == "sql_queries" and isinstance(value, list):
149+
temp_layer[key] = "<<sql_queries>>"
150+
# Replace LANGUAGES, which is available in i18n context
151+
# processor
152+
elif key == "LANGUAGES" and isinstance(value, tuple):
153+
temp_layer[key] = "<<languages>>"
154+
# QuerySet would trigger the database: user can run the
155+
# query from SQL Panel
156+
elif isinstance(value, (QuerySet, RawQuerySet)):
157+
temp_layer[key] = "<<{} of {}>>".format(
158+
value.__class__.__name__.lower(),
159+
value.model._meta.label,
160+
)
161+
else:
162+
token = allow_sql.set(False) # noqa: FBT003
163+
try:
164+
saferepr(value) # this MAY trigger a db query
165+
except SQLQueryTriggered:
166+
temp_layer[key] = "<<triggers database query>>"
167+
except UnicodeEncodeError:
168+
temp_layer[key] = "<<Unicode encode error>>"
169+
except Exception:
170+
temp_layer[key] = "<<unhandled exception>>"
171+
else:
172+
temp_layer[key] = value
173+
finally:
174+
allow_sql.reset(token)
175+
pformatted = pformat(temp_layer)
176+
self.pformat_layers.append((context_layer, pformatted))
177+
context_list.append(pformatted)
178+
179+
return context_list
180+
170181
def generate_stats(self, request, response):
171182
template_context = []
172183
for template_data in self.templates:
@@ -182,8 +193,11 @@ def generate_stats(self, request, response):
182193
info["template"] = template
183194
# Clean up context for better readability
184195
if self.toolbar.config["SHOW_TEMPLATE_CONTEXT"]:
185-
context_list = template_data.get("context", [])
186-
info["context"] = "\n".join(context_list)
196+
if "context_list" not in template_data:
197+
template_data["context_list"] = self.process_context_list(
198+
template_data.get("context", [])
199+
)
200+
info["context"] = "\n".join(template_data["context_list"])
187201
template_context.append(info)
188202

189203
# Fetch context_processors/template_dirs from any template

docs/changes.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ Pending
66

77
* Removed outdated third-party panels from the list.
88
* Avoided the unnecessary work of recursively quoting SQL parameters.
9+
* Postponed context process in templates panel to include lazy evaluated
10+
content.
11+
* Fixed template panel to avoid evaluating ``LazyObject`` when not already
12+
evaluated.
913

1014
4.2.0 (2023-08-10)
1115
------------------

tests/panels/test_template.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from django.contrib.auth.models import User
33
from django.template import Context, RequestContext, Template
44
from django.test import override_settings
5+
from django.utils.functional import SimpleLazyObject
56

67
from ..base import BaseTestCase, IntegrationTestCase
78
from ..forms import TemplateReprForm
@@ -21,6 +22,7 @@ def tearDown(self):
2122
super().tearDown()
2223

2324
def test_queryset_hook(self):
25+
response = self.panel.process_request(self.request)
2426
t = Template("No context variables here!")
2527
c = Context(
2628
{
@@ -29,12 +31,13 @@ def test_queryset_hook(self):
2931
}
3032
)
3133
t.render(c)
34+
self.panel.generate_stats(self.request, response)
3235

3336
# ensure the query was NOT logged
3437
self.assertEqual(len(self.sql_panel._queries), 0)
3538

3639
self.assertEqual(
37-
self.panel.templates[0]["context"],
40+
self.panel.templates[0]["context_list"],
3841
[
3942
"{'False': False, 'None': None, 'True': True}",
4043
"{'deep_queryset': '<<triggers database query>>',\n"
@@ -99,16 +102,34 @@ def test_disabled(self):
99102
self.assertFalse(self.panel.enabled)
100103

101104
def test_empty_context(self):
105+
response = self.panel.process_request(self.request)
102106
t = Template("")
103107
c = Context({})
104108
t.render(c)
109+
self.panel.generate_stats(self.request, response)
105110

106111
# Includes the builtin context but not the empty one.
107112
self.assertEqual(
108-
self.panel.templates[0]["context"],
113+
self.panel.templates[0]["context_list"],
109114
["{'False': False, 'None': None, 'True': True}"],
110115
)
111116

117+
def test_lazyobject(self):
118+
response = self.panel.process_request(self.request)
119+
t = Template("")
120+
c = Context({"lazy": SimpleLazyObject(lambda: "lazy_value")})
121+
t.render(c)
122+
self.panel.generate_stats(self.request, response)
123+
self.assertNotIn("lazy_value", self.panel.content)
124+
125+
def test_lazyobject_eval(self):
126+
response = self.panel.process_request(self.request)
127+
t = Template("{{lazy}}")
128+
c = Context({"lazy": SimpleLazyObject(lambda: "lazy_value")})
129+
self.assertEqual(t.render(c), "lazy_value")
130+
self.panel.generate_stats(self.request, response)
131+
self.assertIn("lazy_value", self.panel.content)
132+
112133

113134
@override_settings(
114135
DEBUG=True, DEBUG_TOOLBAR_PANELS=["debug_toolbar.panels.templates.TemplatesPanel"]

0 commit comments

Comments
 (0)