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

Persistent watch expressions (continuation of #150) #661

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
- 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
  • Loading branch information
Kevin-Prichard committed Oct 18, 2024
commit 9c5b77f9768ddb2798bc34a7dfb8e5f9432f120d
129 changes: 129 additions & 0 deletions pudb/test/test_var_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 3,8 @@
import string
import unittest

from unittest.mock import patch, mock_open

from pudb.var_view import (
STRINGIFIERS,
BasicValueWalker,
Expand All @@ -14,6 16,7 @@
ValueWalker,
get_stringifier,
ui_log,
WatchExpression, Watches,
)


Expand Down Expand Up @@ -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)
39 changes: 31 additions & 8 deletions pudb/var_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,17 199,18 @@ 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()

@property
def expression(self):
return self._expression

@expression.setter
def expression(self, value):
self._expression = value.strip()

def __hash__(self):
return hash(self._expression)

Expand Down Expand Up @@ -799,33 800,55 @@ 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()

@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)
Expand All @@ -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()
Expand Down