feat(debug): enhance type hinting in gdb and Python debug visualizers
This commit is contained in:
@@ -28,10 +28,28 @@ This mirrors debuggers/gdb_debug_graph.py's auto-discovery: it's what lets
|
||||
show the whole list, because `lst` (a sibling local) gets auto-walked too.
|
||||
"""
|
||||
|
||||
import builtins
|
||||
import json
|
||||
import sys
|
||||
import types
|
||||
|
||||
# On gdb (C++/Rust), typing bare `root` into the Watch box happens to work
|
||||
# as a throwaway "just show me the locals/globals" expression -- gdb
|
||||
# resolves it to a real (if coincidental) always-null `void*` symbol from
|
||||
# libc.so.6, so $debug_graph(root) contributes no primary node of its own
|
||||
# but still triggers full auto-discovery of the frame's locals/globals.
|
||||
# debugpy has no such accident to lean on -- evaluating a name that isn't
|
||||
# actually bound anywhere raises NameError before our code ever runs -- so
|
||||
# this deliberately recreates the same "always-resolvable, always-None"
|
||||
# spelling as a builtin. This runs as an import-time side effect, but that's
|
||||
# safe to rely on even the *first* time someone types `root`: evaluating a
|
||||
# call expression like `__import__('py_debug_graph').debug_graph(root)`
|
||||
# resolves the callable (which imports this module and runs this line)
|
||||
# before it evaluates the `root` argument, so the patch is always in place
|
||||
# by the time `root` itself gets looked up.
|
||||
if not hasattr(builtins, "root"):
|
||||
builtins.root = None
|
||||
|
||||
|
||||
def _addr_str(obj_id):
|
||||
return hex(obj_id)
|
||||
@@ -67,9 +85,15 @@ def _append_field(fields_out, name, value, is_pointer=False, type_hint=None):
|
||||
fields_out.append(field)
|
||||
|
||||
|
||||
def _describe_struct(val, node_id, nodes, edges, visited):
|
||||
def _fields_from_items(items, node_id, nodes, edges, visited):
|
||||
"""Builds one field row per (name, value) pair, recursing into
|
||||
describable values as edges exactly like a struct's own members --
|
||||
shared between _describe_struct's vars(obj).items() and the
|
||||
globals()-as-root special case's plain dict.items() below, so a
|
||||
"container of named values" only has one field-building rule
|
||||
regardless of whether the container is an object or a dict."""
|
||||
fields_out = []
|
||||
for name, member_val in vars(val).items():
|
||||
for name, member_val in items:
|
||||
if _is_describable(member_val):
|
||||
member_id = id(member_val)
|
||||
_append_field(fields_out, name, _addr_str(member_id), is_pointer=True, type_hint=_type_name(member_val))
|
||||
@@ -78,8 +102,16 @@ def _describe_struct(val, node_id, nodes, edges, visited):
|
||||
visited.add(member_id)
|
||||
_describe_struct(member_val, _addr_str(member_id), nodes, edges, visited)
|
||||
else:
|
||||
_append_field(fields_out, name, _format_scalar(member_val))
|
||||
nodes[node_id] = fields_out
|
||||
# Every field gets a type hint, not just the describable ones --
|
||||
# a plain int/str/None member is just as worth labeling as a
|
||||
# pointer-like one.
|
||||
_append_field(fields_out, name, _format_scalar(member_val), type_hint=_type_name(member_val))
|
||||
return fields_out
|
||||
|
||||
|
||||
def _describe_struct(val, node_id, nodes, edges, visited):
|
||||
fields_out = _fields_from_items(vars(val).items(), node_id, nodes, edges, visited)
|
||||
nodes[node_id] = {"fields": fields_out, "type": _type_name(val)}
|
||||
|
||||
|
||||
def _walk_primary_root(root_val, nodes, edges, visited):
|
||||
@@ -89,7 +121,13 @@ def _walk_primary_root(root_val, nodes, edges, visited):
|
||||
_describe_struct(root_val, node_id, nodes, edges, visited)
|
||||
return node_id
|
||||
if root_val is not None:
|
||||
nodes["root"] = [{"name": "value", "value": _format_scalar(root_val), "isPointer": False}]
|
||||
nodes["root"] = {
|
||||
"fields": [{
|
||||
"name": "value", "value": _format_scalar(root_val), "isPointer": False,
|
||||
"typeHint": _type_name(root_val),
|
||||
}],
|
||||
"type": _type_name(root_val),
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
@@ -135,7 +173,26 @@ def build_graph_json(root_val, watched_roots, frame):
|
||||
skip_ids = set()
|
||||
used_names = set()
|
||||
|
||||
primary_id = _walk_primary_root(root_val, nodes, edges, visited)
|
||||
if frame is not None and root_val is globals:
|
||||
# Bare `globals` (no parens, e.g. typing "globals" into the Watch
|
||||
# box) evaluates to the *builtin function itself*, not its result --
|
||||
# walked normally that's just an opaque "<function globals at ...>"
|
||||
# scalar. Calling it here would also be wrong: globals() returns
|
||||
# whatever frame *calls* it, which would be this module's own
|
||||
# globals, not the paused frame's. Substituting the frame we
|
||||
# already have (passed in from debug_graph()'s sys._getframe(1))
|
||||
# is what actually shows the debuggee's global variables. Filtered
|
||||
# through the same _skip_global used for auto-discovered globals
|
||||
# below, so this node shows the same clean set, not every dunder,
|
||||
# module and class in the namespace.
|
||||
primary_id = "globals()"
|
||||
fields_out = _fields_from_items(
|
||||
[(k, v) for k, v in frame.f_globals.items() if not _skip_global(k, v)],
|
||||
primary_id, nodes, edges, visited,
|
||||
)
|
||||
nodes[primary_id] = {"fields": fields_out, "type": "dict"}
|
||||
else:
|
||||
primary_id = _walk_primary_root(root_val, nodes, edges, visited)
|
||||
if primary_id is not None:
|
||||
skip_ids.add(primary_id)
|
||||
|
||||
@@ -166,7 +223,12 @@ def build_graph_json(root_val, watched_roots, frame):
|
||||
used_names.add(name)
|
||||
_add_named_root(name, val, "global", nodes, edges, visited, roots, skip_ids)
|
||||
|
||||
nodes_json = [{"id": nid, "fields": fields} for nid, fields in nodes.items()]
|
||||
nodes_json = [
|
||||
{"id": nid, "fields": node["fields"], "type": node["type"]}
|
||||
if node.get("type")
|
||||
else {"id": nid, "fields": node["fields"]}
|
||||
for nid, node in nodes.items()
|
||||
]
|
||||
edges_json = [
|
||||
{"from": src, "to": dst, "label": label}
|
||||
for (src, dst, label) in edges
|
||||
|
||||
Reference in New Issue
Block a user