Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add global and "by reference" watches #525

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 90 additions & 36 deletions pudb/debugger.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ def setup_state(self):
def restart(self):
from linecache import checkcache
checkcache()
self.ui.reset_global_watch_values()
self.ui.set_source_code_provider(NullSourceCodeProvider())
self.setup_state()

Expand Down Expand Up @@ -1024,13 +1025,59 @@ def change_var_state(w, size, key):
elif key == "m":
iinfo.show_methods = not iinfo.show_methods
elif key == "delete":
fvi = self.get_frame_var_info(read_only=False)
for i, watch_expr in enumerate(fvi.watches):
if watch_expr is var.watch_expr:
del fvi.watches[i]
self.delete_watch(var.watch_expr)

self.update_var_view(focus_index=focus_index)

def _watch_editors(watch_expr):
"""
Create widgets for editing the given expression.
"""
def set_watch_scope(radio_button, new_state, user_data):
if new_state:
watch_expr.set_scope(user_data)

def set_watch_method(radio_button, new_state, user_data):
if new_state:
watch_expr.set_method(user_data)

watch_edit = urwid.Edit([("label", "Watch expression: ")],
watch_expr.expression)

scope_rbs = []
urwid.RadioButton(
group=scope_rbs,
label="Local: watch in current frame only",
state=watch_expr.scope == "local",
on_state_change=set_watch_scope,
user_data="local",
)
urwid.RadioButton(
group=scope_rbs,
label="Global: watch in all frames",
state=watch_expr.scope == "global",
on_state_change=set_watch_scope,
user_data="global",
)

method_rbs = []
urwid.RadioButton(
group=method_rbs,
label="Expression: always re-evaluate the expression",
state=watch_expr.method == "expression",
on_state_change=set_watch_method,
user_data="expression",
)
urwid.RadioButton(
group=method_rbs,
label="Reference: evaluate once, watch the resulting value",
state=watch_expr.method == "reference",
on_state_change=set_watch_method,
user_data="reference",
)

return watch_edit, scope_rbs, method_rbs

def edit_inspector_detail(w, size, key):
var = self.var_list._w.focus

Expand All @@ -1046,13 +1093,17 @@ def edit_inspector_detail(w, size, key):
]

if var.watch_expr is not None:
watch_edit = urwid.Edit([
("label", "Watch expression: ")
], var.watch_expr.expression)
watch_edit, scope_rbs, method_rbs = _watch_editors(var.watch_expr)
id_segment = [
urwid.AttrMap(watch_edit, "input", "focused input"),
urwid.Text(""),
]
urwid.AttrMap(watch_edit, "input", "focused input"),
urwid.Text(""),
urwid.Text("Scope:"),
] + scope_rbs + [
urwid.Text(""),
urwid.Text("Method:"),
] + method_rbs + [
urwid.Text("")
]

buttons.extend([None, ("Delete", "del")])

Expand Down Expand Up @@ -1100,8 +1151,11 @@ def edit_inspector_detail(w, size, key):

lb = urwid.ListBox(urwid.SimpleListWalker(
id_segment
+ [urwid.Text("Stringifier:")]
+ rb_grp_show + [urwid.Text("")]
+ [urwid.Text("Access:")]
+ rb_grp_access + [urwid.Text("")]
+ [urwid.Text("Options:")]
+ [
wrap_checkbox,
expanded_checkbox,
Expand Down Expand Up @@ -1140,33 +1194,36 @@ def edit_inspector_detail(w, size, key):
iinfo.access_level = "all"

if var.watch_expr is not None:
var.watch_expr.expression = watch_edit.get_edit_text()
var.watch_expr.set_expression(watch_edit.get_edit_text())
self.change_watch_scope(var.watch_expr, fvi)

elif result == "del":
for i, watch_expr in enumerate(fvi.watches):
if watch_expr is var.watch_expr:
del fvi.watches[i]
self.delete_watch(var.watch_expr, fvi)

self.update_var_view()

def insert_watch(w, size, key):
watch_edit = urwid.Edit([
("label", "Watch expression: ")
])
from pudb.var_view import WatchExpression
watch_expr = WatchExpression()
watch_edit, scope_rbs, method_rbs = _watch_editors(watch_expr)

if self.dialog(
urwid.ListBox(urwid.SimpleListWalker([
urwid.AttrMap(watch_edit, "input", "focused input")
])),
[
("OK", True),
("Cancel", False),
], title="Add Watch Expression"):

from pudb.var_view import WatchExpression
we = WatchExpression(watch_edit.get_edit_text())
fvi = self.get_frame_var_info(read_only=False)
fvi.watches.append(we)
urwid.ListBox(urwid.SimpleListWalker([
urwid.AttrMap(watch_edit, "input", "focused input"),
urwid.Text(""),
urwid.Text("Scope:"),
] + scope_rbs + [
urwid.Text(""),
urwid.Text("Method:"),
] + method_rbs)),
[
("OK", True),
("Cancel", False),
],
title="Add Watch Expression"
):
watch_expr.expression = watch_edit.get_edit_text()
self.add_watch(watch_expr)
self.update_var_view()

self.var_list.listen("\\", change_var_state)
Expand Down Expand Up @@ -2843,16 +2900,13 @@ def set_current_line(self, line, source_code_provider):
self.current_line = self.source[line]
self.current_line.set_current(True)

def update_var_view(self, locals=None, globals=None, focus_index=None):
if locals is None:
locals = self.debugger.curframe.f_locals
if globals is None:
globals = self.debugger.curframe.f_globals

def update_var_view(self, focus_index=None):
from pudb.var_view import make_var_view
self.locals[:] = make_var_view(
self.global_watches,
self.get_frame_var_info(read_only=True),
locals, globals)
self.debugger.curframe.f_globals,
self.debugger.curframe.f_locals)
if focus_index is not None:
# Have to set the focus _after_ updating the locals list, as there
# appears to be a brief moment while reseting the list when the
Expand Down
127 changes: 112 additions & 15 deletions pudb/var_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

from abc import ABC, abstractmethod
from collections.abc import Callable, Sized
from itertools import chain
from typing import Tuple, List
from pudb.lowlevel import ui_log
from pudb.ui_tools import text_width
Expand Down Expand Up @@ -169,11 +170,14 @@ def length(cls, mapping):
# {{{ data

class FrameVarInfo:
def __init__(self):
def __init__(self, global_watch_iinfo):
self.id_path_to_iinfo = {}
self.watches = []
self.global_watch_iinfo = global_watch_iinfo

def get_inspect_info(self, id_path, read_only):
if id_path in self.global_watch_iinfo:
return self.global_watch_iinfo[id_path]
if read_only:
return self.id_path_to_iinfo.get(
id_path, InspectInfo())
Expand All @@ -197,8 +201,60 @@ def __init__(self):


class WatchExpression:
def __init__(self, expression):
NOT_EVALUATED = object()

def __init__(self, expression="", scope="local", method="expression"):
self.expression = expression
self.scope = scope
self.method = method
self._value = self.NOT_EVALUATED

def id_path(self):
return str(id(self))

def eval(self, frame_globals, frame_locals):
if (self.method == "expression"
or self._value is self.NOT_EVALUATED):
try:
self._value = eval(self.expression, frame_globals, frame_locals)
except Exception:
return WatchEvalError()
return self._value

def label(self, value, frame_globals, frame_locals):
scope_str = self.scope[0]
expression = self.expression
if self.method == "reference":
found = False
# locals first as that's the context the user is more likely to be
# interested in re: seeing renames.
for mapping in (frame_locals, frame_globals):
for k, v in mapping.items():
if v is value:
expression = f"{expression} ({k})"
found = True
break
if found:
break
method_str = "*"
else:
method_str = "="
return f"[{scope_str}{method_str}] {expression}"

def set_expression(self, expression):
if expression != self.expression:
self.expression = expression
self._value = self.NOT_EVALUATED

def set_method(self, method):
self.method = method
self._value = self.NOT_EVALUATED

def set_scope(self, scope):
self.scope = scope

def reset_value(self):
self._value = self.NOT_EVALUATED


class WatchEvalError:
Expand Down Expand Up @@ -717,30 +773,28 @@ def add_item(self, parent, var_label, value_str, id_path, attr_prefix=None):
SEPARATOR = urwid.AttrMap(urwid.Text(""), "variable separator")


def make_var_view(frame_var_info, locals, globals):
vars = list(locals.keys())
def make_var_view(global_watches, frame_var_info, frame_globals, frame_locals):
vars = list(frame_locals.keys())
vars.sort(key=str.lower)

tmv_walker = TopAndMainVariableWalker(frame_var_info)
ret_walker = BasicValueWalker(frame_var_info)
watch_widget_list = []

for watch_expr in frame_var_info.watches:
try:
value = eval(watch_expr.expression, globals, locals)
except Exception:
value = WatchEvalError()

for watch_expr in chain(global_watches, frame_var_info.watches):
value = watch_expr.eval(frame_globals, frame_locals)
label = watch_expr.label(value, frame_globals, frame_locals)
id_path = watch_expr.id_path()
WatchValueWalker(frame_var_info, watch_widget_list, watch_expr) \
.walk_value(None, watch_expr.expression, value)
.walk_value(None, label, value, id_path)

if "__return__" in vars:
ret_walker.walk_value(None, "Return", locals["__return__"],
ret_walker.walk_value(None, "Return", frame_locals["__return__"],
attr_prefix="return")

for var in vars:
if not (var.startswith("__") and var.endswith("__")):
tmv_walker.walk_value(None, var, locals[var])
tmv_walker.walk_value(None, var, frame_locals[var])

result = tmv_walker.main_widget_list

Expand All @@ -759,15 +813,58 @@ def make_var_view(frame_var_info, locals, globals):
class FrameVarInfoKeeper:
def __init__(self):
self.frame_var_info = {}
self.global_watches = []

# In order to have the global watch expression presented the same way in
# all frames, we need persistent storage for global InspectInfo.
self.global_watch_iinfo = {}

def get_frame_var_info(self, read_only, ssid=None):
if ssid is None:
# self.debugger set by subclass
ssid = self.debugger.get_stack_situation_id() # noqa: E501 # pylint: disable=no-member
if read_only:
return self.frame_var_info.get(ssid, FrameVarInfo())
return self.frame_var_info.get(
ssid,
FrameVarInfo(self.global_watch_iinfo),
)
else:
return self.frame_var_info.setdefault(ssid, FrameVarInfo())
return self.frame_var_info.setdefault(
ssid,
FrameVarInfo(self.global_watch_iinfo),
)

def add_watch(self, watch_expr: WatchExpression, fvi=None):
if watch_expr.scope == "local":
if fvi is None:
fvi = self.get_frame_var_info(read_only=False)
fvi.watches.append(watch_expr)
elif watch_expr.scope == "global":
self.global_watches.append(watch_expr)
self.global_watch_iinfo[watch_expr.id_path()] = InspectInfo()

def delete_watch(self, watch_expr: WatchExpression, fvi=None):
if fvi is None:
fvi = self.get_frame_var_info(read_only=False)
# Need to delete both locally and globally- could be in either!
# (The watch_expr.scope attribute may have changed)
try:
fvi.watches.remove(watch_expr)
except ValueError:
pass
try:
self.global_watches.remove(watch_expr)
self.global_watch_iinfo.pop(watch_expr.id_path())
except ValueError:
pass

def change_watch_scope(self, watch_expr, fvi=None):
self.delete_watch(watch_expr, fvi)
self.add_watch(watch_expr, fvi)

def reset_global_watch_values(self):
for watch_expr in self.global_watches:
watch_expr.reset_value()

# }}}

Expand Down
2 changes: 1 addition & 1 deletion test/test_var_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def test_get_stringifier():

class FrameVarInfoForTesting(FrameVarInfo):
def __init__(self, paths_to_expand=None):
super().__init__()
super().__init__(global_watch_iinfo={})
if paths_to_expand is None:
paths_to_expand = set()
self.paths_to_expand = paths_to_expand
Expand Down