Skip to content

Commit ccdf705

Browse files
living180tim-schilling
authored andcommitted
Format frame locals when captured
Before this commit, if ENABLE_STACKTRACES_LOCALS was True, each stack frame's locals dict would be stored directly in the captured stack trace, and only converted to a string once the stack trace was rendered. This means that for panels, such as the SQL Panel, which do not render the stack traces until later after they have all been captured, it is quite possible that the values in the locals dicts at the point when the stack trace was rendered would no longer be the same as they were at the time when the stack trace was captured, resulting in incorrect values being displayed to the user. Fix by converting the locals dict to a string immediately when it is captured (in the get_stack_trace() function as well as in the deprecated tidy_stacktrace() function) instead of in the render_stacktrace() function.
1 parent 5b4450a commit ccdf705

File tree

4 files changed

+38
-4
lines changed

4 files changed

+38
-4
lines changed

debug_toolbar/utils.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import os.path
44
import sys
55
import warnings
6-
from pprint import pformat
6+
from pprint import PrettyPrinter, pformat
77
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
88

99
from asgiref.local import Local
@@ -62,7 +62,7 @@ def tidy_stacktrace(stack: List[stubs.InspectStack]) -> stubs.TidyStackTrace:
6262
continue
6363
text = "".join(text).strip() if text else ""
6464
frame_locals = (
65-
frame.f_locals
65+
pformat(frame.f_locals)
6666
if dt_settings.get_config()["ENABLE_STACKTRACES_LOCALS"]
6767
else None
6868
)
@@ -99,7 +99,7 @@ def render_stacktrace(trace: stubs.TidyStackTrace) -> SafeString:
9999
if show_locals:
100100
html += format_html(
101101
' <pre class="djdt-locals">{}</pre>\n',
102-
pformat(locals_),
102+
locals_,
103103
)
104104
html += "\n"
105105
return mark_safe(html)
@@ -266,6 +266,8 @@ def _stack_frames(*, skip=0):
266266

267267

268268
class _StackTraceRecorder:
269+
pretty_printer = PrettyPrinter()
270+
269271
def __init__(self):
270272
self.filename_cache = {}
271273

@@ -315,7 +317,10 @@ def get_stack_trace(
315317
else:
316318
source_line = ""
317319

318-
frame_locals = frame.f_locals if include_locals else None
320+
if include_locals:
321+
frame_locals = self.pretty_printer.pformat(frame.f_locals)
322+
else:
323+
frame_locals = None
319324

320325
trace.append((filename, line_no, func_name, source_line, frame_locals))
321326
trace.reverse()

docs/changes.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ Pending
1515
environments (but not others). The maintainers judged that time and effort is
1616
better invested elsewhere.
1717
* Added support for psycopg3.
18+
* When ``ENABLE_STACKTRACE_LOCALS`` is ``True``, the stack frames' locals dicts
19+
will be converted to strings when the stack trace is captured rather when it
20+
is rendered, so that the correct values will be displayed in the rendered
21+
stack trace, as they may have changed between the time the stack trace was
22+
captured and when it is rendered.
1823

1924
3.8.1 (2022-12-03)
2025
------------------

docs/spelling_wordlist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ backends
33
backported
44
checkbox
55
contrib
6+
dicts
67
django
78
fallbacks
89
flamegraph

tests/test_utils.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,26 @@ def test_deprecated_functions(self):
8080
with self.assertWarns(DeprecationWarning):
8181
stack_trace = tidy_stacktrace(reversed(stack))
8282
self.assertEqual(stack_trace[-1][0], __file__)
83+
84+
@override_settings(DEBUG_TOOLBAR_CONFIG={"ENABLE_STACKTRACES_LOCALS": True})
85+
def test_locals(self):
86+
# This wrapper class is necessary to mask the repr() of the list
87+
# returned by get_stack_trace(); otherwise the 'test_locals_value_1'
88+
# string will also be present in rendered_stack_2.
89+
class HideRepr:
90+
def __init__(self, value):
91+
self.value = value
92+
93+
x = "test_locals_value_1"
94+
stack_1_wrapper = HideRepr(get_stack_trace())
95+
96+
x = x.replace("1", "2")
97+
stack_2_wrapper = HideRepr(get_stack_trace())
98+
99+
rendered_stack_1 = render_stacktrace(stack_1_wrapper.value)
100+
self.assertIn("test_locals_value_1", rendered_stack_1)
101+
self.assertNotIn("test_locals_value_2", rendered_stack_1)
102+
103+
rendered_stack_2 = render_stacktrace(stack_2_wrapper.value)
104+
self.assertNotIn("test_locals_value_1", rendered_stack_2)
105+
self.assertIn("test_locals_value_2", rendered_stack_2)

0 commit comments

Comments
 (0)