Merge pull request #2102 from Textualize/verb-methods-return-self

Return 'self' in some widget verb methods.
This commit is contained in:
Rodrigo Girão Serrão
2023-03-23 15:00:18 +00:00
committed by GitHub
11 changed files with 194 additions and 64 deletions

View File

@@ -33,6 +33,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Breaking change: changed default behaviour of `Vertical` (see `VerticalScroll`) https://github.com/Textualize/textual/issues/1957 - Breaking change: changed default behaviour of `Vertical` (see `VerticalScroll`) https://github.com/Textualize/textual/issues/1957
- The default `overflow` style for `Horizontal` was changed to `hidden hidden` https://github.com/Textualize/textual/issues/1957 - The default `overflow` style for `Horizontal` was changed to `hidden hidden` https://github.com/Textualize/textual/issues/1957
- `DirectoryTree` also accepts `pathlib.Path` objects as the path to list https://github.com/Textualize/textual/issues/1438 - `DirectoryTree` also accepts `pathlib.Path` objects as the path to list https://github.com/Textualize/textual/issues/1438
- Some widget methods now return `self` instead of `None` https://github.com/Textualize/textual/pull/2102:
- `Widget`: `refresh`, `focus`, `reset_focus`
- `Button.press`
- `DataTable`: `clear`, `refresh_coordinate`, `refresh_row`, `refresh_column`, `sort`
- `Placehoder.cycle_variant`
- `Switch.toggle`
- `Tabs.clear`
- `TextLog`: `write`, `clear`
- `TreeNode`: `expand`, `expand_all`, `collapse`, `collapse_all`, `toggle`, `toggle_all`
- `Tree`: `clear`, `reset`
### Removed ### Removed

View File

@@ -42,7 +42,7 @@ if TYPE_CHECKING:
from .css.query import DOMQuery, QueryType from .css.query import DOMQuery, QueryType
from .screen import Screen from .screen import Screen
from .widget import Widget from .widget import Widget
from typing_extensions import TypeAlias from typing_extensions import Self, TypeAlias
from typing_extensions import Literal from typing_extensions import Literal
@@ -950,5 +950,5 @@ class DOMNode(MessagePump):
has_pseudo_classes = self.pseudo_classes.issuperset(class_names) has_pseudo_classes = self.pseudo_classes.issuperset(class_names)
return has_pseudo_classes return has_pseudo_classes
def refresh(self, *, repaint: bool = True, layout: bool = False) -> None: def refresh(self, *, repaint: bool = True, layout: bool = False) -> Self:
pass return self

View File

@@ -2560,7 +2560,7 @@ class Widget(DOMNode):
*regions: Region, *regions: Region,
repaint: bool = True, repaint: bool = True,
layout: bool = False, layout: bool = False,
) -> None: ) -> Self:
"""Initiate a refresh of the widget. """Initiate a refresh of the widget.
This method sets an internal flag to perform a refresh, which will be done on the This method sets an internal flag to perform a refresh, which will be done on the
@@ -2576,8 +2576,11 @@ class Widget(DOMNode):
Args: Args:
*regions: Additional screen regions to mark as dirty. *regions: Additional screen regions to mark as dirty.
repaint: Repaint the widget (will call render() again). Defaults to True. repaint: Repaint the widget (will call render() again).
layout: Also layout widgets in the view. Defaults to False. layout: Also layout widgets in the view.
Returns:
The `Widget` instance.
""" """
if layout and not self._layout_required: if layout and not self._layout_required:
self._layout_required = True self._layout_required = True
@@ -2595,6 +2598,7 @@ class Widget(DOMNode):
self._repaint_required = True self._repaint_required = True
self.check_idle() self.check_idle()
return self
def remove(self) -> AwaitRemove: def remove(self) -> AwaitRemove:
"""Remove the Widget from the DOM (effectively deleting it). """Remove the Widget from the DOM (effectively deleting it).
@@ -2676,12 +2680,14 @@ class Widget(DOMNode):
self._layout_required = False self._layout_required = False
screen.post_message(messages.Layout()) screen.post_message(messages.Layout())
def focus(self, scroll_visible: bool = True) -> None: def focus(self, scroll_visible: bool = True) -> Self:
"""Give focus to this widget. """Give focus to this widget.
Args: Args:
scroll_visible: Scroll parent to make this widget scroll_visible: Scroll parent to make this widget visible.
visible. Defaults to True.
Returns:
The `Widget` instance.
""" """
def set_focus(widget: Widget): def set_focus(widget: Widget):
@@ -2692,13 +2698,19 @@ class Widget(DOMNode):
pass pass
self.app.call_later(set_focus, self) self.app.call_later(set_focus, self)
return self
def reset_focus(self) -> None: def reset_focus(self) -> Self:
"""Reset the focus (move it to the next available widget).""" """Reset the focus (move it to the next available widget).
Returns:
The `Widget` instance.
"""
try: try:
self.screen._reset_focus(self) self.screen._reset_focus(self)
except NoScreen: except NoScreen:
pass pass
return self
def capture_mouse(self, capture: bool = True) -> None: def capture_mouse(self, capture: bool = True) -> None:
"""Capture (or release) the mouse. """Capture (or release) the mouse.

View File

@@ -1,11 +1,10 @@
from __future__ import annotations from __future__ import annotations
from functools import partial from functools import partial
from typing import cast
import rich.repr import rich.repr
from rich.text import Text, TextType from rich.text import Text, TextType
from typing_extensions import Literal from typing_extensions import Literal, Self
from .. import events from .. import events
from ..css._error_tools import friendly_list from ..css._error_tools import friendly_list
@@ -233,14 +232,18 @@ class Button(Static, can_focus=True):
event.stop() event.stop()
self.press() self.press()
def press(self) -> None: def press(self) -> Self:
"""Respond to a button press.""" """Respond to a button press.
Returns:
The button instance."""
if self.disabled or not self.display: if self.disabled or not self.display:
return return self
# Manage the "active" effect: # Manage the "active" effect:
self._start_active_affect() self._start_active_affect()
# ...and let other components know that we've just been clicked: # ...and let other components know that we've just been clicked:
self.post_message(Button.Pressed(self)) self.post_message(Button.Pressed(self))
return self
def _start_active_affect(self) -> None: def _start_active_affect(self) -> None:
"""Start a small animation to show the button was clicked.""" """Start a small animation to show the button was clicked."""

View File

@@ -13,7 +13,7 @@ from rich.protocol import is_renderable
from rich.segment import Segment from rich.segment import Segment
from rich.style import Style from rich.style import Style
from rich.text import Text, TextType from rich.text import Text, TextType
from typing_extensions import Literal, TypeAlias from typing_extensions import Literal, Self, TypeAlias
from .. import events from .. import events
from .._cache import LRUCache from .._cache import LRUCache
@@ -1156,11 +1156,14 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
full_column_region = Region(x, 0, width, height) full_column_region = Region(x, 0, width, height)
return full_column_region return full_column_region
def clear(self, columns: bool = False) -> None: def clear(self, columns: bool = False) -> Self:
"""Clear the table. """Clear the table.
Args: Args:
columns: Also clear the columns. Defaults to False. columns: Also clear the columns.
Returns:
The `DataTable` instance.
""" """
self._clear_caches() self._clear_caches()
self._y_offsets.clear() self._y_offsets.clear()
@@ -1176,6 +1179,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
self._label_column = Column(self._label_column_key, Text(), auto_width=True) self._label_column = Column(self._label_column_key, Text(), auto_width=True)
self._labelled_row_exists = False self._labelled_row_exists = False
self.refresh() self.refresh()
return self
def add_column( def add_column(
self, label: TextType, *, width: int | None = None, key: str | None = None self, label: TextType, *, width: int | None = None, key: str | None = None
@@ -1333,49 +1337,66 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
self._updated_cells.clear() self._updated_cells.clear()
self._update_column_widths(updated_columns) self._update_column_widths(updated_columns)
def refresh_coordinate(self, coordinate: Coordinate) -> None: def refresh_coordinate(self, coordinate: Coordinate) -> Self:
"""Refresh the cell at a coordinate. """Refresh the cell at a coordinate.
Args: Args:
coordinate: The coordinate to refresh. coordinate: The coordinate to refresh.
Returns:
The `DataTable` instance.
""" """
if not self.is_valid_coordinate(coordinate): if not self.is_valid_coordinate(coordinate):
return return self
region = self._get_cell_region(coordinate) region = self._get_cell_region(coordinate)
self._refresh_region(region) self._refresh_region(region)
return self
def refresh_row(self, row_index: int) -> None: def refresh_row(self, row_index: int) -> Self:
"""Refresh the row at the given index. """Refresh the row at the given index.
Args: Args:
row_index: The index of the row to refresh. row_index: The index of the row to refresh.
Returns:
The `DataTable` instance.
""" """
if not self.is_valid_row_index(row_index): if not self.is_valid_row_index(row_index):
return return self
region = self._get_row_region(row_index) region = self._get_row_region(row_index)
self._refresh_region(region) self._refresh_region(region)
return self
def refresh_column(self, column_index: int) -> None: def refresh_column(self, column_index: int) -> Self:
"""Refresh the column at the given index. """Refresh the column at the given index.
Args: Args:
column_index: The index of the column to refresh. column_index: The index of the column to refresh.
Returns:
The `DataTable` instance.
""" """
if not self.is_valid_column_index(column_index): if not self.is_valid_column_index(column_index):
return return self
region = self._get_column_region(column_index) region = self._get_column_region(column_index)
self._refresh_region(region) self._refresh_region(region)
return self
def _refresh_region(self, region: Region) -> None: def _refresh_region(self, region: Region) -> Self:
"""Refresh a region of the DataTable, if it's visible within """Refresh a region of the DataTable, if it's visible within the window.
the window. This method will translate the region to account
for scrolling.""" This method will translate the region to account for scrolling.
Returns:
The `DataTable` instance.
"""
if not self.window_region.overlaps(region): if not self.window_region.overlaps(region):
return return self
region = region.translate(-self.scroll_offset) region = region.translate(-self.scroll_offset)
self.refresh(region) self.refresh(region)
return self
def is_valid_row_index(self, row_index: int) -> bool: def is_valid_row_index(self, row_index: int) -> bool:
"""Return a boolean indicating whether the row_index is within table bounds. """Return a boolean indicating whether the row_index is within table bounds.
@@ -1839,12 +1860,15 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
self, self,
*columns: ColumnKey | str, *columns: ColumnKey | str,
reverse: bool = False, reverse: bool = False,
) -> None: ) -> Self:
"""Sort the rows in the DataTable by one or more column keys. """Sort the rows in the `DataTable` by one or more column keys.
Args: Args:
columns: One or more columns to sort by the values in. columns: One or more columns to sort by the values in.
reverse: If True, the sort order will be reversed. reverse: If True, the sort order will be reversed.
Returns:
The `DataTable` instance.
""" """
def sort_by_column_keys( def sort_by_column_keys(
@@ -1862,6 +1886,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
) )
self._update_count += 1 self._update_count += 1
self.refresh() self.refresh()
return self
def _scroll_cursor_into_view(self, animate: bool = False) -> None: def _scroll_cursor_into_view(self, animate: bool = False) -> None:
"""When the cursor is at a boundary of the DataTable and moves out """When the cursor is at a boundary of the DataTable and moves out

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from itertools import cycle from itertools import cycle
from rich.console import RenderableType from rich.console import RenderableType
from typing_extensions import Literal from typing_extensions import Literal, Self
from .. import events from .. import events
from ..css._error_tools import friendly_list from ..css._error_tools import friendly_list
@@ -132,9 +132,14 @@ class Placeholder(Widget):
""" """
return self._renderables[self.variant] return self._renderables[self.variant]
def cycle_variant(self) -> None: def cycle_variant(self) -> Self:
"""Get the next variant in the cycle.""" """Get the next variant in the cycle.
Returns:
The `Placeholder` instance.
"""
self.variant = next(self._variants_cycle) self.variant = next(self._variants_cycle)
return self
def watch_variant( def watch_variant(
self, old_variant: PlaceholderVariant, variant: PlaceholderVariant self, old_variant: PlaceholderVariant, variant: PlaceholderVariant

View File

@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import ClassVar from typing import TYPE_CHECKING, ClassVar
from rich.console import RenderableType from rich.console import RenderableType
@@ -11,6 +11,9 @@ from ..reactive import reactive
from ..scrollbar import ScrollBarRender from ..scrollbar import ScrollBarRender
from ..widget import Widget from ..widget import Widget
if TYPE_CHECKING:
from typing_extensions import Self
class Switch(Widget, can_focus=True): class Switch(Widget, can_focus=True):
"""A switch widget that represents a boolean value. """A switch widget that represents a boolean value.
@@ -158,10 +161,14 @@ class Switch(Widget, can_focus=True):
"""Toggle the state of the switch.""" """Toggle the state of the switch."""
self.toggle() self.toggle()
def toggle(self) -> None: def toggle(self) -> Self:
"""Toggle the switch value. """Toggle the switch value.
As a result of the value changing, a `Switch.Changed` message will As a result of the value changing, a `Switch.Changed` message will
be posted. be posted.
Returns:
The `Switch` instance.
""" """
self.value = not self.value self.value = not self.value
return self

View File

@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import ClassVar from typing import TYPE_CHECKING, ClassVar
import rich.repr import rich.repr
from rich.style import Style from rich.style import Style
@@ -18,6 +18,9 @@ from ..renderables.underline_bar import UnderlineBar
from ..widget import Widget from ..widget import Widget
from ..widgets import Static from ..widgets import Static
if TYPE_CHECKING:
from typing_extensions import Self
class Underline(Widget): class Underline(Widget):
"""The animated underline beneath tabs.""" """The animated underline beneath tabs."""
@@ -316,13 +319,18 @@ class Tabs(Widget, can_focus=True):
self.call_after_refresh(refresh_active) self.call_after_refresh(refresh_active)
def clear(self) -> None: def clear(self) -> Self:
"""Clear all the tabs.""" """Clear all the tabs.
Returns:
The `Tabs` instance.
"""
underline = self.query_one(Underline) underline = self.query_one(Underline)
underline.highlight_start = 0 underline.highlight_start = 0
underline.highlight_end = 0 underline.highlight_end = 0
self.query("#tabs-list > Tab").remove() self.query("#tabs-list > Tab").remove()
self.post_message(self.Cleared(self)) self.post_message(self.Cleared(self))
return self
def remove_tab(self, tab_or_id: Tab | str | None) -> None: def remove_tab(self, tab_or_id: Tab | str | None) -> None:
"""Remove a tab. """Remove a tab.

View File

@@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Optional, cast from typing import TYPE_CHECKING, Optional, cast
from rich.console import RenderableType from rich.console import RenderableType
from rich.highlighter import ReprHighlighter from rich.highlighter import ReprHighlighter
@@ -18,6 +18,9 @@ from ..reactive import var
from ..scroll_view import ScrollView from ..scroll_view import ScrollView
from ..strip import Strip from ..strip import Strip
if TYPE_CHECKING:
from typing_extensions import Self
class TextLog(ScrollView, can_focus=True): class TextLog(ScrollView, can_focus=True):
"""A widget for logging text.""" """A widget for logging text."""
@@ -89,7 +92,7 @@ class TextLog(ScrollView, can_focus=True):
width: int | None = None, width: int | None = None,
expand: bool = False, expand: bool = False,
shrink: bool = True, shrink: bool = True,
) -> None: ) -> Self:
"""Write text or a rich renderable. """Write text or a rich renderable.
Args: Args:
@@ -97,6 +100,9 @@ class TextLog(ScrollView, can_focus=True):
width: Width to render or ``None`` to use optimal width. width: Width to render or ``None`` to use optimal width.
expand: Enable expand to widget width, or ``False`` to use `width`. expand: Enable expand to widget width, or ``False`` to use `width`.
shrink: Enable shrinking of content to fit width. shrink: Enable shrinking of content to fit width.
Returns:
The `TextLog` instance.
""" """
renderable: RenderableType renderable: RenderableType
@@ -136,7 +142,7 @@ class TextLog(ScrollView, can_focus=True):
) )
lines = list(Segment.split_lines(segments)) lines = list(Segment.split_lines(segments))
if not lines: if not lines:
return return self
self.max_width = max( self.max_width = max(
self.max_width, self.max_width,
@@ -154,14 +160,21 @@ class TextLog(ScrollView, can_focus=True):
self.virtual_size = Size(self.max_width, len(self.lines)) self.virtual_size = Size(self.max_width, len(self.lines))
self.scroll_end(animate=False) self.scroll_end(animate=False)
def clear(self) -> None: return self
"""Clear the text log."""
def clear(self) -> Self:
"""Clear the text log.
Returns:
The `TextLog` instance.
"""
self.lines.clear() self.lines.clear()
self._line_cache.clear() self._line_cache.clear()
self._start_line = 0 self._start_line = 0
self.max_width = 0 self.max_width = 0
self.virtual_size = Size(self.max_width, len(self.lines)) self.virtual_size = Size(self.max_width, len(self.lines))
self.refresh() self.refresh()
return self
def render_line(self, y: int) -> Strip: def render_line(self, y: int) -> Strip:
scroll_x, scroll_y = self.scroll_offset scroll_x, scroll_y = self.scroll_offset

View File

@@ -5,7 +5,7 @@ In particular it provides `Checkbox`, `RadioButton` and `RadioSet`.
from __future__ import annotations from __future__ import annotations
from typing import ClassVar from typing import TYPE_CHECKING, ClassVar
from rich.style import Style from rich.style import Style
from rich.text import Text, TextType from rich.text import Text, TextType
@@ -17,6 +17,9 @@ from ..message import Message
from ..reactive import reactive from ..reactive import reactive
from ._static import Static from ._static import Static
if TYPE_CHECKING:
from typing_extensions import Self
class ToggleButton(Static, can_focus=True): class ToggleButton(Static, can_focus=True):
"""Base toggle button widget. """Base toggle button widget.
@@ -201,9 +204,14 @@ class ToggleButton(Static, can_focus=True):
def get_content_height(self, container: Size, viewport: Size, width: int) -> int: def get_content_height(self, container: Size, viewport: Size, width: int) -> int:
return 1 return 1
def toggle(self) -> None: def toggle(self) -> Self:
"""Toggle the value of the widget.""" """Toggle the value of the widget.
Returns:
The `ToggleButton` instance.
"""
self.value = not self.value self.value = not self.value
return self
def action_toggle(self) -> None: def action_toggle(self) -> None:
"""Toggle the value of the widget when called as an action. """Toggle the value of the widget when called as an action.

View File

@@ -22,7 +22,7 @@ from ..scroll_view import ScrollView
from ..strip import Strip from ..strip import Strip
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import TypeAlias from typing_extensions import Self, TypeAlias
NodeID = NewType("NodeID", int) NodeID = NewType("NodeID", int)
"""The type of an ID applied to a [TreeNode][textual.widgets._tree.TreeNode].""" """The type of an ID applied to a [TreeNode][textual.widgets._tree.TreeNode]."""
@@ -201,15 +201,25 @@ class TreeNode(Generic[TreeDataType]):
for child in self.children: for child in self.children:
child._expand(expand_all) child._expand(expand_all)
def expand(self) -> None: def expand(self) -> Self:
"""Expand the node (show its children).""" """Expand the node (show its children).
Returns:
The `TreeNode` instance.
"""
self._expand(False) self._expand(False)
self._tree._invalidate() self._tree._invalidate()
return self
def expand_all(self) -> None: def expand_all(self) -> Self:
"""Expand the node (show its children) and all those below it.""" """Expand the node (show its children) and all those below it.
Returns:
The `TreeNode` instance.
"""
self._expand(True) self._expand(True)
self._tree._invalidate() self._tree._invalidate()
return self
def _collapse(self, collapse_all: bool) -> None: def _collapse(self, collapse_all: bool) -> None:
"""Mark the node as collapsed (its children are hidden). """Mark the node as collapsed (its children are hidden).
@@ -223,29 +233,49 @@ class TreeNode(Generic[TreeDataType]):
for child in self.children: for child in self.children:
child._collapse(collapse_all) child._collapse(collapse_all)
def collapse(self) -> None: def collapse(self) -> Self:
"""Collapse the node (hide its children).""" """Collapse the node (hide its children).
Returns:
The `TreeNode` instance.
"""
self._collapse(False) self._collapse(False)
self._tree._invalidate() self._tree._invalidate()
return self
def collapse_all(self) -> None: def collapse_all(self) -> Self:
"""Collapse the node (hide its children) and all those below it.""" """Collapse the node (hide its children) and all those below it.
Returns:
The `TreeNode` instance.
"""
self._collapse(True) self._collapse(True)
self._tree._invalidate() self._tree._invalidate()
return self
def toggle(self) -> None: def toggle(self) -> Self:
"""Toggle the node's expanded state.""" """Toggle the node's expanded state.
Returns:
The `TreeNode` instance.
"""
if self._expanded: if self._expanded:
self.collapse() self.collapse()
else: else:
self.expand() self.expand()
return self
def toggle_all(self) -> None: def toggle_all(self) -> Self:
"""Toggle the node's expanded state and make all those below it match.""" """Toggle the node's expanded state and make all those below it match.
Returns:
The `TreeNode` instance.
"""
if self._expanded: if self._expanded:
self.collapse_all() self.collapse_all()
else: else:
self.expand_all() self.expand_all()
return self
@property @property
def label(self) -> TextType: def label(self) -> TextType:
@@ -597,8 +627,12 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
label = self.render_label(node, NULL_STYLE, NULL_STYLE) label = self.render_label(node, NULL_STYLE, NULL_STYLE)
return label.cell_len return label.cell_len
def clear(self) -> None: def clear(self) -> Self:
"""Clear all nodes under root.""" """Clear all nodes under root.
Returns:
The `Tree` instance.
"""
self._line_cache.clear() self._line_cache.clear()
self._tree_lines_cached = None self._tree_lines_cached = None
self._current_id = 0 self._current_id = 0
@@ -614,17 +648,22 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
) )
self._updates += 1 self._updates += 1
self.refresh() self.refresh()
return self
def reset(self, label: TextType, data: TreeDataType | None = None) -> None: def reset(self, label: TextType, data: TreeDataType | None = None) -> Self:
"""Clear the tree and reset the root node. """Clear the tree and reset the root node.
Args: Args:
label: The label for the root node. label: The label for the root node.
data: Optional data for the root node. data: Optional data for the root node.
Returns:
The `Tree` instance.
""" """
self.clear() self.clear()
self.root.label = label self.root.label = label
self.root.data = data self.root.data = data
return self
def select_node(self, node: TreeNode[TreeDataType] | None) -> None: def select_node(self, node: TreeNode[TreeDataType] | None) -> None:
"""Move the cursor to the given node, or reset cursor. """Move the cursor to the given node, or reset cursor.