diff options
author | Alexey Stepanov <penguinolog@users.noreply.github.com> | 2023-04-12 07:47:14 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-12 07:47:14 +0200 |
commit | 8a0a6d166e4cef88a7e7b644abee8712eacee7f6 (patch) | |
tree | a96c0ccd10f838bcbbc00482b04b547c0b1a83c5 | |
parent | 0c0ea377ab9b418cbb5233fa6e178dd05f1f4e5a (diff) | |
download | urwid-8a0a6d166e4cef88a7e7b644abee8712eacee7f6.tar.gz |
Deprecate legacy property creation (#533)
* Deprecate legacy property creation
* Drop long time ago removed methods (never returning methods)
* Move large part of property implementations under `@property`
* Emit `PendingDeprecationWarning`
for old compatibility code
for public methods used as core for property and methods for compatibility
* Emit `DeprecationWarning`
for private methods used in property construction
using `property()` call
Due to amount of copy-paste like changes, for containers shared part is moved to the existing base classes
Add `__len__` to the list based containers. Related #445
Fix typo in type annotation for `Frame.mouse_event`
* Update urwid/canvas.py
Co-authored-by: Ian Ward <ian@excess.org>
* Update urwid/canvas.py
Co-authored-by: Ian Ward <ian@excess.org>
* Update urwid/tests/test_container.py
Co-authored-by: Ian Ward <ian@excess.org>
* Fix typo in test name
* Frame `header`, `body` and `footer` also has property and methods from pre-property era
Make consistent with other containers
---------
Co-authored-by: Aleksei Stepanov <alekseis@nvidia.com>
Co-authored-by: Ian Ward <ian@excess.org>
-rwxr-xr-x | examples/calc.py | 50 | ||||
-rwxr-xr-x | examples/dialog.py | 22 | ||||
-rw-r--r-- | urwid/canvas.py | 35 | ||||
-rwxr-xr-x | urwid/container.py | 926 | ||||
-rwxr-xr-x | urwid/decoration.py | 183 | ||||
-rwxr-xr-x | urwid/display_common.py | 61 | ||||
-rwxr-xr-x | urwid/graphics.py | 18 | ||||
-rw-r--r-- | urwid/listbox.py | 56 | ||||
-rwxr-xr-x | urwid/main_loop.py | 45 | ||||
-rwxr-xr-x | urwid/monitored_list.py | 63 | ||||
-rw-r--r-- | urwid/tests/test_container.py | 29 | ||||
-rw-r--r-- | urwid/widget.py | 51 |
12 files changed, 1137 insertions, 402 deletions
diff --git a/examples/calc.py b/examples/calc.py index bf31ba6..b83cc07 100755 --- a/examples/calc.py +++ b/examples/calc.py @@ -570,7 +570,7 @@ class CalcDisplay: def __init__(self): self.columns = urwid.Columns([HelpColumn(), CellColumn("A")], 1) self.col_list = self.columns.widget_list - self.columns.set_focus_column( 1 ) + self.columns.focus_position = 1 view = urwid.AttrWrap(self.columns, 'body') self.view = urwid.Frame(view) # for showing messages self.col_link = {} @@ -629,7 +629,7 @@ class CalcDisplay: if key is None: return - if self.columns.get_focus_column() == 0: + if self.columns.focus_position == 0: if key not in ('up','down','page up','page down'): raise CalcEvent(E_invalid_in_help_col) @@ -648,41 +648,41 @@ class CalcDisplay: if key.upper() in COLUMN_KEYS: # column switch i = COLUMN_KEYS.index(key.upper()) - if i >= len( self.col_list ): + if i >= len(self.columns): raise CalcEvent(E_no_such_column % key.upper()) - self.columns.set_focus_column( i ) + self.columns.focus_position = i return elif key == "(": # open a new column - if len( self.col_list ) >= len(COLUMN_KEYS): + if len(self.columns) >= len(COLUMN_KEYS): raise CalcEvent(E_no_more_columns) - i = self.columns.get_focus_column() + i = self.columns.focus_position if i == 0: # makes no sense in help column return key - col = self.col_list[i] - new_letter = COLUMN_KEYS[len(self.col_list)] - parent, child = col.create_child( new_letter ) + col = self.columns.contents[i][0] + new_letter = COLUMN_KEYS[len(self.columns)] + parent, child = col.create_child(new_letter ) if child is None: # something invalid in focus return key self.col_list.append(child) self.set_link( parent, col, child ) - self.columns.set_focus_column(len(self.col_list)-1) + self.columns.focus_position = len(self.columns)-1 elif key == ")": - i = self.columns.get_focus_column() + i = self.columns.focus_position if i == 0: # makes no sense in help column return key - col = self.col_list[i] + col = self.columns.contents[i][0] parent, pcol = self.get_parent( col ) if parent is None: # column has no parent raise CalcEvent(E_no_parent_column) new_i = self.col_list.index( pcol ) - self.columns.set_focus_column( new_i ) + self.columns.focus_position = new_i else: return key @@ -704,7 +704,7 @@ class CalcDisplay: """Return True if the column passed is empty.""" i = COLUMN_KEYS.index(letter) - col = self.col_list[i] + col = self.columns.contents[i][0] return col.is_empty() @@ -712,19 +712,19 @@ class CalcDisplay: """Delete the column with the given letter.""" i = COLUMN_KEYS.index(letter) - col = self.col_list[i] + col = self.columns.contents[i][0] parent, pcol = self.get_parent( col ) - f = self.columns.get_focus_column() + f = self.columns.focus_position if f == i: # move focus to the parent column f = self.col_list.index(pcol) - self.columns.set_focus_column(f) + self.columns.focus_position = f parent.remove_child() pcol.update_results(parent) - del self.col_list[i] + del self.columns.contents[i] # delete children of this column keep_right_cols = [] @@ -742,8 +742,8 @@ class CalcDisplay: self.col_list[i:] = keep_right_cols # fix the letter assignments - for j in range(i, len(self.col_list)): - col = self.col_list[j] + for j in range(i, len(self.columns)): + col = self.columns.contents[j][0] # fix the column heading col.set_letter( COLUMN_KEYS[j] ) parent, pcol = self.get_parent( col ) @@ -753,8 +753,8 @@ class CalcDisplay: def update_parent_columns(self): "Update the parent columns of the current focus column." - f = self.columns.get_focus_column() - col = self.col_list[f] + f = self.columns.focus_position + col = self.columns.contents[f][0] while 1: parent, pcol = self.get_parent(col) if pcol is None: @@ -765,13 +765,11 @@ class CalcDisplay: return col = pcol - def get_expression_result(self): """Return (expression, result) as strings.""" - col = self.col_list[1] - return col.get_expression(), "%d"%col.get_result() - + col = self.columns.contents[1][0] + return col.get_expression(), f"{col.get_result():d}" class CalcNumLayout(urwid.TextLayout): diff --git a/examples/dialog.py b/examples/dialog.py index ec8c8e0..47ec0f3 100755 --- a/examples/dialog.py +++ b/examples/dialog.py @@ -118,16 +118,16 @@ class InputDialogDisplay(DialogDisplay): DialogDisplay.__init__(self, text, height, width, body) - self.frame.set_focus('body') + self.frame.focus_position = 'body' def unhandled_key(self, size, k): if k in ('up','page up'): - self.frame.set_focus('body') + self.frame.focus_position = 'body' if k in ('down','page down'): - self.frame.set_focus('footer') + self.frame.focus_position = 'footer' if k == 'enter': # pass enter to the "ok" button - self.frame.set_focus('footer') + self.frame.focus_position = 'footer' self.view.keypress( size, k ) def on_exit(self, exitcode): @@ -148,9 +148,9 @@ class TextDialogDisplay(DialogDisplay): def unhandled_key(self, size, k): if k in ('up','page up','down','page down'): - self.frame.set_focus('body') + self.frame.focus_position = 'body' self.view.keypress( size, k ) - self.frame.set_focus('footer') + self.frame.focus_position = 'footer' class ListDialogDisplay(DialogDisplay): @@ -178,17 +178,17 @@ class ListDialogDisplay(DialogDisplay): lb = urwid.AttrWrap( lb, "selectable" ) DialogDisplay.__init__(self, text, height, width, lb ) - self.frame.set_focus('body') + self.frame.focus_position = 'body' def unhandled_key(self, size, k): if k in ('up','page up'): - self.frame.set_focus('body') + self.frame.focus_position = 'body' if k in ('down','page down'): - self.frame.set_focus('footer') + self.frame.focus_position = 'footer' if k == 'enter': # pass enter to the "ok" button - self.frame.set_focus('footer') - self.buttons.set_focus(0) + self.frame.focus_position = 'footer' + self.buttons.focus_position = 0 self.view.keypress( size, k ) def on_exit(self, exitcode): diff --git a/urwid/canvas.py b/urwid/canvas.py index 7838ec2..ed71c54 100644 --- a/urwid/canvas.py +++ b/urwid/canvas.py @@ -23,6 +23,7 @@ from __future__ import annotations import typing +import warnings import weakref from collections.abc import Sequence @@ -203,10 +204,6 @@ class Canvas: _renamed_error = CanvasError("The old Canvas class is now called " "TextCanvas. Canvas is now the base class for all canvas " "classes.") - _old_repr_error = CanvasError("The internal representation of " - "canvases is no longer stored as .text, .attr, and .cs " - "lists, please see the TextCanvas class for the new " - "representation of canvas content.") def __init__( self, @@ -240,23 +237,35 @@ class Canvas: raise self._finalized_error self._widget_info = widget, size, focus - def _get_widget_info(self): + @property + def widget_info(self): return self._widget_info - widget_info = property(_get_widget_info) - - def _raise_old_repr_error(self, val=None) -> typing.NoReturn: - raise self._old_repr_error - def _text_content(self): + def _get_widget_info(self): + warnings.warn( + f"Method `{self.__class__.__name__}._get_widget_info` is deprecated, " + f"please use property `{self.__class__.__name__}.widget_info`", + DeprecationWarning, + stacklevel=2, + ) + return self.widget_info + + @property + def text(self) -> list[bytes]: """ Return the text content of the canvas as a list of strings, one for each row. """ return [b''.join([text for (attr, cs, text) in row]) for row in self.content()] - text = property(_text_content, _raise_old_repr_error) - attr = property(_raise_old_repr_error, _raise_old_repr_error) - cs = property(_raise_old_repr_error, _raise_old_repr_error) + def _text_content(self): + warnings.warn( + f"Method `{self.__class__.__name__}._text_content` is deprecated, " + f"please use property `{self.__class__.__name__}.text`", + DeprecationWarning, + stacklevel=2, + ) + return self.text def content( self, diff --git a/urwid/container.py b/urwid/container.py index 9914e9b..6923388 100755 --- a/urwid/container.py +++ b/urwid/container.py @@ -22,8 +22,10 @@ from __future__ import annotations +import abc import typing -from collections.abc import Iterable, Sequence +import warnings +from collections.abc import Iterable, Iterator, Sequence from itertools import chain, repeat from urwid.canvas import CanvasCombine, CanvasJoin, CanvasOverlay, CompositeCanvas, SolidCanvas @@ -70,7 +72,7 @@ class WidgetContainerMixin: """ Mixin class for widget containers implementing common container methods """ - def __getitem__(self, position): + def __getitem__(self, position) -> Widget: """ Container short-cut for self.contents[position][0].base_widget which means "give me the child widget at position without any @@ -136,26 +138,110 @@ class WidgetContainerMixin: return out out.append(w) + @property + @abc.abstractmethod + def focus(self) -> Widget: + """ + Read-only property returning the child widget in focus for + container widgets. This default implementation + always returns ``None``, indicating that this widget has no children. + """ + + def _get_focus(self) -> Widget: + warnings.warn( + f"method `{self.__class__.__name__}._get_focus` is deprecated, " + f"please use `{self.__class__.__name__}.focus` property", + DeprecationWarning, + stacklevel=2, + ) + return self.focus + class WidgetContainerListContentsMixin: """ Mixin class for widget containers whose positions are indexes into a list available as self.contents. """ - def __iter__(self): + def __iter__(self) -> Iterator[int]: """ Return an iterable of positions for this container from first to last. """ return iter(range(len(self.contents))) - def __reversed__(self): + def __reversed__(self) -> Iterator[int]: """ Return an iterable of positions for this container from last to first. """ return iter(range(len(self.contents) - 1, -1, -1)) + def __len__(self) -> int: + return len(self.contents) + + @property + @abc.abstractmethod + def contents(self) -> list[tuple[Widget, typing.Any]]: + """The contents of container as a list of (widget, options)""" + + @contents.setter + def contents(self, new_contents: list[tuple[Widget, typing.Any]]) -> None: + """The contents of container as a list of (widget, options)""" + + def _get_contents(self) -> list[tuple[Widget, typing.Any]]: + warnings.warn( + f"method `{self.__class__.__name__}._get_contents` is deprecated, " + f"please use `{self.__class__.__name__}.contents` property", + DeprecationWarning, + stacklevel=2, + ) + return self.contents + + def _set_contents(self, c: list[tuple[Widget, typing.Any]]) -> None: + warnings.warn( + f"method `{self.__class__.__name__}._set_contents` is deprecated, " + f"please use `{self.__class__.__name__}.contents` property", + DeprecationWarning, + stacklevel=2, + ) + self.contents = c + + @property + @abc.abstractmethod + def focus_position(self) -> int | None: + """ + index of child widget in focus. + """ + + @focus_position.setter + def focus_position(self, position: int) -> None: + """ + index of child widget in focus. + """ + + def _get_focus_position(self) -> int | None: + warnings.warn( + f"method `{self.__class__.__name__}._get_focus_position` is deprecated, " + f"please use `{self.__class__.__name__}.focus_position` property", + DeprecationWarning, + stacklevel=2, + ) + return self.focus_position + + def _set_focus_position(self, position: int) -> None: + """ + Set the widget in focus. + + position -- index of child widget to be made focus + """ + warnings.warn( + f"method `{self.__class__.__name__}._set_focus_position` is deprecated, " + f"please use `{self.__class__.__name__}.focus_position` property", + DeprecationWarning, + stacklevel=2, + ) + self.focus_position = position + class GridFlowError(Exception): pass @@ -172,14 +258,14 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi def __init__( self, - cells: Sequence[Widget], + cells: Iterable[Widget], cell_width: int, h_sep: int, v_sep: int, align: Literal['left', 'center', 'right'] | tuple[Literal['relative'], int], ): """ - :param cells: list of flow widgets to display + :param cells: iterable of flow widgets to display :param cell_width: column width for each cell :param h_sep: blank columns between each cell horizontally :param v_sep: blank rows between cells vertically @@ -198,7 +284,7 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi self._cache_maxcol = None super().__init__(None) # set self._w to something other than None - self.get_display_widget(((h_sep+cell_width)*len(cells),)) + self.get_display_widget(((h_sep+cell_width)*len(self._contents),)) def _invalidate(self) -> None: self._cache_maxcol = None @@ -213,47 +299,97 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi except (TypeError, ValueError): raise GridFlowError(f"added content invalid {item!r}") - def _get_cells(self): + @property + def cells(self): + """ + A list of the widgets in this GridFlow + + .. note:: only for backwards compatibility. You should use the new + standard container property :attr:`contents` to modify GridFlow + contents. + """ + warnings.warn( + "only for backwards compatibility." + "You should use the new standard container property `contents` to modify GridFlow", + PendingDeprecationWarning, + stacklevel=2 + ) ml = MonitoredList(w for w, t in self.contents) + def user_modified(): - self._set_cells(ml) + self.cells = ml ml.set_modified_callback(user_modified) return ml - def _set_cells(self, widgets): + @cells.setter + def cells(self, widgets: Sequence[Widget]): + warnings.warn( + "only for backwards compatibility." + "You should use the new standard container property `contents` to modify GridFlow", + PendingDeprecationWarning, + stacklevel=2 + ) focus_position = self.focus_position self.contents = [ (new, (GIVEN, self._cell_width)) for new in widgets] if focus_position < len(widgets): self.focus_position = focus_position - cells = property(_get_cells, _set_cells, doc=""" - A list of the widgets in this GridFlow - .. note:: only for backwards compatibility. You should use the new - standard container property :attr:`contents` to modify GridFlow - contents. - """) + def _get_cells(self): + warnings.warn( + "only for backwards compatibility." + "You should use the new standard container property `contents` to modify GridFlow", + DeprecationWarning, + stacklevel=2, + ) + return self.cells + + def _set_cells(self, widgets: Sequence[Widget]): + warnings.warn( + "only for backwards compatibility." + "You should use the new standard container property `contents` to modify GridFlow", + DeprecationWarning, + stacklevel=2, + ) + self.cells = widgets - def _get_cell_width(self) -> int: + @property + def cell_width(self) -> int: + """ + The width of each cell in the GridFlow. Setting this value affects + all cells. + """ return self._cell_width - def _set_cell_width(self, width: int) -> None: + @cell_width.setter + def cell_width(self, width: int) -> None: focus_position = self.focus_position self.contents = [ (w, (GIVEN, width)) for (w, options) in self.contents] self.focus_position = focus_position self._cell_width = width - cell_width = property(_get_cell_width, _set_cell_width, doc=""" - The width of each cell in the GridFlow. Setting this value affects - all cells. - """) - def _get_contents(self): - return self._contents + def _get_cell_width(self) -> int: + warnings.warn( + f"Method `{self.__class__.__name__}._get_cell_width` is deprecated, " + f"please use property `{self.__class__.__name__}.cell_width`", + DeprecationWarning, + stacklevel=2, + ) + return self.cell_width - def _set_contents(self, c): - self._contents[:] = c - contents = property(_get_contents, _set_contents, doc=""" + def _set_cell_width(self, width: int) -> None: + warnings.warn( + f"Method `{self.__class__.__name__}._set_cell_width` is deprecated, " + f"please use property `{self.__class__.__name__}.cell_width`", + DeprecationWarning, + stacklevel=2, + ) + self.cell_width = width + + @property + def contents(self): + """ The contents of this GridFlow as a list of (widget, options) tuples. @@ -265,7 +401,12 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi widget will update automatically. .. seealso:: Create new options tuples with the :meth:`options` method. - """) + """ + return self._contents + + @contents.setter + def contents(self, c): + self._contents[:] = c def options( self, @@ -284,7 +425,7 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi width_amount = self._cell_width return (width_type, width_amount) - def set_focus(self, cell: Widget | int): + def set_focus(self, cell: Widget | int) -> None: """ Set the cell in focus, for backwards compatibility. @@ -294,9 +435,23 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi :param cell: contained element to focus :type cell: Widget or int """ + warnings.warn( + "only for backwards compatibility." + "You may also use the new standard container property `focus_position` to set the focus.", + PendingDeprecationWarning, + stacklevel=2, + ) if isinstance(cell, int): - return self._set_focus_position(cell) - return self._set_focus_cell(cell) + self.focus_position = cell + return + self.focus_cell = cell + + @property + def focus(self) -> Widget | None: + """the child widget in focus or None when GridFlow is empty""" + if not self.contents: + return None + return self.contents[self.focus_position][0] def get_focus(self): """ @@ -305,37 +460,62 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi .. note:: only for backwards compatibility. You may also use the new standard container property :attr:`focus` to get the focus. """ - if not self.contents: - return None - return self.contents[self.focus_position][0] - focus = property(get_focus, - doc="the child widget in focus or None when GridFlow is empty") - - def _set_focus_cell(self, cell: Widget) -> None: + warnings.warn( + "only for backwards compatibility." + "You may also use the new standard container property `focus` to get the focus.", + PendingDeprecationWarning, + stacklevel=2, + ) + return self.focus + + @property + def focus_cell(self): + warnings.warn( + "only for backwards compatibility." + "You may also use the new standard container property" + "`focus` to get the focus and `focus_position` to get/set the cell in focus by index", + PendingDeprecationWarning, + stacklevel=2, + ) + return self.focus + + @focus_cell.setter + def focus_cell(self, cell: Widget) -> None: + warnings.warn( + "only for backwards compatibility." + "You may also use the new standard container property" + "`focus` to get the focus and `focus_position` to get/set the cell in focus by index", + PendingDeprecationWarning, + stacklevel=2, + ) for i, (w, options) in enumerate(self.contents): if cell == w: self.focus_position = i return raise ValueError(f"Widget not found in GridFlow contents: {cell!r}") - focus_cell = property(get_focus, _set_focus_cell, doc=""" - The widget in focus, for backwards compatibility. - .. note:: only for backwards compatibility. You should use the new - standard container property :attr:`focus` to get the widget in - focus and :attr:`focus_position` to get/set the cell in focus by - index. - """) + def _set_focus_cell(self, cell: Widget) -> None: + warnings.warn( + "only for backwards compatibility." + "You may also use the new standard container property" + "`focus` to get the focus and `focus_position` to get/set the cell in focus by index", + DeprecationWarning, + stacklevel=2, + ) + self.focus_cell = cell - def _get_focus_position(self) -> int | None: + @property + def focus_position(self) -> int | None: """ - Return the index of the widget in focus or None if this GridFlow is - empty. + index of child widget in focus. + Raises :exc:`IndexError` if read when GridFlow is empty, or when set to an invalid index. """ if not self.contents: raise IndexError("No focus_position, GridFlow is empty") return self.contents.focus - def _set_focus_position(self, position: int) -> None: + @focus_position.setter + def focus_position(self, position: int) -> None: """ Set the widget in focus. @@ -347,10 +527,6 @@ class GridFlow(WidgetWrap, WidgetContainerMixin, WidgetContainerListContentsMixi except (TypeError, IndexError): raise IndexError(f"No GridFlow child widget at position {position}") self.contents.focus = position - focus_position = property(_get_focus_position, _set_focus_position, doc=""" - index of child widget in focus. Raises :exc:`IndexError` if read when - GridFlow is empty, or when set to an invalid index. - """) def get_display_widget(self, size: tuple[int]) -> Divider | Pile: """ @@ -697,21 +873,24 @@ class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): """Pass keypress to top_w.""" return self.top_w.keypress(self.top_w_size(size, *self.calculate_padding_filler(size, True)), key) - def _get_focus(self) -> Widget: + @property + def focus(self) -> Widget: """ - Currently self.top_w is always the focus of an Overlay + Read-only property returning the child widget in focus for + container widgets. This default implementation + always returns ``None``, indicating that this widget has no children. """ return self.top_w - focus = property(_get_focus, - doc="the top widget in this overlay is always in focus") - def _get_focus_position(self) -> Literal[1]: + @property + def focus_position(self) -> Literal[1]: """ Return the top widget position (currently always 1). """ return 1 - def _set_focus_position(self, position): + @focus_position.setter + def focus_position(self, position: int) -> None: """ Set the widget in focus. Currently only position 0 is accepted. @@ -719,10 +898,30 @@ class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): """ if position != 1: raise IndexError(f"Overlay widget focus_position currently must always be set to 1, not {position}") - focus_position = property(_get_focus_position, _set_focus_position, - doc="index of child widget in focus, currently always 1") - def _contents(self): + @property + def contents(self): + """ + a list-like object similar to:: + + [(bottom_w, bottom_options)), + (top_w, top_options)] + + This object may be used to read or update top and bottom widgets and + top widgets's options, but no widgets may be added or removed. + + `top_options` takes the form + `(align_type, align_amount, width_type, width_amount, min_width, left, + right, valign_type, valign_amount, height_type, height_amount, + min_height, top, bottom)` + + bottom_options is always + `('left', None, 'relative', 100, None, 0, 0, + 'top', None, 'relative', 100, None, 0, 0)` + which means that bottom widget always covers the full area of the Overlay. + writing a different value for `bottom_options` raises an + :exc:`OverlayError`. + """ class OverlayContents: def __len__(inner_self): return 2 @@ -730,6 +929,13 @@ class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): __setitem__ = self._contents__setitem__ return OverlayContents() + @contents.setter + def contents(self, new_contents): + if len(new_contents) != 2: + raise ValueError("Contents length for overlay should be only 2") + self.contents[0] = new_contents[0] + self.contents[1] = new_contents[1] + def _contents__getitem__(self, index: Literal[0, 1]): if index == 0: return (self.bottom_w, self._DEFAULT_BOTTOM_OPTIONS) @@ -786,27 +992,6 @@ class Overlay(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): else: raise IndexError(f"Overlay.contents has no position {index!r}") self._invalidate() - contents = property(_contents, doc=""" - a list-like object similar to:: - - [(bottom_w, bottom_options)), - (top_w, top_options)] - - This object may be used to read or update top and bottom widgets and - top widgets's options, but no widgets may be added or removed. - - `top_options` takes the form - `(align_type, align_amount, width_type, width_amount, min_width, left, - right, valign_type, valign_amount, height_type, height_amount, - min_height, top, bottom)` - - bottom_options is always - `('left', None, 'relative', 100, None, 0, 0, - 'top', None, 'relative', 100, None, 0, 0)` - which means that bottom widget always covers the full area of the Overlay. - writing a different value for `bottom_options` raises an - :exc:`OverlayError`. - """) def get_cursor_coords(self, size: tuple[int, int]) -> tuple[int, int] | None: """Return cursor coords from top_w, if any.""" @@ -944,54 +1129,121 @@ class Frame(Widget, WidgetContainerMixin): self._footer = footer self.focus_part = focus_part - def get_header(self) -> Widget | None: + @property + def header(self) -> Widget | None: return self._header - def set_header(self, header: Widget | None): + @header.setter + def header(self, header: Widget | None): self._header = header if header is None and self.focus_part == 'header': self.focus_part = 'body' self._invalidate() - header = property(get_header, set_header) - def get_body(self) -> Widget: + def get_header(self) -> Widget | None: + warnings.warn( + f"method `{self.__class__.__name__}.get_header` is deprecated, " + f"standard property `{self.__class__.__name__}.header` should be used instead", + PendingDeprecationWarning, + stacklevel=2, + ) + return self.header + + def set_header(self, header: Widget | None): + warnings.warn( + f"method `{self.__class__.__name__}.set_header` is deprecated, " + f"standard property `{self.__class__.__name__}.header` should be used instead", + PendingDeprecationWarning, + stacklevel=2, + ) + self.header = header + + @property + def body(self) -> Widget: return self._body - def set_body(self, body: Widget) -> None: + + @body.setter + def body(self, body: Widget) -> None: self._body = body self._invalidate() - body = property(get_body, set_body) - def get_footer(self) -> Widget | None: + def get_body(self) -> Widget: + warnings.warn( + f"method `{self.__class__.__name__}.get_body` is deprecated, " + f"standard property {self.__class__.__name__}.body should be used instead", + PendingDeprecationWarning, + stacklevel=2, + ) + return self.body + + def set_body(self, body: Widget) -> None: + warnings.warn( + f"method `{self.__class__.__name__}.set_body` is deprecated, " + f"standard property `{self.__class__.__name__}.body` should be used instead", + PendingDeprecationWarning, + stacklevel=2, + ) + self.body = body + + @property + def footer(self) -> Widget | None: return self._footer - def set_footer(self, footer: Widget | None) -> None: + @footer.setter + def footer(self, footer: Widget | None) -> None: self._footer = footer if footer is None and self.focus_part == 'footer': self.focus_part = 'body' self._invalidate() - footer = property(get_footer, set_footer) - def set_focus(self, part: Literal['header', 'footer', 'body']) -> None: + def get_footer(self) -> Widget | None: + warnings.warn( + f"method `{self.__class__.__name__}.get_footer` is deprecated, " + f"standard property `{self.__class__.__name__}.footer` should be used instead", + PendingDeprecationWarning, + stacklevel=2, + ) + return self.footer + + def set_footer(self, footer: Widget | None) -> None: + warnings.warn( + f"method `{self.__class__.__name__}.set_footer` is deprecated, " + f"standard property `{self.__class__.__name__}.footer` should be used instead", + PendingDeprecationWarning, + stacklevel=2, + ) + self.footer = footer + + @property + def focus_position(self) -> Literal['header', 'footer', 'body']: """ - Determine which part of the frame is in focus. + writeable property containing an indicator which part of the frame + that is in focus: `'body', 'header'` or `'footer'`. - .. note:: included for backwards compatibility. You should rather use - the container property :attr:`.focus_position` to set this value. + :returns: one of 'header', 'footer' or 'body'. + :rtype: str + """ + return self.focus_part + + @focus_position.setter + def focus_position(self, part: Literal['header', 'footer', 'body']) -> None: + """ + Determine which part of the frame is in focus. :param part: 'header', 'footer' or 'body' :type part: str """ if part not in ('header', 'footer', 'body'): raise IndexError(f'Invalid position for Frame: {part}') - if (part == 'header' and self._header is None) or ( - part == 'footer' and self._footer is None): + if (part == 'header' and self._header is None) or (part == 'footer' and self._footer is None): raise IndexError(f'This Frame has no {part}') self.focus_part = part self._invalidate() def get_focus(self) -> Literal['header', 'footer', 'body']: """ - Return an indicator which part of the frame is in focus + writeable property containing an indicator which part of the frame + that is in focus: `'body', 'header'` or `'footer'`. .. note:: included for backwards compatibility. You should rather use the container property :attr:`.focus_position` to get this value. @@ -999,24 +1251,54 @@ class Frame(Widget, WidgetContainerMixin): :returns: one of 'header', 'footer' or 'body'. :rtype: str """ - return self.focus_part + warnings.warn( + "included for backwards compatibility." + "You should rather use the container property `.focus_position` to get this value.", + PendingDeprecationWarning, + ) + return self.focus_position - def _get_focus(self) -> Widget: + def set_focus(self, part: Literal['header', 'footer', 'body']) -> None: + warnings.warn( + "included for backwards compatibility." + "You should rather use the container property `.focus_position` to set this value.", + PendingDeprecationWarning, + ) + self.focus_position = part + + @property + def focus(self) -> Widget: + """ + child :class:`Widget` in focus: the body, header or footer widget. + This is a read-only property.""" return { 'header': self._header, 'footer': self._footer, 'body': self._body }[self.focus_part] - focus = property(_get_focus, doc=""" - child :class:`Widget` in focus: the body, header or footer widget. - This is a read-only property.""") - focus_position = property(get_focus, set_focus, doc=""" - writeable property containing an indicator which part of the frame - that is in focus: `'body', 'header'` or `'footer'`. - """) + @property + def contents(self): + """ + a dict-like object similar to:: - def _contents(self): + { + 'body': (body_widget, None), + 'header': (header_widget, None), # if frame has a header + 'footer': (footer_widget, None) # if frame has a footer + } + + This object may be used to read or update the contents of the Frame. + + The values are similar to the list-like .contents objects used + in other containers with (:class:`Widget`, options) tuples, but are + constrained to keys for each of the three usual parts of a Frame. + When other keys are used a :exc:`KeyError` will be raised. + + Currently all options are `None`, but using the :meth:`options` method + to create the options value is recommended for forwards + compatibility. + """ class FrameContents: def __len__(inner_self): return len(inner_self.keys()) @@ -1080,33 +1362,21 @@ class Frame(Widget, WidgetContainerMixin): def _contents__delitem__(self, key: Literal['header', 'footer', 'body']): if key not in ('header', 'footer'): raise KeyError(f"Frame.contents can't remove key: {key!r}") - if (key == 'header' and self._header is None - ) or (key == 'footer' and self._footer is None): + if (key == 'header' and self._header is None) or (key == 'footer' and self._footer is None): raise KeyError(f"Frame.contents has no key: {key!r}") if key == 'header': self.header = None else: self.footer = None - contents = property(_contents, doc=""" - a dict-like object similar to:: - - { - 'body': (body_widget, None), - 'header': (header_widget, None), # if frame has a header - 'footer': (footer_widget, None) # if frame has a footer - } - - This object may be used to read or update the contents of the Frame. - - The values are similar to the list-like .contents objects used - in other containers with (:class:`Widget`, options) tuples, but are - constrained to keys for each of the three usual parts of a Frame. - When other keys are used a :exc:`KeyError` will be raised. - Currently all options are `None`, but using the :meth:`options` method - to create the options value is recommended for forwards - compatibility. - """) + def _contents(self): + warnings.warn( + f"method `{self.__class__.__name__}._contents` is deprecated, " + f"please use property `{self.__class__.__name__}.contents`", + DeprecationWarning, + stacklevel=2, + ) + return self.contents def options(self) -> None: """ @@ -1238,19 +1508,18 @@ class Frame(Widget, WidgetContainerMixin): return key return self.body.keypress( (maxcol, remaining), key ) - def mouse_event(self, size: tuple[str, str], event, button: int, col: int, row: int, focus: bool) -> bool | None: + def mouse_event(self, size: tuple[int, int], event, button: int, col: int, row: int, focus: bool) -> bool | None: """ Pass mouse event to appropriate part of frame. Focus may be changed on button 1 press. """ (maxcol, maxrow) = size - (htrim, ftrim),(hrows, frows) = self.frame_top_bottom((maxcol, maxrow), focus) + (htrim, ftrim), (hrows, frows) = self.frame_top_bottom((maxcol, maxrow), focus) if row < htrim: # within header focus = focus and self.focus_part == 'header' - if is_mouse_press(event) and button==1: - if self.header.selectable(): - self.set_focus('header') + if is_mouse_press(event) and button == 1 and self.header.selectable(): + self.focus_position = 'header' if not hasattr(self.header, 'mouse_event'): return False return self.header.mouse_event( (maxcol,), event, @@ -1258,9 +1527,8 @@ class Frame(Widget, WidgetContainerMixin): if row >= maxrow-ftrim: # within footer focus = focus and self.focus_part == 'footer' - if is_mouse_press(event) and button==1: - if self.footer.selectable(): - self.set_focus('footer') + if is_mouse_press(event) and button == 1 and self.footer.selectable(): + self.focus_position = 'footer' if not hasattr(self.footer, 'mouse_event'): return False return self.footer.mouse_event( (maxcol,), event, @@ -1270,7 +1538,7 @@ class Frame(Widget, WidgetContainerMixin): focus = focus and self.focus_part == 'body' if is_mouse_press(event) and button==1: if self.body.selectable(): - self.set_focus('body') + self.focus_position = 'body' if not hasattr(self.body, 'mouse_event'): return False @@ -1391,7 +1659,7 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): focus_item = i if self.contents and focus_item is not None: - self.set_focus(focus_item) + self.focus_position = focus_item self.pref_col = 0 @@ -1412,16 +1680,29 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): except (TypeError, ValueError): raise PileError(f"added content invalid: {item!r}") - def _get_widget_list(self): + @property + def widget_list(self): + """ + A list of the widgets in this Pile + + .. note:: only for backwards compatibility. You should use the new + standard container property :attr:`contents`. + """ + warnings.warn( + "only for backwards compatibility. You should use the new standard container property `contents`", + PendingDeprecationWarning, + stacklevel=2, + ) ml = MonitoredList(w for w, t in self.contents) def user_modified(): - self._set_widget_list(ml) + self.widget_list = ml ml.set_modified_callback(user_modified) return ml - def _set_widget_list(self, widgets): + @widget_list.setter + def widget_list(self, widgets): focus_position = self.focus_position self.contents = [ (new, options) for (new, (w, options)) in zip(widgets, @@ -1429,25 +1710,37 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): chain(self.contents, repeat((None, (WEIGHT, 1)))))] if focus_position < len(widgets): self.focus_position = focus_position - widget_list = property(_get_widget_list, _set_widget_list, doc=""" - A list of the widgets in this Pile + + @property + def item_types(self): + """ + A list of the options values for widgets in this Pile. .. note:: only for backwards compatibility. You should use the new standard container property :attr:`contents`. - """) - - def _get_item_types(self): + """ + warnings.warn( + "only for backwards compatibility. You should use the new standard container property `contents`", + PendingDeprecationWarning, + stacklevel=2, + ) ml = MonitoredList( # return the old item type names ({GIVEN: FIXED, PACK: FLOW}.get(f, f), height) for w, (f, height) in self.contents) def user_modified(): - self._set_item_types(ml) + self.item_types = ml ml.set_modified_callback(user_modified) return ml - def _set_item_types(self, item_types): + @item_types.setter + def item_types(self, item_types): + warnings.warn( + "only for backwards compatibility. You should use the new standard container property `contents`", + PendingDeprecationWarning, + stacklevel=2, + ) focus_position = self.focus_position self.contents = [ (w, ({FIXED: GIVEN, FLOW: PACK}.get(new_t, new_t), new_height)) @@ -1455,18 +1748,10 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): in zip(item_types, self.contents)] if focus_position < len(item_types): self.focus_position = focus_position - item_types = property(_get_item_types, _set_item_types, doc=""" - A list of the options values for widgets in this Pile. - - .. note:: only for backwards compatibility. You should use the new - standard container property :attr:`contents`. - """) - def _get_contents(self): - return self._contents - def _set_contents(self, c): - self._contents[:] = c - contents = property(_get_contents, _set_contents, doc=""" + @property + def contents(self): + """ The contents of this Pile as a list of (widget, options) tuples. options currently may be one of @@ -1491,7 +1776,12 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): will updated automatically. .. seealso:: Create new options tuples with the :meth:`options` method - """) + """ + return self._contents + + @contents.setter + def contents(self, c): + self._contents[:] = c @staticmethod def options( @@ -1512,7 +1802,15 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): raise PileError(f'invalid height_type: {height_type!r}') return (height_type, height_amount) - def set_focus(self, item: Widget | int) -> None: + @property + def focus(self) -> Widget | None: + """the child widget in focus or None when Pile is empty""" + if not self.contents: + return None + return self.contents[self.focus_position][0] + + @focus.setter + def focus(self, item: Widget | int) -> None: """ Set the item in focus, for backwards compatibility. @@ -1524,7 +1822,8 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): :type item: Widget or int """ if isinstance(item, int): - return self._set_focus_position(item) + self.focus_position = item + return for i, (w, options) in enumerate(self.contents): if item == w: self.focus_position = i @@ -1537,32 +1836,57 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): also use the new standard container property .focus to get the child widget in focus. """ - if not self.contents: - return None - return self.contents[self.focus_position][0] - focus = property(get_focus, - doc="the child widget in focus or None when Pile is empty") - - focus_item = property(get_focus, set_focus, doc=""" - A property for reading and setting the widget in focus. - - .. note:: + warnings.warn( + "for backwards compatibility." + "You may also use the new standard container property .focus to get the child widget in focus.", + PendingDeprecationWarning, + stacklevel=2, + ) + return self.focus - only for backwards compatibility. You should use the new - standard container properties :attr:`focus` and - :attr:`focus_position` to get the child widget in focus or modify the - focus position. - """) + def set_focus(self, item: Widget | int) -> None: + warnings.warn( + "for backwards compatibility." + "You may also use the new standard container property .focus to get the child widget in focus.", + PendingDeprecationWarning, + stacklevel=2, + ) + self.focus = item + + @property + def focus_item(self): + warnings.warn( + "only for backwards compatibility." + "You should use the new standard container properties " + "`focus` and `focus_position` to get the child widget in focus or modify the focus position.", + DeprecationWarning, + stacklevel=2, + ) + return self.focus + + @focus_item.setter + def focus_item(self, new_item): + warnings.warn( + "only for backwards compatibility." + "You should use the new standard container properties " + "`focus` and `focus_position` to get the child widget in focus or modify the focus position.", + DeprecationWarning, + stacklevel=2, + ) + self.focus = new_item - def _get_focus_position(self) -> int: + @property + def focus_position(self) -> int: """ - Return the index of the widget in focus or None if this Pile is - empty. + index of child widget in focus. + Raises :exc:`IndexError` if read when Pile is empty, or when set to an invalid index. """ if not self.contents: raise IndexError("No focus_position, Pile is empty") return self.contents.focus - def _set_focus_position(self, position: int) -> None: + + @focus_position.setter + def focus_position(self, position: int) -> None: """ Set the widget in focus. @@ -1574,10 +1898,6 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): except (TypeError, IndexError): raise IndexError(f"No Pile child widget at position {position}") self.contents.focus = position - focus_position = property(_get_focus_position, _set_focus_position, doc=""" - index of child widget in focus. Raises :exc:`IndexError` if read when - Pile is empty, or when set to an invalid index. - """) def get_pref_col(self, size): """Return the preferred column for the cursor, or None.""" @@ -1625,7 +1945,7 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): if f == GIVEN: l.append(height) else: - l.append(w.rows((maxcol,), focus=focus and self.focus_item == w)) + l.append(w.rows((maxcol,), focus=focus and self.focus == w)) return l # pile is a box widget @@ -1633,7 +1953,7 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): wtotal = 0 for w, (f, height) in self.contents: if f == PACK: - rows = w.rows((maxcol,), focus=focus and self.focus_item == w) + rows = w.rows((maxcol,), focus=focus and self.focus == w) l.append(rows) remaining -= rows elif f == GIVEN: @@ -1666,7 +1986,7 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): combinelist = [] for i, (w, (f, height)) in enumerate(self.contents): - item_focus = self.focus_item == w + item_focus = self.focus == w canv = None if f == GIVEN: canv = w.render((maxcol, height), focus=focus and item_focus) @@ -1694,7 +2014,7 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): """Return the cursor coordinates of the focus widget.""" if not self.selectable(): return None - if not hasattr(self.focus_item, 'get_cursor_coords'): + if not hasattr(self.focus, 'get_cursor_coords'): return None i = self.focus_position @@ -1708,9 +2028,9 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): if item_rows is None: item_rows = self.get_item_rows(size, focus=True) maxrow = item_rows[i] - coords = self.focus_item.get_cursor_coords((maxcol, maxrow)) + coords = self.focus.get_cursor_coords((maxcol, maxrow)) else: - coords = self.focus_item.get_cursor_coords((maxcol,)) + coords = self.focus.get_cursor_coords((maxcol,)) if coords is None: return None @@ -1766,8 +2086,7 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): rowlist = list(range(rows)) for row in rowlist: tsize = self.get_item_size(size, j, True, item_rows) - if self.focus_item.move_cursor_to_coords( - tsize, self.pref_col, row): + if self.focus.move_cursor_to_coords(tsize, self.pref_col, row): break return @@ -1835,7 +2154,7 @@ class Pile(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): else: return False - focus = focus and self.focus_item == w + focus = focus and self.focus == w if is_mouse_press(event) and button == 1: if w.selectable(): self.focus_position = i @@ -1956,14 +2275,33 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): except (TypeError, ValueError): raise ColumnsError(f"added content invalid {item!r}") - def _get_widget_list(self) -> MonitoredList: + @property + def widget_list(self) -> MonitoredList: + """ + A list of the widgets in this Columns + + .. note:: only for backwards compatibility. You should use the new + standard container property :attr:`contents`. + """ + warnings.warn( + "only for backwards compatibility. You should use the new standard container `contents`", + PendingDeprecationWarning, + stacklevel=2 + ) ml = MonitoredList(w for w, t in self.contents) + def user_modified(): - self._set_widget_list(ml) + self.widget_list = ml ml.set_modified_callback(user_modified) return ml - def _set_widget_list(self, widgets): + @widget_list.setter + def widget_list(self, widgets): + warnings.warn( + "only for backwards compatibility. You should use the new standard container `contents`", + PendingDeprecationWarning, + stacklevel=2 + ) focus_position = self.focus_position self.contents = [ # need to grow contents list if widgets is longer @@ -1974,24 +2312,38 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): ] if focus_position < len(widgets): self.focus_position = focus_position - widget_list = property(_get_widget_list, _set_widget_list, doc=""" - A list of the widgets in this Columns - .. note:: only for backwards compatibility. You should use the new - standard container property :attr:`contents`. - """) - - def _get_column_types(self) -> MonitoredList: + @property + def column_types(self) -> MonitoredList: + """ + A list of the old partial options values for widgets in this Pile, + for backwards compatibility only. You should use the new standard + container property .contents to modify Pile contents. + """ + warnings.warn( + "for backwards compatibility only." + "You should use the new standard container property .contents to modify Pile contents.", + PendingDeprecationWarning, + stacklevel=2, + ) ml = MonitoredList( # return the old column type names ({GIVEN: FIXED, PACK: FLOW}.get(t, t), n) for w, (t, n, b) in self.contents) + def user_modified(): - self._set_column_types(ml) + self.column_types = ml ml.set_modified_callback(user_modified) return ml - def _set_column_types(self, column_types): + @column_types.setter + def column_types(self, column_types): + warnings.warn( + "for backwards compatibility only." + "You should use the new standard container property .contents to modify Pile contents.", + PendingDeprecationWarning, + stacklevel=2, + ) focus_position = self.focus_position self.contents = [ (w, ({FIXED: GIVEN, FLOW: PACK}.get(new_t, new_t), new_n, b)) @@ -2000,57 +2352,69 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): ] if focus_position < len(column_types): self.focus_position = focus_position - column_types = property(_get_column_types, _set_column_types, doc=""" - A list of the old partial options values for widgets in this Pile, - for backwards compatibility only. You should use the new standard - container property .contents to modify Pile contents. - """) - def _get_box_columns(self) -> MonitoredList: + @property + def box_columns(self) -> MonitoredList: + """ + A list of the indexes of the columns that are to be treated as + box widgets when the Columns is treated as a flow widget. + + .. note:: only for backwards compatibility. You should use the new + standard container property :attr:`contents`. + """ + warnings.warn( + "only for backwards compatibility." + "You should use the new standard container property `contents`", + PendingDeprecationWarning, + stacklevel=2, + ) ml = MonitoredList( i for i, (w, (t, n, b)) in enumerate(self.contents) if b) + def user_modified(): - self._set_box_columns(ml) + self.box_columns = ml ml.set_modified_callback(user_modified) return ml - def _set_box_columns(self, box_columns): + @box_columns.setter + def box_columns(self, box_columns): + warnings.warn( + "only for backwards compatibility." + "You should use the new standard container property `contents`", + PendingDeprecationWarning, + stacklevel=2, + ) box_columns = set(box_columns) self.contents = [ (w, (t, n, i in box_columns)) for (i, (w, (t, n, b))) in enumerate(self.contents)] - box_columns = property(_get_box_columns, _set_box_columns, doc=""" - A list of the indexes of the columns that are to be treated as - box widgets when the Columns is treated as a flow widget. - .. note:: only for backwards compatibility. You should use the new - standard container property :attr:`contents`. - """) - - def _get_has_pack_type(self) -> bool: - import warnings + @property + def has_flow_type(self) -> bool: + """ + .. deprecated:: 1.0 Read values from :attr:`contents` instead. + """ warnings.warn(".has_flow_type is deprecated, read values from .contents instead.", DeprecationWarning) return PACK in self.column_types - def _set_has_pack_type(self, value): - import warnings + @has_flow_type.setter + def has_flow_type(self, value): warnings.warn(".has_flow_type is deprecated, read values from .contents instead.", DeprecationWarning) - has_flow_type = property(_get_has_pack_type, _set_has_pack_type, doc=""" - .. deprecated:: 1.0 Read values from :attr:`contents` instead. - """) - def _get_contents(self): - return self._contents - - def _set_contents(self, c): - self._contents[:] = c - contents = property(_get_contents, _set_contents, doc=""" + @property + def contents(self): + """ The contents of this Columns as a list of `(widget, options)` tuples. This list may be modified like a normal list and the Columns widget will update automatically. .. seealso:: Create new options tuples with the :meth:`options` method - """) + """ + return self._contents + + @contents.setter + def contents(self, c): + self._contents[:] = c @staticmethod def options( @@ -2099,7 +2463,13 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): .. note:: only for backwards compatibility. You may also use the new standard container property :attr:`focus_position` to set the focus. """ - self._set_focus_position(num) + warnings.warn( + "only for backwards compatibility." + "You may also use the new standard container property `focus_position`", + PendingDeprecationWarning, + stacklevel=2, + ) + self.focus_position = num def get_focus_column(self) -> int: """ @@ -2108,6 +2478,12 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): .. note:: only for backwards compatibility. You may also use the new standard container property :attr:`focus_position` to get the focus. """ + warnings.warn( + "only for backwards compatibility." + "You may also use the new standard container property `focus_position`", + PendingDeprecationWarning, + stacklevel=2, + ) return self.focus_position def set_focus(self, item: Widget | int) -> None: @@ -2118,16 +2494,26 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): standard container property :attr:`focus_position` to get the focus. :param item: widget or integer index""" + warnings.warn( + "only for backwards compatibility." + "You may also use the new standard container property `focus_position` to get the focus.", + PendingDeprecationWarning, + stacklevel=2, + ) if isinstance(item, int): - return self._set_focus_position(item) + self.focus_position = item + return for i, (w, options) in enumerate(self.contents): if item == w: self.focus_position = i return raise ValueError(f"Widget not found in Columns contents: {item!r}") - def get_focus(self) -> Widget | None: + @property + def focus(self) -> Widget | None: """ + the child widget in focus or None when Columns is empty + Return the widget in focus, for backwards compatibility. You may also use the new standard container property .focus to get the child widget in focus. @@ -2135,19 +2521,34 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): if not self.contents: return None return self.contents[self.focus_position][0] - focus = property(get_focus, - doc="the child widget in focus or None when Columns is empty") - def _get_focus_position(self) -> int | None: + def get_focus(self): """ - Return the index of the widget in focus or None if this Columns is - empty. + Return the widget in focus, for backwards compatibility. + + .. note:: only for backwards compatibility. You may also use the new + standard container property :attr:`focus` to get the focus. + """ + warnings.warn( + "only for backwards compatibility." + "You may also use the new standard container property `focus` to get the focus.", + PendingDeprecationWarning, + stacklevel=2, + ) + return self.focus + + @property + def focus_position(self) -> int | None: + """ + index of child widget in focus. + Raises :exc:`IndexError` if read when Columns is empty, or when set to an invalid index. """ if not self.widget_list: raise IndexError("No focus_position, Columns is empty") return self.contents.focus - def _set_focus_position(self, position: int) -> None: + @focus_position.setter + def focus_position(self, position: int) -> None: """ Set the widget in focus. @@ -2159,18 +2560,33 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): except (TypeError, IndexError): raise IndexError(f"No Columns child widget at position {position}") self.contents.focus = position - focus_position = property(_get_focus_position, _set_focus_position, doc=""" - index of child widget in focus. Raises :exc:`IndexError` if read when - Columns is empty, or when set to an invalid index. - """) - focus_col = property(_get_focus_position, _set_focus_position, doc=""" + @property + def focus_col(self): + """ A property for reading and setting the index of the column in focus. .. note:: only for backwards compatibility. You may also use the new standard container property :attr:`focus_position` to get the focus. - """) + """ + warnings.warn( + "only for backwards compatibility." + "You may also use the new standard container property `focus_position` to get the focus.", + PendingDeprecationWarning, + stacklevel=2, + ) + return self.focus_position + + @focus_col.setter + def focus_col(self, new_position) -> None: + warnings.warn( + "only for backwards compatibility." + "You may also use the new standard container property `focus_position` to get the focus.", + PendingDeprecationWarning, + stacklevel=2, + ) + self.focus_position = new_position def column_widths(self, size: tuple[int] | tuple[int, int], focus: bool = False) -> list[int]: """ @@ -2370,14 +2786,14 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): for i, (width, (w, (t, n, b))) in enumerate(zip(widths, self.contents)): if col < x: return False - w = self.widget_list[i] + w = self.contents[i][0] end = x + width if col >= end: x = end + self.dividechars continue - focus = focus and self.focus_col == i + focus = focus and self.focus_position == i if is_mouse_press(event) and button == 1 and w.selectable(): self.focus_position = i @@ -2404,7 +2820,7 @@ class Columns(Widget, WidgetContainerMixin, WidgetContainerListContentsMixin): else: col = w.get_pref_col((cwidth,) + size[1:]) if isinstance(col, int): - col += self.focus_col * self.dividechars + col += self.focus_position * self.dividechars col += sum(widths[:self.focus_position]) if col is None: col = self.pref_col diff --git a/urwid/decoration.py b/urwid/decoration.py index f1ab16c..ff7a5a0 100755 --- a/urwid/decoration.py +++ b/urwid/decoration.py @@ -23,6 +23,7 @@ from __future__ import annotations import typing +import warnings from collections.abc import Hashable, Mapping from urwid.canvas import CompositeCanvas, SolidCanvas @@ -77,15 +78,35 @@ class WidgetDecoration(Widget): # "decorator" was already taken def _repr_words(self): return super()._repr_words() + [repr(self._original_widget)] - def _get_original_widget(self) -> Widget: + @property + def original_widget(self) -> Widget: return self._original_widget - def _set_original_widget(self, original_widget): + @original_widget.setter + def original_widget(self, original_widget: Widget) -> None: self._original_widget = original_widget self._invalidate() - original_widget = property(_get_original_widget, _set_original_widget) - def _get_base_widget(self): + def _get_original_widget(self) -> Widget: + warnings.warn( + f"Method `{self.__class__.__name__}._get_original_widget` is deprecated, " + f"please use property `{self.__class__.__name__}.original_widget`", + DeprecationWarning, + stacklevel=2, + ) + return self.original_widget + + def _set_original_widget(self, original_widget): + warnings.warn( + f"Method `{self.__class__.__name__}._set_original_widget` is deprecated, " + f"please use property `{self.__class__.__name__}.original_widget`", + DeprecationWarning, + stacklevel=2, + ) + self.original_widget = original_widget + + @property + def base_widget(self) -> Widget: """ Return the widget without decorations. If there is only one Decoration then this is the same as original_widget. @@ -104,7 +125,14 @@ class WidgetDecoration(Widget): # "decorator" was already taken w = w._original_widget return w - base_widget = property(_get_base_widget) + def _get_base_widget(self): + warnings.warn( + f"Method `{self.__class__.__name__}._get_base_widget` is deprecated, " + f"please use property `{self.__class__.__name__}.base_widget`", + DeprecationWarning, + stacklevel=2, + ) + return self.base_widget def selectable(self) -> bool: return self._original_widget.selectable() @@ -299,10 +327,40 @@ class AttrWrap(AttrMap): d['focus_attr'] = self.focus_attr return d - # backwards compatibility, widget used to be stored as w - get_w = WidgetDecoration._get_original_widget - set_w = WidgetDecoration._set_original_widget - w = property(get_w, set_w) + @property + def w(self) -> Widget: + """backwards compatibility, widget used to be stored as w""" + warnings.warn( + "backwards compatibility, widget used to be stored as w", + PendingDeprecationWarning, + stacklevel=2, + ) + return self.original_widget + + @w.setter + def w(self, new_widget: Widget) -> None: + warnings.warn( + "backwards compatibility, widget used to be stored as w", + PendingDeprecationWarning, + stacklevel=2, + ) + self.original_widget = new_widget + + def get_w(self): + warnings.warn( + "backwards compatibility, widget used to be stored as w", + DeprecationWarning, + stacklevel=2, + ) + return self.original_widget + + def set_w(self, new_widget: Widget) -> None: + warnings.warn( + "backwards compatibility, widget used to be stored as w", + DeprecationWarning, + stacklevel=2, + ) + self.original_widget = new_widget def get_attr(self): return self.attr_map[None] @@ -387,7 +445,23 @@ class BoxAdapter(WidgetDecoration): return dict(super()._repr_attrs(), height=self.height) # originally stored as box_widget, keep for compatibility - box_widget = property(WidgetDecoration._get_original_widget, WidgetDecoration._set_original_widget) + @property + def box_widget(self) -> Widget: + warnings.warn( + "originally stored as box_widget, keep for compatibility", + PendingDeprecationWarning, + stacklevel=2, + ) + return self.original_widget + + @box_widget.setter + def box_widget(self, widget: Widget): + warnings.warn( + "originally stored as box_widget, keep for compatibility", + PendingDeprecationWarning, + stacklevel=2, + ) + self.original_widget = widget def sizing(self): return {FLOW} @@ -571,33 +645,71 @@ class Padding(WidgetDecoration): min_width=self.min_width) return remove_defaults(attrs, Padding.__init__) - def _get_align(self) -> Literal['left', 'center', 'right'] | tuple[Literal['relative'], int]: + @property + def align(self) -> Literal['left', 'center', 'right'] | tuple[Literal['relative'], int]: """ Return the padding alignment setting. """ return simplify_align(self._align_type, self._align_amount) - def _set_align(self, align: Literal['left', 'center', 'right'] | tuple[Literal['relative'], int]) -> None: + @align.setter + def align(self, align: Literal['left', 'center', 'right'] | tuple[Literal['relative'], int]) -> None: """ Set the padding alignment. """ self._align_type, self._align_amount = normalize_align(align, PaddingError) self._invalidate() - align = property(_get_align, _set_align) - def _get_width(self) -> Literal['clip', 'pack'] | int | tuple[Literal['relative'], int]: + def _get_align(self) -> Literal['left', 'center', 'right'] | tuple[Literal['relative'], int]: + warnings.warn( + f"Method `{self.__class__.__name__}._get_align` is deprecated, " + f"please use property `{self.__class__.__name__}.align`", + DeprecationWarning, + stacklevel=2, + ) + return self.align + + def _set_align(self, align: Literal['left', 'center', 'right'] | tuple[Literal['relative'], int]) -> None: + warnings.warn( + f"Method `{self.__class__.__name__}._set_align` is deprecated, " + f"please use property `{self.__class__.__name__}.align`", + DeprecationWarning, + stacklevel=2, + ) + self.align = align + + @property + def width(self) -> Literal['clip', 'pack'] | int | tuple[Literal['relative'], int]: """ Return the padding width. """ return simplify_width(self._width_type, self._width_amount) - def _set_width(self, width: Literal['clip', 'pack'] | int | tuple[Literal['relative'], int]) -> None: + @width.setter + def width(self, width: Literal['clip', 'pack'] | int | tuple[Literal['relative'], int]) -> None: """ Set the padding width. """ self._width_type, self._width_amount = normalize_width(width, PaddingError) self._invalidate() - width = property(_get_width, _set_width) + + def _get_width(self) -> Literal['clip', 'pack'] | int | tuple[Literal['relative'], int]: + warnings.warn( + f"Method `{self.__class__.__name__}._get_width` is deprecated, " + f"please use property `{self.__class__.__name__}.width`", + DeprecationWarning, + stacklevel=2, + ) + return self.width + + def _set_width(self, width: Literal['clip', 'pack'] | int | tuple[Literal['relative'], int]) -> None: + warnings.warn( + f"Method `{self.__class__.__name__}._set_width` is deprecated, " + f"please use property `{self.__class__.__name__}.width`", + DeprecationWarning, + stacklevel=2, + ) + self.width = width def render( self, @@ -847,10 +959,41 @@ class Filler(WidgetDecoration): min_height=self.min_height) return remove_defaults(attrs, Filler.__init__) - # backwards compatibility, widget used to be stored as body - get_body = WidgetDecoration._get_original_widget - set_body = WidgetDecoration._set_original_widget - body = property(get_body, set_body) + @property + def body(self): + """backwards compatibility, widget used to be stored as body""" + warnings.warn( + "backwards compatibility, widget used to be stored as body", + PendingDeprecationWarning, + stacklevel=2, + ) + return self.original_widget + + @body.setter + def body(self, new_body): + warnings.warn( + "backwards compatibility, widget used to be stored as body", + PendingDeprecationWarning, + stacklevel=2, + ) + self.original_widget = new_body + + def get_body(self): + """backwards compatibility, widget used to be stored as body""" + warnings.warn( + "backwards compatibility, widget used to be stored as body", + DeprecationWarning, + stacklevel=2, + ) + return self.original_widget + + def set_body(self, new_body): + warnings.warn( + "backwards compatibility, widget used to be stored as body", + DeprecationWarning, + stacklevel=2, + ) + self.original_widget = new_body def selectable(self) -> bool: """Return selectable from body.""" diff --git a/urwid/display_common.py b/urwid/display_common.py index a4636fe..30ece96 100755 --- a/urwid/display_common.py +++ b/urwid/display_common.py @@ -610,7 +610,8 @@ class AttrSpec: def strikethrough(self) -> bool: return self._value & _STRIKETHROUGH != 0 - def _colors(self) -> int: + @property + def colors(self) -> int: """ Return the maximum colors required for this object. @@ -625,7 +626,15 @@ class AttrSpec: if self._value & (_BG_BASIC_COLOR | _FG_BASIC_COLOR): return 16 return 1 - colors = property(_colors) + + def _colors(self) -> int: + warnings.warn( + f"Method `{self.__class__.__name__}._colors` is deprecated, " + f"please use property `{self.__class__.__name__}.colors`", + DeprecationWarning, + stacklevel=2, + ) + return self.colors def __repr__(self) -> str: """ @@ -650,7 +659,8 @@ class AttrSpec: return _color_desc_true(self.foreground_number) return _color_desc_256(self.foreground_number) - def _foreground(self) -> str: + @property + def foreground(self) -> str: return ( self._foreground_color() + ',bold' * self.bold @@ -661,7 +671,8 @@ class AttrSpec: + ',strikethrough' * self.strikethrough ) - def _set_foreground(self, foreground: str) -> None: + @foreground.setter + def foreground(self, foreground: str) -> None: color = None flags = 0 # handle comma-separated foreground @@ -699,9 +710,26 @@ class AttrSpec: color = 0 self._value = (self._value & ~_FG_MASK) | color | flags - foreground = property(_foreground, _set_foreground) + def _foreground(self) -> str: + warnings.warn( + f"Method `{self.__class__.__name__}._foreground` is deprecated, " + f"please use property `{self.__class__.__name__}.foreground`", + DeprecationWarning, + stacklevel=2, + ) + return self.foreground - def _background(self) -> str: + def _set_foreground(self, foreground: str) -> None: + warnings.warn( + f"Method `{self.__class__.__name__}._set_foreground` is deprecated, " + f"please use property `{self.__class__.__name__}.foreground`", + DeprecationWarning, + stacklevel=2, + ) + self.foreground = foreground + + @property + def background(self) -> str: """Return the background color.""" if not (self.background_basic or self.background_high or self.background_true): return 'default' @@ -713,7 +741,8 @@ class AttrSpec: return _color_desc_true(self.background_number) return _color_desc_256(self.background_number) - def _set_background(self, background: str) -> None: + @background.setter + def background(self, background: str) -> None: flags = 0 if background in ('', 'default'): color = 0 @@ -733,7 +762,23 @@ class AttrSpec: raise AttrSpecError(f"Unrecognised color specification in background ({background!r})") self._value = (self._value & ~_BG_MASK) | (color << _BG_SHIFT) | flags - background = property(_background, _set_background) + def _background(self) -> str: + warnings.warn( + f"Method `{self.__class__.__name__}._background` is deprecated, " + f"please use property `{self.__class__.__name__}.background`", + DeprecationWarning, + stacklevel=2, + ) + return self.background + + def _set_background(self, background: str) -> None: + warnings.warn( + f"Method `{self.__class__.__name__}._set_background` is deprecated, " + f"please use property `{self.__class__.__name__}.background`", + DeprecationWarning, + stacklevel=2, + ) + self.background = background def get_rgb_values(self): """ diff --git a/urwid/graphics.py b/urwid/graphics.py index b3356a4..310b9a5 100755 --- a/urwid/graphics.py +++ b/urwid/graphics.py @@ -23,6 +23,7 @@ from __future__ import annotations import typing +import warnings from urwid.canvas import CanvasCombine, CanvasJoin, CompositeCanvas, SolidCanvas, TextCanvas from urwid.container import Columns, Pile @@ -942,13 +943,26 @@ class ProgressBar(Widget): self._invalidate() current = property(lambda self: self._current, set_completion) - def _set_done(self, done): + @property + def done(self): + return self._done + + @done.setter + def done(self, done): """ done -- progress amount at 100% """ self._done = done self._invalidate() - done = property(lambda self: self._done, _set_done) + + def _set_done(self, done): + warnings.warn( + f"Method `{self.__class__.__name__}._set_done` is deprecated, " + f"please use property `{self.__class__.__name__}.done`", + DeprecationWarning, + stacklevel=2, + ) + self.done = done def rows(self, size, focus: bool = False) -> int: return 1 diff --git a/urwid/listbox.py b/urwid/listbox.py index d4dddc4..a8cd91c 100644 --- a/urwid/listbox.py +++ b/urwid/listbox.py @@ -23,6 +23,7 @@ from __future__ import annotations import typing +import warnings from collections.abc import MutableSequence from urwid import signals @@ -118,14 +119,23 @@ class SimpleListWalker(MonitoredList, ListWalker): self.focus = 0 self.wrap_around = wrap_around - def _get_contents(self): + @property + def contents(self): """ Return self. Provides compatibility with old SimpleListWalker class. """ return self - contents = property(_get_contents) + + def _get_contents(self): + warnings.warn( + f"Method `{self.__class__.__name__}._get_contents` is deprecated, " + f"please use property`{self.__class__.__name__}.contents`", + DeprecationWarning, + stacklevel=2, + ) + return self.contents def _modified(self): if self.focus >= len(self): @@ -257,14 +267,14 @@ class ListBox(Widget, WidgetContainerMixin): _selectable = True _sizing = frozenset([BOX]) - def __init__(self, body): + def __init__(self, body: ListWalker): """ :param body: a ListWalker subclass such as :class:`SimpleFocusListWalker` that contains widgets to be displayed inside the list box :type body: ListWalker """ - self._set_body(body) + self.body = body # offset_rows is the number of rows between the top of the view # and the top of the focused item @@ -284,10 +294,16 @@ class ListBox(Widget, WidgetContainerMixin): # variable for delayed valign change used by set_focus_valign self.set_focus_valign_pending = None - def _get_body(self): + @property + def body(self): + """ + a ListWalker subclass such as :class:`SimpleFocusListWalker` that contains + widgets to be displayed inside the list box + """ return self._body - def _set_body(self, body): + @body.setter + def body(self, body): try: disconnect_signal(self._body, "modified", self._invalidate) except AttributeError: @@ -306,10 +322,23 @@ class ListBox(Widget, WidgetContainerMixin): self.render = nocache_widget_render_instance(self) self._invalidate() - body = property(_get_body, _set_body, doc=""" - a ListWalker subclass such as :class:`SimpleFocusListWalker` that contains - widgets to be displayed inside the list box - """) + def _get_body(self): + warnings.warn( + f"Method `{self.__class__.__name__}._get_body` is deprecated, " + f"please use property `{self.__class__.__name__}.body`", + DeprecationWarning, + stacklevel=2, + ) + return self.body + + def _set_body(self, body): + warnings.warn( + f"Method `{self.__class__.__name__}._set_body` is deprecated, " + f"please use property `{self.__class__.__name__}.body`", + DeprecationWarning, + stacklevel=2, + ) + self.body = body def calculate_visible(self, size: tuple[int, int], focus: bool = False): """ @@ -591,13 +620,14 @@ class ListBox(Widget, WidgetContainerMixin): """ return self._body.get_focus() - def _get_focus(self): + @property + def focus(self): """ + the child widget in focus or None when ListBox is empty. + Return the widget in focus according to our :obj:`list walker <ListWalker>`. """ return self._body.get_focus()[0] - focus = property(_get_focus, - doc="the child widget in focus or None when ListBox is empty") def _get_focus_position(self): """ diff --git a/urwid/main_loop.py b/urwid/main_loop.py index 7f93f2e..5cc7514 100755 --- a/urwid/main_loop.py +++ b/urwid/main_loop.py @@ -31,6 +31,7 @@ import signal import sys import time import typing +import warnings from collections.abc import Callable, Iterable from functools import wraps from itertools import count @@ -154,25 +155,51 @@ class MainLoop: self._watch_pipes = {} - def _set_widget(self, widget): + @property + def widget(self) -> Widget: + """ + Property for the topmost widget used to draw the screen. + This must be a box widget. + """ + return self._widget + + @widget.setter + def widget(self, widget: Widget) -> None: self._widget = widget if self.pop_ups: self._topmost_widget.original_widget = self._widget else: self._topmost_widget = self._widget - widget = property(lambda self:self._widget, _set_widget, doc= - """ - Property for the topmost widget used to draw the screen. - This must be a box widget. - """) - def _set_pop_ups(self, pop_ups): + def _set_widget(self, widget: Widget) -> None: + warnings.warn( + f"method `{self.__class__.__name__}._set_widget` is deprecated, " + f"please use `{self.__class__.__name__}.widget` property", + DeprecationWarning, + stacklevel=2, + ) + self.widget = widget + + @property + def pop_ups(self): + return self._pop_ups + + @pop_ups.setter + def pop_ups(self, pop_ups) -> None: self._pop_ups = pop_ups if pop_ups: self._topmost_widget = PopUpTarget(self._widget) else: self._topmost_widget = self._widget - pop_ups = property(lambda self:self._pop_ups, _set_pop_ups) + + def _set_pop_ups(self, pop_ups) -> None: + warnings.warn( + f"method `{self.__class__.__name__}._set_pop_ups` is deprecated, " + f"please use `{self.__class__.__name__}.pop_ups` property", + DeprecationWarning, + stacklevel=2, + ) + self.pop_ups = pop_ups def set_alarm_in(self, sec, callback, user_data=None): """ @@ -1039,7 +1066,6 @@ class GLibEventLoop(EventLoop): except ExitMainLoop: self._loop.quit() except: - import sys self._exc_info = sys.exc_info() if self._loop.is_running(): self._loop.quit() @@ -1377,7 +1403,6 @@ class TwistedEventLoop(EventLoop): if self.manage_reactor: self.reactor.stop() except: - import sys print(sys.exc_info()) self._exc_info = sys.exc_info() if self.manage_reactor: diff --git a/urwid/monitored_list.py b/urwid/monitored_list.py index 3d6d6fb..1a91495 100755 --- a/urwid/monitored_list.py +++ b/urwid/monitored_list.py @@ -23,6 +23,7 @@ from __future__ import annotations import functools import typing +import warnings from collections.abc import Callable if typing.TYPE_CHECKING: @@ -131,20 +132,26 @@ class MonitoredFocusList(MonitoredList): def __repr__(self): return f"{self.__class__.__name__}({list(self)!r}, focus={self.focus!r})" - def _get_focus(self) -> int | None: + @property + def focus(self) -> int | None: """ + Get/set the focus index. This value is read as None when the list + is empty, and may only be set to a value between 0 and len(self)-1 + or an IndexError will be raised. + Return the index of the item "in focus" or None if the list is empty. - >>> MonitoredFocusList([1,2,3], focus=2)._get_focus() + >>> MonitoredFocusList([1,2,3], focus=2).focus 2 - >>> MonitoredFocusList()._get_focus() + >>> MonitoredFocusList().focus """ if not self: return None return self._focus - def _set_focus(self, index: int) -> None: + @focus.setter + def focus(self, index: int) -> None: """ index -- index into this list, any index out of range will raise an IndexError, except when the list is empty and @@ -157,11 +164,11 @@ class MonitoredFocusList(MonitoredList): instance with set_focus_changed_callback(). >>> ml = MonitoredFocusList([9, 10, 11]) - >>> ml._set_focus(2); ml._get_focus() + >>> ml.focus = 2; ml.focus 2 - >>> ml._set_focus(0); ml._get_focus() + >>> ml.focus = 0; ml.focus 0 - >>> ml._set_focus(-2) + >>> ml.focus = -2 Traceback (most recent call last): ... IndexError: focus index is out of range: -2 @@ -178,11 +185,23 @@ class MonitoredFocusList(MonitoredList): self._focus_changed(index) self._focus = index - focus = property(_get_focus, _set_focus, doc=""" - Get/set the focus index. This value is read as None when the list - is empty, and may only be set to a value between 0 and len(self)-1 - or an IndexError will be raised. - """) + def _get_focus(self) -> int | None: + warnings.warn( + f"method `{self.__class__.__name__}._get_focus` is deprecated, " + f"please use `{self.__class__.__name__}.focus` property", + DeprecationWarning, + stacklevel=2, + ) + return self.focus + + def _set_focus(self, index: int) -> None: + warnings.warn( + f"method `{self.__class__.__name__}._set_focus` is deprecated, " + f"please use `{self.__class__.__name__}.focus` property", + DeprecationWarning, + stacklevel=2, + ) + self.focus = index def _focus_changed(self, new_focus: int): pass @@ -306,7 +325,7 @@ class MonitoredFocusList(MonitoredList): else: focus = self._adjust_focus_on_contents_modified(slice(y, y+1 or None)) rval = super().__delitem__(y) - self._set_focus(focus) + self.focus = focus return rval def __setitem__(self, i: int | slice, y): @@ -343,7 +362,7 @@ class MonitoredFocusList(MonitoredList): else: focus = self._adjust_focus_on_contents_modified(slice(i, i+1 or None), [y]) rval = super().__setitem__(i, y) - self._set_focus(focus) + self.focus = focus return rval def __imul__(self, n: int): @@ -366,7 +385,7 @@ class MonitoredFocusList(MonitoredList): else: # all contents are being removed focus = self._adjust_focus_on_contents_modified(slice(0, len(self))) rval = super().__imul__(n) - self._set_focus(focus) + self.focus = focus return rval def append(self, item): @@ -381,7 +400,7 @@ class MonitoredFocusList(MonitoredList): focus = self._adjust_focus_on_contents_modified( slice(len(self), len(self)), [item]) rval = super().append(item) - self._set_focus(focus) + self.focus = focus return rval def extend(self, items): @@ -396,7 +415,7 @@ class MonitoredFocusList(MonitoredList): focus = self._adjust_focus_on_contents_modified( slice(len(self), len(self)), items) rval = super().extend(items) - self._set_focus(focus) + self.focus = focus return rval def insert(self, index: int, item): @@ -411,7 +430,7 @@ class MonitoredFocusList(MonitoredList): """ focus = self._adjust_focus_on_contents_modified(slice(index, index), [item]) rval = super().insert(index, item) - self._set_focus(focus) + self.focus = focus return rval def pop(self, index: int = -1): @@ -432,7 +451,7 @@ class MonitoredFocusList(MonitoredList): """ focus = self._adjust_focus_on_contents_modified(slice(index, index + 1 or None)) rval = super().pop(index) - self._set_focus(focus) + self.focus = focus return rval def remove(self, value): @@ -449,7 +468,7 @@ class MonitoredFocusList(MonitoredList): focus = self._adjust_focus_on_contents_modified(slice(index, index+1 or None)) rval = super().remove(value) - self._set_focus(focus) + self.focus = focus return rval def reverse(self): @@ -459,7 +478,7 @@ class MonitoredFocusList(MonitoredList): MonitoredFocusList([4, 3, 2, 1, 0], focus=3) """ rval = super().reverse() - self._set_focus(max(0, len(self) - self._focus - 1)) + self.focus = max(0, len(self) - self._focus - 1) return rval def sort(self, **kwargs): @@ -472,7 +491,7 @@ class MonitoredFocusList(MonitoredList): return value = self[self._focus] rval = super().sort(**kwargs) - self._set_focus(self.index(value)) + self.focus = self.index(value) return rval if hasattr(list, 'clear'): diff --git a/urwid/tests/test_container.py b/urwid/tests/test_container.py index f5a9182..3796cc7 100644 --- a/urwid/tests/test_container.py +++ b/urwid/tests/test_container.py @@ -134,6 +134,11 @@ class PileTest(unittest.TestCase): p.mouse_event((5,), 'button press', 1, 1, 1, False) p.mouse_event((5,), 'button press', 1, 1, 1, True) + def test_length(self): + pile = urwid.Pile(urwid.Text(c) for c in "ABC") + self.assertEqual(3, len(pile)) + self.assertEqual(3, len(pile.contents)) + class ColumnsTest(unittest.TestCase): def cwtest(self, desc, l, divide, size, exp, focus_column=0): @@ -202,8 +207,7 @@ class ColumnsTest(unittest.TestCase): c = urwid.Columns( l, divide ) rval = c.move_cursor_to_coords( size, col, row ) assert rval == exp, f"{desc} expected {exp!r}, got {rval!r}" - assert c.focus_col == f_col, "%s expected focus_col %s got %s"%( - desc, f_col, c.focus_col) + assert c.focus_position == f_col, f"{desc} expected focus_col {f_col} got {c.focus_position}" pc = c.get_pref_col( size ) assert pc == pref_col, f"{desc} expected pref_col {pref_col}, got {pc}" @@ -252,6 +256,10 @@ class ColumnsTest(unittest.TestCase): c.mouse_event((10,), 'foo', 1, 0, 0, True) c.get_pref_col((10,)) + def test_length(self): + columns = urwid.Columns(urwid.Text(c) for c in "ABC") + self.assertEqual(3, len(columns)) + self.assertEqual(3, len(columns.contents)) class OverlayTest(unittest.TestCase): @@ -274,6 +282,18 @@ class OverlayTest(unittest.TestCase): urwid.SolidFill('B'), 'right', 1, 'bottom', 1).get_cursor_coords((2,2)), (1,1)) + def test_length(self): + ovl = urwid.Overlay( + urwid.SolidFill('X'), + urwid.SolidFill('O'), + 'center', + ("relative", 20), + "middle", + ("relative", 20), + ) + self.assertEqual(2, len(ovl)) + self.assertEqual(2, len(ovl.contents)) + class GridFlowTest(unittest.TestCase): def test_cell_width(self): @@ -298,6 +318,11 @@ class GridFlowTest(unittest.TestCase): self.assertEqual(gf.keypress((20,), "enter"), None) call_back.assert_called_with(button) + def test_length(self): + grid = urwid.GridFlow((urwid.Text(c) for c in "ABC"), 1, 0, 0, 'left') + self.assertEqual(3, len(grid)) + self.assertEqual(3, len(grid.contents)) + class WidgetSquishTest(unittest.TestCase): def wstest(self, w): diff --git a/urwid/widget.py b/urwid/widget.py index d5abf40..71ade15 100644 --- a/urwid/widget.py +++ b/urwid/widget.py @@ -25,6 +25,7 @@ from __future__ import annotations import functools import typing import warnings +from collections.abc import Callable from operator import attrgetter from urwid import signals, text_layout @@ -1338,17 +1339,6 @@ class Edit(Text): else: return pref_col - def update_text(self) -> typing.NoReturn: - """ - No longer supported. - - >>> Edit().update_text() - Traceback (most recent call last): - EditError: update_text() has been removed. Use set_caption() or set_edit_text() instead. - """ - raise EditError("update_text() has been removed. Use " - "set_caption() or set_edit_text() instead.") - def set_caption(self, caption): """ Set the caption markup for this widget. @@ -1906,6 +1896,31 @@ class WidgetWrap(delegate_to_widget_mixin('_wrapped_widget'), Widget): """ self._wrapped_widget = w + @property + def _w(self) -> Widget: + return self._wrapped_widget + + @_w.setter + def _w(self, new_widget: Widget) -> None: + """ + Change the wrapped widget. This is meant to be called + only by subclasses. + + >>> size = (10,) + >>> ww = WidgetWrap(Edit("hello? ","hi")) + >>> ww.render(size).text # ... = b in Python 3 + [...'hello? hi '] + >>> ww.selectable() + True + >>> ww._w = Text("goodbye") # calls _set_w() + >>> ww.render(size).text + [...'goodbye '] + >>> ww.selectable() + False + """ + self._wrapped_widget = new_widget + self._invalidate() + def _set_w(self, w): """ Change the wrapped widget. This is meant to be called @@ -1923,17 +1938,13 @@ class WidgetWrap(delegate_to_widget_mixin('_wrapped_widget'), Widget): >>> ww.selectable() False """ + warnings.warn( + "_set_w is deprecated. Please use 'WidgetWrap._w' property directly", + DeprecationWarning, + stacklevel=2, + ) self._wrapped_widget = w self._invalidate() - _w = property(lambda self:self._wrapped_widget, _set_w) - - def _raise_old_name_error(self, val=None): - raise WidgetWrapError("The WidgetWrap.w member variable has " - "been renamed to WidgetWrap._w (not intended for use " - "outside the class and its subclasses). " - "Please update your code to use self._w " - "instead of self.w.") - w = property(_raise_old_name_error, _raise_old_name_error) def _test(): |