From d5e32fab0f5cb4b15ac3cee0b43ec9228ba7fe78 Mon Sep 17 00:00:00 2001 From: Aleksey Maksimov Date: Wed, 24 Jun 2015 23:47:14 +0800 Subject: [PATCH 01/13] Added auto save/load of watch expressions --- pudb/debugger.py | 12 ++++++++++++ pudb/settings.py | 33 +++++++++++++++++++++++++++++++++ pudb/var_view.py | 14 ++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/pudb/debugger.py b/pudb/debugger.py index 8a9c07ad..8a823952 100644 --- a/pudb/debugger.py +++ b/pudb/debugger.py @@ -1157,6 +1157,18 @@ def edit_inspector_detail(w, size, key): var.watch_expr.expression = watch_edit.get_edit_text() elif result == "del": + from pudb.settings import load_watches, save_watches + stored_expressions = [expr.strip() for expr in load_watches()] + + # Remove saved expression + for i, stored_expr in enumerate(stored_expressions): + if stored_expr == var.watch_expr.expression: + del stored_watches[i] + + # Save it here because self.update_var_view() is going to + # read saved watches again + save_watches(stored_watches) + for i, watch_expr in enumerate(fvi.watches): if watch_expr is var.watch_expr: del fvi.watches[i] diff --git a/pudb/settings.py b/pudb/settings.py index c4f4e153..25363928 100644 --- a/pudb/settings.py +++ b/pudb/settings.py @@ -62,6 +62,7 @@ def get_save_config_path(): SAVED_BREAKPOINTS_FILE_NAME = "saved-breakpoints-%d.%d" % sys.version_info[:2] BREAKPOINTS_FILE_NAME = "breakpoints-%d.%d" % sys.version_info[:2] +SAVED_WATCHES_FILE_NAME = "saved-watches-%d.%d" % sys.version_info[:2] _config_ = [None] @@ -618,4 +619,36 @@ def save_breakpoints(bp_list): # }}} + +def get_watches_file_name(): + from os.path import join + return join(get_save_config_path(), SAVED_WATCHES_FILE_NAME) + + +def save_watches(w_list): + """ + :arg w_list: a list of strings + """ + + try: + with open(get_watches_file_name(), 'w+') as histfile: + for watch in w_list: + histfile.write(watch + '\n') + except: + pass + + +def load_watches(): + if os.path.exists(get_watches_file_name()): + try: + with open(get_watches_file_name(), 'r') as histfile: + watches = histfile.readlines() + for line in watches: + line = line.strip() + return watches + except: + pass + + return [] + # vim:foldmethod=marker diff --git a/pudb/var_view.py b/pudb/var_view.py index 4048ed8c..7c622105 100644 --- a/pudb/var_view.py +++ b/pudb/var_view.py @@ -727,6 +727,20 @@ def make_var_view(frame_var_info, locals, globals): ret_walker = BasicValueWalker(frame_var_info) watch_widget_list = [] + from pudb.settings import load_watches, save_watches + stored_expressions = [expr.strip() for expr in load_watches()] + + # As watch expressions are stored in a list, simply appending stored + # expressions to that list will add duplicates. This part is to avoid that. + from pudb.var_view import WatchExpression + existing_expressions = [expr.expression for expr in frame_var_info.watches] + for stored_expr in stored_expressions: + if stored_expr not in existing_expressions: + frame_var_info.watches.append(WatchExpression(stored_expr)) + + # Save watches because new ones may have added to a list + save_watches([expr.expression for expr in frame_var_info.watches]) + for watch_expr in frame_var_info.watches: try: value = eval(watch_expr.expression, globals, locals) From ea7c0eff5f5677f48adcbf3eaa1153b96b5ac9a4 Mon Sep 17 00:00:00 2001 From: Aleksey Maksimov Date: Mon, 11 Dec 2017 21:27:49 +0800 Subject: [PATCH 02/13] add option in Preferences to keep watches (default is Off) --- pudb/debugger.py | 24 +++++++++++++----------- pudb/settings.py | 26 ++++++++++++++++++++++++++ pudb/var_view.py | 29 ++++++++++++++++------------- 3 files changed, 55 insertions(+), 24 deletions(-) diff --git a/pudb/debugger.py b/pudb/debugger.py index 8a823952..a9f39c11 100644 --- a/pudb/debugger.py +++ b/pudb/debugger.py @@ -1157,17 +1157,19 @@ def edit_inspector_detail(w, size, key): var.watch_expr.expression = watch_edit.get_edit_text() elif result == "del": - from pudb.settings import load_watches, save_watches - stored_expressions = [expr.strip() for expr in load_watches()] - - # Remove saved expression - for i, stored_expr in enumerate(stored_expressions): - if stored_expr == var.watch_expr.expression: - del stored_watches[i] - - # Save it here because self.update_var_view() is going to - # read saved watches again - save_watches(stored_watches) + if CONFIG["persist_watches"]: + from pudb.settings import load_watches, save_watches + stored_expressions = [expr.strip() + for expr in load_watches()] + + # Remove saved expression + for i, stored_expr in enumerate(stored_expressions): + if stored_expr == var.watch_expr.expression: + del stored_expressions[i] + + # Save it here because self.update_var_view() is going to + # read saved watches again + save_watches(stored_expressions) for i, watch_expr in enumerate(fvi.watches): if watch_expr is var.watch_expr: diff --git a/pudb/settings.py b/pudb/settings.py index 25363928..3a0cfcf2 100644 --- a/pudb/settings.py +++ b/pudb/settings.py @@ -111,6 +111,8 @@ def load_config(): conf_dict.setdefault("wrap_variables", "True") conf_dict.setdefault("default_variables_access_level", "public") + conf_dict.setdefault("persist_watches", False) + conf_dict.setdefault("display", "auto") conf_dict.setdefault("prompt_on_quit", "True") @@ -135,6 +137,7 @@ def normalize_bool_inplace(name): normalize_bool_inplace("line_numbers") normalize_bool_inplace("wrap_variables") + normalize_bool_inplace("persist_watches") normalize_bool_inplace("prompt_on_quit") normalize_bool_inplace("hide_cmdline_win") @@ -197,6 +200,9 @@ def _update_default_variables_access_level(): def _update_wrap_variables(): ui.update_var_view() + def _update_persist_watches(): + pass + def _update_config(check_box, new_state, option_newvalue): option, newvalue = option_newvalue new_conf_dict = {option: newvalue} @@ -253,6 +259,11 @@ def _update_config(check_box, new_state, option_newvalue): conf_dict.update(new_conf_dict) _update_wrap_variables() + elif option == "persist_watches": + new_conf_dict["persist_watches"] = not check_box.get_state() + conf_dict.update(new_conf_dict) + _update_persist_watches() + heading = urwid.Text("This is the preferences screen for PuDB. " "Hit Ctrl-P at any time to get back to it.\n\n" "Configuration settings are saved in " @@ -417,6 +428,17 @@ def _update_config(check_box, new_state, option_newvalue): # }}} + # {{{ persist watches + + cb_persist_watches = urwid.CheckBox("Persist watches", + bool(conf_dict["persist_watches"]), on_state_change=_update_config, + user_data=("persist_watches", None)) + + persist_watches_info = urwid.Text("\nKeep watched expressions between " + "debugging sessions.") + + # }}} + # {{{ display display_info = urwid.Text("What driver is used to talk to your terminal. " @@ -470,6 +492,10 @@ def _update_config(check_box, new_state, option_newvalue): + [cb_wrap_variables] + [wrap_variables_info] + + [urwid.AttrMap(urwid.Text("\nPersist Watches:\n"), "group head")] + + [cb_persist_watches] + + [persist_watches_info] + + [urwid.AttrMap(urwid.Text("\nDisplay driver:\n"), "group head")] + [display_info] + display_rbs diff --git a/pudb/var_view.py b/pudb/var_view.py index 7c622105..445acb15 100644 --- a/pudb/var_view.py +++ b/pudb/var_view.py @@ -727,19 +727,22 @@ def make_var_view(frame_var_info, locals, globals): ret_walker = BasicValueWalker(frame_var_info) watch_widget_list = [] - from pudb.settings import load_watches, save_watches - stored_expressions = [expr.strip() for expr in load_watches()] - - # As watch expressions are stored in a list, simply appending stored - # expressions to that list will add duplicates. This part is to avoid that. - from pudb.var_view import WatchExpression - existing_expressions = [expr.expression for expr in frame_var_info.watches] - for stored_expr in stored_expressions: - if stored_expr not in existing_expressions: - frame_var_info.watches.append(WatchExpression(stored_expr)) - - # Save watches because new ones may have added to a list - save_watches([expr.expression for expr in frame_var_info.watches]) + if CONFIG["persist_watches"]: + from pudb.settings import load_watches, save_watches + stored_expressions = [expr.strip() for expr in load_watches()] + + # As watch expressions are stored in a list, simply appending stored + # expressions to that list will add duplicates. This part is to avoid that. + from pudb.var_view import WatchExpression + existing_expressions = [expr.expression + for expr in frame_var_info.watches] + + for stored_expr in stored_expressions: + if stored_expr not in existing_expressions: + frame_var_info.watches.append(WatchExpression(stored_expr)) + + # Save watches because new ones may have added to a list + save_watches([expr.expression for expr in frame_var_info.watches]) for watch_expr in frame_var_info.watches: try: From 9db31aa68252a552e8a3f5d168591a7b56a59b54 Mon Sep 17 00:00:00 2001 From: kprichard Date: Sat, 28 Sep 2024 22:00:03 -0700 Subject: [PATCH 03/13] - var_view.py: import pudb.debugger.CONFIG to local scope, to fix discovered bug after merging lechat:save_load_watches into inducer:main --- pudb/var_view.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pudb/var_view.py b/pudb/var_view.py index 445acb15..6947af4f 100644 --- a/pudb/var_view.py +++ b/pudb/var_view.py @@ -727,6 +727,8 @@ def make_var_view(frame_var_info, locals, globals): ret_walker = BasicValueWalker(frame_var_info) watch_widget_list = [] + from pudb.debugger import CONFIG + if CONFIG["persist_watches"]: from pudb.settings import load_watches, save_watches stored_expressions = [expr.strip() for expr in load_watches()] From 79521448e1fa4e98ba8800a131e9ae326901f202 Mon Sep 17 00:00:00 2001 From: kprichard Date: Sat, 28 Sep 2024 22:08:38 -0700 Subject: [PATCH 04/13] - debugger.py: call save_watches() after a watch expression is deleted by the user, making that item become de-persisted. This fixes an issue where the delete key tends to do nothing in the variables Watch expression subpane, following merge of lechat:save_load_watches into inducer:main. The issue occurs because load_watches() is called every time make_var_view() is invoked by update_var_view(), which itself is invoked frequently -whenever the UI needs redrawing. So, the removal of an expression by the delete key lasts for only approximately two shakes of a lamb's tail. --- pudb/debugger.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pudb/debugger.py b/pudb/debugger.py index a9f39c11..82dfdcc6 100644 --- a/pudb/debugger.py +++ b/pudb/debugger.py @@ -1043,6 +1043,9 @@ def change_var_state(w, size, key): del fvi.watches[i] break + from pudb.settings import save_watches + save_watches([expr.expression for expr in fvi.watches]) + self.update_var_view(focus_index=focus_index) def edit_inspector_detail(w, size, key): From b916e3f3d4926507275df37ec443bada787da429 Mon Sep 17 00:00:00 2001 From: kprichard Date: Wed, 2 Oct 2024 17:10:34 -0700 Subject: [PATCH 05/13] - debugger.py, settings.py, var_view.py: consolidating watch-related code into new class Watches, acting as global container - this resolves the previous dual, competing watch sources: the list FrameVarInfo.watches, plus the file maintained by save_watches() and load_watches(). FrameVarInfo.watches caused apparent non-determinism in the Watches pane, because it was associated with a particular stack frame, so whatever was in .watches on a particular frame would get displayed while the debugger was in that state, but would disappear from the pane when the execution point caused that frame to be pushed or popped. So, in removing FrameVarInfo.watches and consolidating all WatchExpression storage into the single, global container Watches, the watches pane remains consistent no matter the current frame. Caveat that there can be expressions which won't evaluate in the current frame context, of course. That's the trade-off, and I hope that makes sense to other developers. --- pudb/debugger.py | 41 ++++------------- pudb/settings.py | 27 ----------- pudb/var_view.py | 117 ++++++++++++++++++++++++++++++++++++----------- 3 files changed, 100 insertions(+), 85 deletions(-) diff --git a/pudb/debugger.py b/pudb/debugger.py index 82dfdcc6..a5983ca1 100644 --- a/pudb/debugger.py +++ b/pudb/debugger.py @@ -590,7 +590,7 @@ def runcall(self, *args, **kwargs): labelled_value, make_hotkey_markup, ) -from pudb.var_view import FrameVarInfoKeeper +from pudb.var_view import FrameVarInfoKeeper, WatchExpression, Watches # {{{ display setup @@ -1037,14 +1037,7 @@ 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] - break - - from pudb.settings import save_watches - save_watches([expr.expression for expr in fvi.watches]) + Watches.remove(var.watch_expr) self.update_var_view(focus_index=focus_index) @@ -1160,24 +1153,8 @@ def edit_inspector_detail(w, size, key): var.watch_expr.expression = watch_edit.get_edit_text() elif result == "del": - if CONFIG["persist_watches"]: - from pudb.settings import load_watches, save_watches - stored_expressions = [expr.strip() - for expr in load_watches()] - - # Remove saved expression - for i, stored_expr in enumerate(stored_expressions): - if stored_expr == var.watch_expr.expression: - del stored_expressions[i] - - # Save it here because self.update_var_view() is going to - # read saved watches again - save_watches(stored_expressions) - - for i, watch_expr in enumerate(fvi.watches): - if watch_expr is var.watch_expr: - del fvi.watches[i] - break + # Remove saved expression + Watches.remove(var.watch_expr) self.update_var_view() @@ -1195,11 +1172,11 @@ def insert_watch(w, size, key): ("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) - self.update_var_view() + # Add new watch expression, if not empty + watch_text = watch_edit.get_edit_text() + if watch_text and watch_text.strip(): + Watches.add(WatchExpression(watch_text)) + self.update_var_view() self.var_list.listen("\\", change_var_state) self.var_list.listen(" ", change_var_state) diff --git a/pudb/settings.py b/pudb/settings.py index 3a0cfcf2..8eaa5185 100644 --- a/pudb/settings.py +++ b/pudb/settings.py @@ -650,31 +650,4 @@ def get_watches_file_name(): from os.path import join return join(get_save_config_path(), SAVED_WATCHES_FILE_NAME) - -def save_watches(w_list): - """ - :arg w_list: a list of strings - """ - - try: - with open(get_watches_file_name(), 'w+') as histfile: - for watch in w_list: - histfile.write(watch + '\n') - except: - pass - - -def load_watches(): - if os.path.exists(get_watches_file_name()): - try: - with open(get_watches_file_name(), 'r') as histfile: - watches = histfile.readlines() - for line in watches: - line = line.strip() - return watches - except: - pass - - return [] - # vim:foldmethod=marker diff --git a/pudb/var_view.py b/pudb/var_view.py index 6947af4f..800d3905 100644 --- a/pudb/var_view.py +++ b/pudb/var_view.py @@ -30,13 +30,14 @@ import warnings from abc import ABC, abstractmethod from collections.abc import Callable, Sized -from typing import List, Tuple +import os +from typing import List, Tuple, Set import urwid -from pudb.lowlevel import ui_log +from pudb.lowlevel import ui_log, settings_log from pudb.ui_tools import text_width - +from pudb.settings import get_watches_file_name try: import numpy @@ -173,7 +174,6 @@ def length(cls, mapping): class FrameVarInfo: def __init__(self): self.id_path_to_iinfo = {} - self.watches = [] def get_inspect_info(self, id_path, read_only): if read_only: @@ -199,13 +199,39 @@ def __init__(self): class WatchExpression: - def __init__(self, expression): - self.expression = expression + def __init__(self, expression: str): + self._expression = expression.strip() + + @property + def expression(self): + return self._expression + + @expression.setter + def expression(self, value): + self._expression = value.strip() + + def __hash__(self): + return hash(self._expression) + + def __lt__(self, other): + return self._expression < other + + def __gt__(self, other): + return self._expression > other + + def __eq__(self, other): + return self._expression == other + + def __str__(self): + return self._expression + + __repr__ = __str__ class WatchEvalError: def __str__(self): return "" + __repr__ = __str__ # }}} @@ -727,26 +753,7 @@ def make_var_view(frame_var_info, locals, globals): ret_walker = BasicValueWalker(frame_var_info) watch_widget_list = [] - from pudb.debugger import CONFIG - - if CONFIG["persist_watches"]: - from pudb.settings import load_watches, save_watches - stored_expressions = [expr.strip() for expr in load_watches()] - - # As watch expressions are stored in a list, simply appending stored - # expressions to that list will add duplicates. This part is to avoid that. - from pudb.var_view import WatchExpression - existing_expressions = [expr.expression - for expr in frame_var_info.watches] - - for stored_expr in stored_expressions: - if stored_expr not in existing_expressions: - frame_var_info.watches.append(WatchExpression(stored_expr)) - - # Save watches because new ones may have added to a list - save_watches([expr.expression for expr in frame_var_info.watches]) - - for watch_expr in frame_var_info.watches: + for watch_expr in Watches.all(): try: value = eval(watch_expr.expression, globals, locals) except Exception: @@ -790,6 +797,64 @@ def get_frame_var_info(self, read_only, ssid=None): else: return self.frame_var_info.setdefault(ssid, FrameVarInfo()) + +class Watches: + _expressions: Set[WatchExpression] = set() + + def __init__(self): + raise RuntimeError("This class is not meant to be instantiated.") + + @classmethod + def add(cls, expression: WatchExpression): + if expression not in cls._expressions: + cls._expressions.add(expression) + cls.save() + + @classmethod + def remove(cls, expression: WatchExpression): + if expression in cls._expressions: + cls._expressions.remove(expression) + cls.save() + + @classmethod + def save(cls): + from pudb.debugger import CONFIG + if CONFIG.get("persist_watches", False): + return + + try: + with open(get_watches_file_name(), 'w+') as histfile: + for watch in cls._expressions: + histfile.write(watch.expression + '\n') + + except Exception as save_exc: + settings_log.exception("Failed to save watches", save_exc) + raise save_exc + + @classmethod + def load(cls): + from pudb.debugger import CONFIG + if CONFIG.get("persist_watches", False): + return + + watch_fn = get_watches_file_name() + if os.path.exists(watch_fn): + try: + with open(watch_fn, 'r') as histfile: + cls._expressions = set() + for line in histfile.readlines(): + cls._expressions.add(WatchExpression(line.strip())) + except Exception as load_exc: + settings_log.exception("Failed to load watches", load_exc) + raise load_exc + + @classmethod + def all(cls): + if not cls._expressions: + cls.load() + + return sorted(cls._expressions, key=lambda x: x.expression) + # }}} # vim: foldmethod=marker From 4b4c7e1430809d04498b38cbdd1ad3253c678c78 Mon Sep 17 00:00:00 2001 From: kprichard Date: Wed, 2 Oct 2024 17:12:22 -0700 Subject: [PATCH 06/13] - settings.py: flip persist_watches default to True, so that it's usable to new and upgraded pudb users --- pudb/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pudb/settings.py b/pudb/settings.py index 8eaa5185..1c71a034 100644 --- a/pudb/settings.py +++ b/pudb/settings.py @@ -111,7 +111,7 @@ def load_config(): conf_dict.setdefault("wrap_variables", "True") conf_dict.setdefault("default_variables_access_level", "public") - conf_dict.setdefault("persist_watches", False) + conf_dict.setdefault("persist_watches", True) conf_dict.setdefault("display", "auto") From 9c5b77f9768ddb2798bc34a7dfb8e5f9432f120d Mon Sep 17 00:00:00 2001 From: kprichard Date: Wed, 2 Oct 2024 22:34:15 -0700 Subject: [PATCH 07/13] - var_view.py: remove setter so expressions are immutable; add clear and has methods; add some type checking; use dict.get for CONFIG instead of direct item ref. - pudb/test/test_var_view.py: add tests for classes WatchExpression and Watches --- pudb/test/test_var_view.py | 129 +++++++++++++++++++++++++++++++++++++ pudb/var_view.py | 39 ++++++++--- 2 files changed, 160 insertions(+), 8 deletions(-) diff --git a/pudb/test/test_var_view.py b/pudb/test/test_var_view.py index fe308f94..8e473d1c 100644 --- a/pudb/test/test_var_view.py +++ b/pudb/test/test_var_view.py @@ -3,6 +3,8 @@ import string import unittest +from unittest.mock import patch, mock_open + from pudb.var_view import ( STRINGIFIERS, BasicValueWalker, @@ -14,6 +16,7 @@ ValueWalker, get_stringifier, ui_log, + WatchExpression, Watches, ) @@ -402,3 +405,129 @@ def test_maybe_unreasonable_classes(self): # This effectively makes sure that class definitions aren't considered # containers. self.assert_class_counts_equal({"other": 2048}) + + +class WatchExpressionTests(unittest.TestCase): + """ + Test class WatchExpression for expected behaviors + """ + + def test_watch_expression_sorting(self): + alpha_watches = [ + WatchExpression("a"), + WatchExpression("c"), + WatchExpression("b"), + WatchExpression("d"), + WatchExpression("f"), + WatchExpression("e"), + ] + self.assertEqual( + "".join(sorted(str(watch_expr) + for watch_expr in alpha_watches)), + "abcdef") + + def test_hashing(self): + we_a = WatchExpression("a") + we_b = WatchExpression("b") + self.assertEqual(hash(we_a), hash("a")) + self.assertEqual(hash(we_b), hash("b")) + self.assertNotEqual(hash(we_a), hash(we_b)) + + def test_equality(self): + we_a = WatchExpression("a") + we_a2 = WatchExpression("a") + we_b = WatchExpression("b") + self.assertEqual(we_a, we_a2) + self.assertNotEqual(we_a, we_b) + + def test_repr(self): + expr = WatchExpression("a") + self.assertEqual(repr(expr), "a") + + def test_str(self): + expr = WatchExpression("a") + self.assertEqual(str(expr), "a") + + def test_set(self): + """ + watch expressions should be hashable and comparable, + and more or less equivalent to class str + """ + expr = WatchExpression("a") + self.assertIn(expr, set(["a"])) + + # test set membership + we_a = WatchExpression("a") + we_b = WatchExpression("b") + test_set1 = set([we_a, we_b]) + self.assertIn(we_a, test_set1) + self.assertIn(we_b, test_set1) + + # test equivalent sets + test_set2 = set([we_b, we_a]) + self.assertEqual(test_set1, test_set2) + self.assertIn(we_a, test_set2) + self.assertIn(we_b, test_set2) + + # test adding a duplicate + test_set2.add(WatchExpression("a")) + self.assertEqual(test_set1, test_set2) + + +class WatchesTests(unittest.TestCase): + """ + Test class Watches for expected behavior + """ + + def tearDown(self): + # Since Watches is a global object, we must clear out after each test + Watches.clear() + + def test_add_watch(self): + we_z = WatchExpression("z") + Watches.add(we_z) + self.assertIn(we_z, Watches.all()) + + def test_add_watches(self): + watch_expressions_file_log = [] + + def mocked_file_write(*args): + watch_expressions_file_log.append(args) + + mocked_open = mock_open() + # mock the write method of the file object + mocked_open.return_value.write = mocked_file_write + we_a = WatchExpression("a") + we_b = WatchExpression("b") + we_c = WatchExpression("c") + expressions = [we_a, we_b, we_c] + + """ + The expressions file is cumulative, writing out whatever + current set of expressions Watches contains, + so we expect to see: [a], [a], [b], [a], [b], [c] + """ + expected_file_log = [] + for i in range(len(expressions)): + for expr in expressions[:i + 1]: + expected_file_log.append((f"{str(expr)}\n", )) + + with patch('builtins.open', mocked_open): + Watches.add(we_a) + Watches.add(we_b) + Watches.add(we_c) + + self.assertEqual(len(watch_expressions_file_log), 6) + self.assertEqual(watch_expressions_file_log, expected_file_log) + + self.assertIn(we_a, Watches.all()) + self.assertIn(we_b, Watches.all()) + self.assertIn(we_c, Watches.all()) + + def test_remove_watch(self): + we_z = WatchExpression("z") + Watches.add(we_z) + self.assertTrue(Watches.has(we_z)) + Watches.remove(we_z) + self.assertFalse(Watches.has(we_z)) + self.assertEqual(len(Watches.all()), 0) diff --git a/pudb/var_view.py b/pudb/var_view.py index 800d3905..da1038b0 100644 --- a/pudb/var_view.py +++ b/pudb/var_view.py @@ -199,6 +199,11 @@ def __init__(self): class WatchExpression: + """ + A few requirements for WatchExpression: + - must be sortable, hashable and comparable + - encloses a string, and is immutable + """ def __init__(self, expression: str): self._expression = expression.strip() @@ -206,10 +211,6 @@ def __init__(self, expression: str): def expression(self): return self._expression - @expression.setter - def expression(self, value): - self._expression = value.strip() - def __hash__(self): return hash(self._expression) @@ -799,19 +800,40 @@ def get_frame_var_info(self, read_only, ssid=None): class Watches: + """ + Watches encloses a set of WatchExpression objects, and exports + its entry to a canonical file whenever altered. It also acts as a + runtime cache so that we don't have to reload and reparse the file + every time we want to refresh the var view. + """ _expressions: Set[WatchExpression] = set() def __init__(self): raise RuntimeError("This class is not meant to be instantiated.") + @classmethod + def clear(cls): + cls._expressions.clear() + cls.save() + + @classmethod + def has(cls, expression: WatchExpression): + if not isinstance(expression, WatchExpression): + raise TypeError("expression must be a WatchExpression object") + return expression in cls._expressions + @classmethod def add(cls, expression: WatchExpression): + if not isinstance(expression, WatchExpression): + raise TypeError("expression must be a WatchExpression object") if expression not in cls._expressions: cls._expressions.add(expression) cls.save() @classmethod def remove(cls, expression: WatchExpression): + if not isinstance(expression, WatchExpression): + raise TypeError("expression must be a WatchExpression object") if expression in cls._expressions: cls._expressions.remove(expression) cls.save() @@ -819,13 +841,14 @@ def remove(cls, expression: WatchExpression): @classmethod def save(cls): from pudb.debugger import CONFIG - if CONFIG.get("persist_watches", False): + if not CONFIG.get("persist_watches", False): return try: with open(get_watches_file_name(), 'w+') as histfile: - for watch in cls._expressions: - histfile.write(watch.expression + '\n') + for watch in cls.all(): + if watch: + histfile.write(watch.expression + '\n') except Exception as save_exc: settings_log.exception("Failed to save watches", save_exc) @@ -834,7 +857,7 @@ def save(cls): @classmethod def load(cls): from pudb.debugger import CONFIG - if CONFIG.get("persist_watches", False): + if not CONFIG.get("persist_watches", False): return watch_fn = get_watches_file_name() From 360069a3f67476698591895c69a0b571d660d56c Mon Sep 17 00:00:00 2001 From: kprichard Date: Fri, 4 Oct 2024 20:52:14 -0700 Subject: [PATCH 08/13] - debugger.py: use Watches.remove and .add to update an edited expression --- pudb/debugger.py | 6 +++++- pudb/test/test_var_view.py | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pudb/debugger.py b/pudb/debugger.py index a5983ca1..0aea726d 100644 --- a/pudb/debugger.py +++ b/pudb/debugger.py @@ -1150,7 +1150,11 @@ 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() + # Remove old expression and add new one, to avoid rehashing + Watches.remove(var.watch_expr) + var.watch_expr = WatchExpression( + watch_edit.get_edit_text()) + Watches.add(var.watch_expr) elif result == "del": # Remove saved expression diff --git a/pudb/test/test_var_view.py b/pudb/test/test_var_view.py index 8e473d1c..b5dde88e 100644 --- a/pudb/test/test_var_view.py +++ b/pudb/test/test_var_view.py @@ -5,6 +5,8 @@ from unittest.mock import patch, mock_open +import pytest + from pudb.var_view import ( STRINGIFIERS, BasicValueWalker, @@ -473,6 +475,12 @@ def test_set(self): test_set2.add(WatchExpression("a")) self.assertEqual(test_set1, test_set2) + def test_immutability(self): + Watches.clear() + we_a = WatchExpression("a") + with pytest.raises(AttributeError): + we_a.expression = "b" + class WatchesTests(unittest.TestCase): """ From 4d05112af2039d772cac29ad43377da9251d1a00 Mon Sep 17 00:00:00 2001 From: kprichard Date: Fri, 18 Oct 2024 13:44:33 -0700 Subject: [PATCH 09/13] - debugger.py: ruff compliance- imports sorted --- pudb/debugger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pudb/debugger.py b/pudb/debugger.py index 0aea726d..4efafeb5 100644 --- a/pudb/debugger.py +++ b/pudb/debugger.py @@ -590,7 +590,7 @@ def runcall(self, *args, **kwargs): labelled_value, make_hotkey_markup, ) -from pudb.var_view import FrameVarInfoKeeper, WatchExpression, Watches +from pudb.var_view import FrameVarInfoKeeper, Watches, WatchExpression # {{{ display setup From 14075f80aaa8260f7962cc04973bd95c39dce0f6 Mon Sep 17 00:00:00 2001 From: kprichard Date: Fri, 18 Oct 2024 13:45:11 -0700 Subject: [PATCH 10/13] - test_var_view.py: ruff compliance- imports sorted --- pudb/test/test_var_view.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pudb/test/test_var_view.py b/pudb/test/test_var_view.py index b5dde88e..69666e04 100644 --- a/pudb/test/test_var_view.py +++ b/pudb/test/test_var_view.py @@ -2,8 +2,7 @@ import itertools import string import unittest - -from unittest.mock import patch, mock_open +from unittest.mock import mock_open, patch import pytest @@ -16,9 +15,10 @@ PudbMapping, PudbSequence, ValueWalker, + Watches, + WatchExpression, get_stringifier, ui_log, - WatchExpression, Watches, ) From 4a24f19b3ef8c8ebeb6af725e32a5727f9589b3c Mon Sep 17 00:00:00 2001 From: kprichard Date: Fri, 18 Oct 2024 13:45:59 -0700 Subject: [PATCH 11/13] - test_var_view.py: ruff compliance- use set literals instead of set() constructor --- pudb/test/test_var_view.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pudb/test/test_var_view.py b/pudb/test/test_var_view.py index 69666e04..3b20cf76 100644 --- a/pudb/test/test_var_view.py +++ b/pudb/test/test_var_view.py @@ -456,17 +456,17 @@ def test_set(self): and more or less equivalent to class str """ expr = WatchExpression("a") - self.assertIn(expr, set(["a"])) + self.assertIn(expr, {"a"}) # test set membership we_a = WatchExpression("a") we_b = WatchExpression("b") - test_set1 = set([we_a, we_b]) + test_set1 = {we_a, we_b} self.assertIn(we_a, test_set1) self.assertIn(we_b, test_set1) # test equivalent sets - test_set2 = set([we_b, we_a]) + test_set2 = {we_b, we_a} self.assertEqual(test_set1, test_set2) self.assertIn(we_a, test_set2) self.assertIn(we_b, test_set2) From 1983fa31e47bac1a5963c377add0b00534e0e968 Mon Sep 17 00:00:00 2001 From: kprichard Date: Fri, 18 Oct 2024 13:46:22 -0700 Subject: [PATCH 12/13] - var_view.py: ruff compliance: imports sorted --- pudb/var_view.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pudb/var_view.py b/pudb/var_view.py index da1038b0..c4c60c60 100644 --- a/pudb/var_view.py +++ b/pudb/var_view.py @@ -27,17 +27,18 @@ # {{{ constants and imports import inspect +import os import warnings from abc import ABC, abstractmethod from collections.abc import Callable, Sized -import os -from typing import List, Tuple, Set +from typing import List, Set, Tuple import urwid -from pudb.lowlevel import ui_log, settings_log -from pudb.ui_tools import text_width +from pudb.lowlevel import settings_log, ui_log from pudb.settings import get_watches_file_name +from pudb.ui_tools import text_width + try: import numpy From 03da90ecbd291cab7295fd20dc1557d48c10b72e Mon Sep 17 00:00:00 2001 From: kprichard Date: Fri, 18 Oct 2024 13:47:15 -0700 Subject: [PATCH 13/13] - test_var_view.py, var_view.py: ruff compliance- prefer double quotes instead of single quotes --- pudb/test/test_var_view.py | 2 +- pudb/var_view.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pudb/test/test_var_view.py b/pudb/test/test_var_view.py index 3b20cf76..bf7919a1 100644 --- a/pudb/test/test_var_view.py +++ b/pudb/test/test_var_view.py @@ -520,7 +520,7 @@ def mocked_file_write(*args): for expr in expressions[:i + 1]: expected_file_log.append((f"{str(expr)}\n", )) - with patch('builtins.open', mocked_open): + with patch("builtins.open", mocked_open): Watches.add(we_a) Watches.add(we_b) Watches.add(we_c) diff --git a/pudb/var_view.py b/pudb/var_view.py index c4c60c60..3185fd29 100644 --- a/pudb/var_view.py +++ b/pudb/var_view.py @@ -846,10 +846,10 @@ def save(cls): return try: - with open(get_watches_file_name(), 'w+') as histfile: + with open(get_watches_file_name(), "w+") as histfile: for watch in cls.all(): if watch: - histfile.write(watch.expression + '\n') + histfile.write(watch.expression + "\n") except Exception as save_exc: settings_log.exception("Failed to save watches", save_exc) @@ -864,7 +864,7 @@ def load(cls): watch_fn = get_watches_file_name() if os.path.exists(watch_fn): try: - with open(watch_fn, 'r') as histfile: + with open(watch_fn, "r") as histfile: cls._expressions = set() for line in histfile.readlines(): cls._expressions.add(WatchExpression(line.strip()))