diff options
author | Alexey Stepanov <penguinolog@users.noreply.github.com> | 2023-04-12 09:03:27 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-12 09:03:27 +0200 |
commit | 2f80badfdd011dc3e96c95e51bf459b282f6d735 (patch) | |
tree | c3b8bc425aa89d9382dfcf462560f96c40229c5c | |
parent | 8a0a6d166e4cef88a7e7b644abee8712eacee7f6 (diff) | |
download | urwid-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.py | 24 | ||||
-rw-r--r-- | urwid/tests/test_listbox.py | 34 |
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)) |