Skip to content

Commit 3f179a8

Browse files
authored
parse richInspectVariables result with ast.literal_eval (#1572)
When the kernel is stopped on a breakpoint, richInspectVariables asks debugpy to evaluate display_formatter.format on the variable and gets the answer back as a string, the repr of a (data, metadata) tuple. That string is then handed to eval to turn it back into two dicts. The bundle values are whatever the object's formatters returned, so any value whose repr is not a plain literal gets executed as an expression a second time, in the control thread, instead of being read as data. A _repr_mimebundle_ that returns an object with a repr of len('abc') comes back from the handler as 3. I ran into it reading through the debug handlers. Parsing the result with ast.literal_eval keeps the normal case identical and leaves the representation empty when the string is not a literal, the same way the non-breakpoint branch does when user_expressions fails. The change stays inside richInspectVariables, with a regression test next to the existing breakpoint one.
2 parents b28a88a + f8abe97 commit 3f179a8

2 files changed

Lines changed: 70 additions & 1 deletion

File tree

‎ipykernel/debugger.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Debugger implementation for the IPython kernel."""
22

3+
import ast
34
import os
45
import re
56
import sys
@@ -665,7 +666,12 @@ async def richInspectVariables(self, message):
665666
}
666667
)
667668
if reply["success"]:
668-
repr_data, repr_metadata = eval(reply["body"]["result"], {}, {})
669+
# The result is the repr of a (data, metadata) tuple, read it
670+
# as a literal so nothing in it gets evaluated here.
671+
try:
672+
repr_data, repr_metadata = ast.literal_eval(reply["body"]["result"])
673+
except (ValueError, TypeError, SyntaxError):
674+
self.log.debug("Could not parse rich representation of %s", var_name)
669675

670676
body = {
671677
"data": repr_data,

‎tests/test_debugger.py‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,69 @@ def test_rich_inspect_at_breakpoint(kernel_with_debug):
361361
assert reply["body"]["data"] == {"text/plain": locals_[0]["value"]}
362362

363363

364+
def test_rich_inspect_at_breakpoint_non_literal_repr(kernel_with_debug):
365+
# The rich representation comes back from debugpy as a string. A value in
366+
# it whose repr is an expression must be read as data, not evaluated.
367+
code = """class Expr:
368+
def __repr__(self):
369+
return "len('abc')"
370+
371+
class Rich:
372+
def _repr_mimebundle_(self, include=None, exclude=None):
373+
return {"text/plain": Expr()}
374+
375+
def f(a):
376+
b = a
377+
return b
378+
379+
f(Rich())"""
380+
381+
r = wait_for_debug_request(kernel_with_debug, "dumpCell", {"code": code})
382+
if debugpy:
383+
source = r["body"]["sourcePath"]
384+
else:
385+
assert r == {}
386+
source = "some path"
387+
388+
wait_for_debug_request(
389+
kernel_with_debug,
390+
"setBreakpoints",
391+
{
392+
"breakpoints": [{"line": 10}],
393+
"source": {"path": source},
394+
"sourceModified": False,
395+
},
396+
)
397+
398+
wait_for_debug_request(kernel_with_debug, "debugInfo")
399+
400+
wait_for_debug_request(kernel_with_debug, "configurationDone")
401+
402+
kernel_with_debug.execute(code)
403+
404+
if not debugpy:
405+
# Cannot stop on breakpoint if debugpy not installed
406+
return
407+
408+
# Wait for stop on breakpoint
409+
msg: dict = {"msg_type": "", "content": {}}
410+
while msg.get("msg_type") != "debug_event" or msg["content"].get("event") != "stopped":
411+
msg = kernel_with_debug.get_iopub_msg(timeout=TIMEOUT)
412+
413+
stacks = wait_for_debug_request(kernel_with_debug, "stackTrace", {"threadId": 1})["body"][
414+
"stackFrames"
415+
]
416+
417+
reply = wait_for_debug_request(
418+
kernel_with_debug,
419+
"richInspectVariables",
420+
{"variableName": "a", "frameId": stacks[0]["id"]},
421+
)
422+
423+
assert reply["success"]
424+
assert reply["body"] == {"data": {}, "metadata": {}}
425+
426+
364427
def test_convert_to_long_pathname():
365428
if sys.platform == "win32":
366429
from ipykernel.compiler import _convert_to_long_pathname

0 commit comments

Comments
 (0)