diff --git a/pudb/debugger.py b/pudb/debugger.py index dc75b33e..860b631e 100644 --- a/pudb/debugger.py +++ b/pudb/debugger.py @@ -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() @@ -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 @@ -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")]) @@ -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, @@ -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) @@ -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 diff --git a/pudb/var_view.py b/pudb/var_view.py index d330da3d..45a1d5ba 100644 --- a/pudb/var_view.py +++ b/pudb/var_view.py @@ -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 @@ -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()) @@ -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: @@ -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 @@ -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() # }}} diff --git a/test/test_var_view.py b/test/test_var_view.py index e263a3a9..239d1771 100644 --- a/test/test_var_view.py +++ b/test/test_var_view.py @@ -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