summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexey Stepanov <penguinolog@users.noreply.github.com>2023-04-12 09:03:27 +0200
committerGitHub <noreply@github.com>2023-04-12 09:03:27 +0200
commit2f80badfdd011dc3e96c95e51bf459b282f6d735 (patch)
treec3b8bc425aa89d9382dfcf462560f96c40229c5c
parent8a0a6d166e4cef88a7e7b644abee8712eacee7f6 (diff)
downloadurwid-2f80badfdd011dc3e96c95e51bf459b282f6d735.tar.gz
Fix #445 - add `__len__` to listbox with validation if body `Sized` (#534)
* If body is not `Sized` - `__len__` call should raise `AttributeError` * Downside: since body can be replaced, `__len__` is always set ant `ListBox` by itself will pass `Sized` instance check * Relax `SimpleListWalker` constructor rules: any Iterable is allowed as source due to `list` is used internally Co-authored-by: Aleksei Stepanov <alekseis@nvidia.com>
-rw-r--r--urwid/listbox.py24
-rw-r--r--urwid/tests/test_listbox.py34
2 files changed, 51 insertions, 7 deletions
diff --git a/urwid/listbox.py b/urwid/listbox.py
index a8cd91c..47b8376 100644
--- a/urwid/listbox.py
+++ b/urwid/listbox.py
@@ -24,7 +24,7 @@ from __future__ import annotations
import typing
import warnings
-from collections.abc import MutableSequence
+from collections.abc import Iterable, Sized
from urwid import signals
from urwid.canvas import CanvasCombine, SolidCanvas
@@ -100,7 +100,7 @@ class ListWalker(metaclass=signals.MetaSignals):
class SimpleListWalker(MonitoredList, ListWalker):
- def __init__(self, contents, wrap_around: bool = False):
+ def __init__(self, contents: Iterable[typing.Any], wrap_around: bool = False):
"""
contents -- list to copy into this object
@@ -113,7 +113,7 @@ class SimpleListWalker(MonitoredList, ListWalker):
detected automatically and will cause ListBox objects using
this list walker to be updated.
"""
- if not isinstance(contents, MutableSequence):
+ if not isinstance(contents, Iterable):
raise ListWalkerError(f"SimpleListWalker expecting list like object, got: {contents!r}")
MonitoredList.__init__(self, contents)
self.focus = 0
@@ -191,7 +191,7 @@ class SimpleListWalker(MonitoredList, ListWalker):
class SimpleFocusListWalker(ListWalker, MonitoredFocusList):
- def __init__(self, contents, wrap_around=False):
+ def __init__(self, contents: Iterable[typing.Any], wrap_around: bool = False):
"""
contents -- list to copy into this object
@@ -208,8 +208,8 @@ class SimpleFocusListWalker(ListWalker, MonitoredFocusList):
normal list methods will cause the focus to be updated
intelligently.
"""
- if not isinstance(contents, MutableSequence):
- raise ListWalkerError(f"SimpleFocusListWalker expecting list like object, got: {contents!r}")
+ if not isinstance(contents, Iterable):
+ raise ListWalkerError(f"SimpleFocusListWalker expecting iterable object, got: {contents!r}")
MonitoredFocusList.__init__(self, contents)
self.wrap_around = wrap_around
@@ -274,7 +274,12 @@ class ListBox(Widget, WidgetContainerMixin):
widgets to be displayed inside the list box
:type body: ListWalker
"""
- self.body = body
+ if getattr(body, 'get_focus', None):
+ self._body: ListWalker = body
+ else:
+ self._body = SimpleListWalker(body)
+
+ self.body = self._body # Initialization hack
# offset_rows is the number of rows between the top of the view
# and the top of the focused item
@@ -340,6 +345,11 @@ class ListBox(Widget, WidgetContainerMixin):
)
self.body = body
+ def __len__(self) -> int:
+ if isinstance(self._body, Sized):
+ return len(self._body)
+ raise AttributeError(f"{self._body.__class__.__name__} is not Sized")
+
def calculate_visible(self, size: tuple[int, int], focus: bool = False):
"""
Returns the widgets that would be displayed in
diff --git a/urwid/tests/test_listbox.py b/urwid/tests/test_listbox.py
index 846b17b..044a0df 100644
--- a/urwid/tests/test_listbox.py
+++ b/urwid/tests/test_listbox.py
@@ -94,6 +94,30 @@ class ListBoxCalculateVisibleTest(unittest.TestCase):
self.cvtest( "cursor way off bottom",
l1, 3, 100, (0,1), 2, (1, 4) )
+ def test_sized(self):
+ lbox = urwid.ListBox(urwid.SimpleListWalker([urwid.Text(str(num)) for num in range(5)]))
+ self.assertEqual(5, len(lbox))
+
+ def test_not_sized(self):
+ class TestWalker(urwid.ListWalker):
+ @property
+ def contents(self):
+ return self
+
+ @staticmethod
+ def next_position(position: int) -> tuple[urwid.Text, int]:
+ return urwid.Text(str(position)), position
+
+ @staticmethod
+ def prev_position(position: int) -> tuple[urwid.Text, int]:
+ return urwid.Text(str(position)), position
+
+ lbox = urwid.ListBox(TestWalker())
+ with self.assertRaises(AttributeError) as exc:
+ len(lbox)
+
+ self.assertEqual(f"{TestWalker.__name__} is not Sized", str(exc.exception))
+
class ListBoxChangeFocusTest(unittest.TestCase):
def cftest(self, desc, body, pos, offset_inset,
@@ -814,3 +838,13 @@ class ListBoxSetBodyTest(unittest.TestCase):
"outdated canvas cache reuse "
"after ListWalker's contents modified"
)
+
+
+class TestListWalkerFromIterable(unittest.TestCase):
+ def test_01_simple_list_walker(self):
+ walker = urwid.SimpleListWalker(str(num) for num in range(5))
+ self.assertEqual(5, len(walker))
+
+ def test_02_simple_focus_list_walker(self):
+ walker = urwid.SimpleFocusListWalker(str(num) for num in range(5))
+ self.assertEqual(5, len(walker))