Merge branch 'main' into promote-disabled

This commit is contained in:
Dave Pearson
2023-02-15 08:47:20 +00:00
20 changed files with 2211 additions and 474 deletions

View File

@@ -7,3 +7,4 @@ exclude_lines =
if TYPE_CHECKING: if TYPE_CHECKING:
if __name__ == "__main__": if __name__ == "__main__":
@overload @overload
__rich_repr__

View File

@@ -19,6 +19,23 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added Shift+scroll wheel and ctrl+scroll wheel to scroll horizontally - Added Shift+scroll wheel and ctrl+scroll wheel to scroll horizontally
- Added `Tree.action_toggle_node` to toggle a node without selecting, and bound it to <kbd>Space</kbd> https://github.com/Textualize/textual/issues/1433 - Added `Tree.action_toggle_node` to toggle a node without selecting, and bound it to <kbd>Space</kbd> https://github.com/Textualize/textual/issues/1433
- Added `Tree.reset` to fully reset a `Tree` https://github.com/Textualize/textual/issues/1437 - Added `Tree.reset` to fully reset a `Tree` https://github.com/Textualize/textual/issues/1437
- Added `DataTable.sort` to sort rows https://github.com/Textualize/textual/pull/1638
- Added `DataTable.get_cell` to retrieve a cell by column/row keys https://github.com/Textualize/textual/pull/1638
- Added `DataTable.get_cell_at` to retrieve a cell by coordinate https://github.com/Textualize/textual/pull/1638
- Added `DataTable.update_cell` to update a cell by column/row keys https://github.com/Textualize/textual/pull/1638
- Added `DataTable.update_cell_at` to update a cell at a coordinate https://github.com/Textualize/textual/pull/1638
- Added `DataTable.ordered_rows` property to retrieve `Row`s as they're currently ordered https://github.com/Textualize/textual/pull/1638
- Added `DataTable.ordered_columns` property to retrieve `Column`s as they're currently ordered https://github.com/Textualize/textual/pull/1638
- Added `DataTable.coordinate_to_cell_key` to find the key for the cell at a coordinate https://github.com/Textualize/textual/pull/1638
- Added `DataTable.is_valid_coordinate` https://github.com/Textualize/textual/pull/1638
- Added `DataTable.is_valid_row_index` https://github.com/Textualize/textual/pull/1638
- Added `DataTable.is_valid_column_index` https://github.com/Textualize/textual/pull/1638
- Added attributes to events emitted from `DataTable` indicating row/column/cell keys https://github.com/Textualize/textual/pull/1638
- Added `DataTable.get_row` to retrieve the values from a row by key https://github.com/Textualize/textual/pull/1786
- Added `DataTable.get_row_at` to retrieve the values from a row by index https://github.com/Textualize/textual/pull/1786
- Added `DataTable.get_column` to retrieve the values from a column by key https://github.com/Textualize/textual/pull/1786
- Added `DataTable.get_column_at` to retrieve the values from a column by index https://github.com/Textualize/textual/pull/1786
- Added `DataTable.HeaderSelected` which is posted when header label clicked https://github.com/Textualize/textual/pull/1788
- Added `DOMNode.watch` and `DOMNode.is_attached` methods https://github.com/Textualize/textual/pull/1750 - Added `DOMNode.watch` and `DOMNode.is_attached` methods https://github.com/Textualize/textual/pull/1750
- Added `DOMNode.css_tree` which is a renderable that shows the DOM and CSS https://github.com/Textualize/textual/pull/1778 - Added `DOMNode.css_tree` which is a renderable that shows the DOM and CSS https://github.com/Textualize/textual/pull/1778
- Added `DOMNode.children_view` which is a view on to a nodes children list, use for querying https://github.com/Textualize/textual/pull/1778 - Added `DOMNode.children_view` which is a view on to a nodes children list, use for querying https://github.com/Textualize/textual/pull/1778
@@ -27,6 +44,21 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Breaking change: `TreeNode` can no longer be imported from `textual.widgets`; it is now available via `from textual.widgets.tree import TreeNode`. https://github.com/Textualize/textual/pull/1637 - Breaking change: `TreeNode` can no longer be imported from `textual.widgets`; it is now available via `from textual.widgets.tree import TreeNode`. https://github.com/Textualize/textual/pull/1637
- `Tree` now shows a (subdued) cursor for a highlighted node when focus has moved elsewhere https://github.com/Textualize/textual/issues/1471 - `Tree` now shows a (subdued) cursor for a highlighted node when focus has moved elsewhere https://github.com/Textualize/textual/issues/1471
- `DataTable.add_row` now accepts `key` argument to uniquely identify the row https://github.com/Textualize/textual/pull/1638
- `DataTable.add_column` now accepts `key` argument to uniquely identify the column https://github.com/Textualize/textual/pull/1638
- `DataTable.add_row` and `DataTable.add_column` now return lists of keys identifying the added rows/columns https://github.com/Textualize/textual/pull/1638
- Breaking change: `DataTable.get_cell_value` renamed to `DataTable.get_value_at` https://github.com/Textualize/textual/pull/1638
- `DataTable.row_count` is now a property https://github.com/Textualize/textual/pull/1638
- Breaking change: `DataTable.cursor_cell` renamed to `DataTable.cursor_coordinate` https://github.com/Textualize/textual/pull/1638
- The method `validate_cursor_cell` was renamed to `validate_cursor_coordinate`.
- The method `watch_cursor_cell` was renamed to `watch_cursor_coordinate`.
- Breaking change: `DataTable.hover_cell` renamed to `DataTable.hover_coordinate` https://github.com/Textualize/textual/pull/1638
- The method `validate_hover_cell` was renamed to `validate_hover_coordinate`.
- Breaking change: `DataTable.data` structure changed, and will be made private in upcoming release https://github.com/Textualize/textual/pull/1638
- Breaking change: `DataTable.refresh_cell` was renamed to `DataTable.refresh_coordinate` https://github.com/Textualize/textual/pull/1638
- Breaking change: `DataTable.get_row_height` now takes a `RowKey` argument instead of a row index https://github.com/Textualize/textual/pull/1638
- Breaking change: `DataTable.data` renamed to `DataTable._data` (it's now private) https://github.com/Textualize/textual/pull/1786
- The `_filter` module was made public (now called `filter`) https://github.com/Textualize/textual/pull/1638
- Breaking change: renamed `Checkbox` to `Switch` https://github.com/Textualize/textual/issues/1746 - Breaking change: renamed `Checkbox` to `Switch` https://github.com/Textualize/textual/issues/1746
- `App.install_screen` name is no longer optional https://github.com/Textualize/textual/pull/1778 - `App.install_screen` name is no longer optional https://github.com/Textualize/textual/pull/1778
- `App.query` now only includes the current screen https://github.com/Textualize/textual/pull/1778 - `App.query` now only includes the current screen https://github.com/Textualize/textual/pull/1778
@@ -47,6 +79,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Fixed issue with renderable width calculation https://github.com/Textualize/textual/issues/1685 - Fixed issue with renderable width calculation https://github.com/Textualize/textual/issues/1685
- Fixed issue with app not processing Paste event https://github.com/Textualize/textual/issues/1666 - Fixed issue with app not processing Paste event https://github.com/Textualize/textual/issues/1666
- Fixed glitch with view position with auto width inputs https://github.com/Textualize/textual/issues/1693 - Fixed glitch with view position with auto width inputs https://github.com/Textualize/textual/issues/1693
- Fixed `DataTable` "selected" events containing wrong coordinates when mouse was used https://github.com/Textualize/textual/issues/1723
### Removed ### Removed

View File

@@ -1,18 +1,18 @@
import csv
import io
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.widgets import DataTable from textual.widgets import DataTable
CSV = """lane,swimmer,country,time ROWS = [
4,Joseph Schooling,Singapore,50.39 ("lane", "swimmer", "country", "time"),
2,Michael Phelps,United States,51.14 (4, "Joseph Schooling", "Singapore", 50.39),
5,Chad le Clos,South Africa,51.14 (2, "Michael Phelps", "United States", 51.14),
6,László Cseh,Hungary,51.14 (5, "Chad le Clos", "South Africa", 51.14),
3,Li Zhuhao,China,51.26 (6, "László Cseh", "Hungary", 51.14),
8,Mehdy Metella,France,51.58 (3, "Li Zhuhao", "China", 51.26),
7,Tom Shields,United States,51.73 (8, "Mehdy Metella", "France", 51.58),
1,Aleksandr Sadovnikov,Russia,51.84""" (7, "Tom Shields", "United States", 51.73),
(1, "Aleksandr Sadovnikov", "Russia", 51.84),
(10, "Darren Burns", "Scotland", 51.84),
]
class TableApp(App): class TableApp(App):
@@ -21,11 +21,11 @@ class TableApp(App):
def on_mount(self) -> None: def on_mount(self) -> None:
table = self.query_one(DataTable) table = self.query_one(DataTable)
rows = csv.reader(io.StringIO(CSV)) rows = iter(ROWS)
table.add_columns(*next(rows)) table.add_columns(*next(rows))
table.add_rows(rows) table.add_rows(rows)
app = TableApp()
if __name__ == "__main__": if __name__ == "__main__":
app = TableApp()
app.run() app.run()

View File

@@ -22,17 +22,17 @@ The example below populates a table with CSV data.
## Reactive Attributes ## Reactive Attributes
| Name | Type | Default | Description | | Name | Type | Default | Description |
|-----------------|---------------------------------------------|--------------------|---------------------------------------------------------| |---------------------|---------------------------------------------|--------------------|-------------------------------------------------------|
| `show_header` | `bool` | `True` | Show the table header | | `show_header` | `bool` | `True` | Show the table header |
| `fixed_rows` | `int` | `0` | Number of fixed rows (rows which do not scroll) | | `fixed_rows` | `int` | `0` | Number of fixed rows (rows which do not scroll) |
| `fixed_columns` | `int` | `0` | Number of fixed columns (columns which do not scroll) | | `fixed_columns` | `int` | `0` | Number of fixed columns (columns which do not scroll) |
| `zebra_stripes` | `bool` | `False` | Display alternating colors on rows | | `zebra_stripes` | `bool` | `False` | Display alternating colors on rows |
| `header_height` | `int` | `1` | Height of header row | | `header_height` | `int` | `1` | Height of header row |
| `show_cursor` | `bool` | `True` | Show the cursor | | `show_cursor` | `bool` | `True` | Show the cursor |
| `cursor_type` | `str` | `"cell"` | One of `"cell"`, `"row"`, `"column"`, or `"none"` | | `cursor_type` | `str` | `"cell"` | One of `"cell"`, `"row"`, `"column"`, or `"none"` |
| `cursor_cell` | [Coordinate][textual.coordinate.Coordinate] | `Coordinate(0, 0)` | The coordinates of the cell the cursor is currently on | | `cursor_coordinate` | [Coordinate][textual.coordinate.Coordinate] | `Coordinate(0, 0)` | The current coordinate of the cursor |
| `hover_cell` | [Coordinate][textual.coordinate.Coordinate] | `Coordinate(0, 0)` | The coordinates of the cell the _mouse_ cursor is above | | `hover_coordinate` | [Coordinate][textual.coordinate.Coordinate] | `Coordinate(0, 0)` | The coordinate the _mouse_ cursor is above |
## Messages ## Messages
@@ -48,6 +48,8 @@ The example below populates a table with CSV data.
### ::: textual.widgets.DataTable.ColumnSelected ### ::: textual.widgets.DataTable.ColumnSelected
### ::: textual.widgets.DataTable.HeaderSelected
## Bindings ## Bindings
The data table widget defines directly the following bindings: The data table widget defines directly the following bindings:

View File

@@ -8,7 +8,7 @@ from rich.segment import Segment
from rich.style import Style from rich.style import Style
from ._border import get_box, render_row from ._border import get_box, render_row
from ._filter import LineFilter from .filter import LineFilter
from ._opacity import _apply_opacity from ._opacity import _apply_opacity
from ._segment_tools import line_pad, line_trim from ._segment_tools import line_pad, line_trim
from .color import Color from .color import Color

View File

@@ -0,0 +1,69 @@
from __future__ import annotations
from typing import Generic, TypeVar
Key = TypeVar("Key")
Value = TypeVar("Value")
class TwoWayDict(Generic[Key, Value]):
"""
A two-way mapping offering O(1) access in both directions.
Wraps two dictionaries and uses them to provide efficient access to
both values (given keys) and keys (given values).
"""
def __init__(self, initial: dict[Key, Value]) -> None:
self._forward: dict[Key, Value] = initial
self._reverse: dict[Value, Key] = {value: key for key, value in initial.items()}
def __setitem__(self, key: Key, value: Value) -> None:
# TODO: Duplicate values need to be managed to ensure consistency,
# decide on best approach.
self._forward.__setitem__(key, value)
self._reverse.__setitem__(value, key)
def __delitem__(self, key: Key) -> None:
value = self._forward[key]
self._forward.__delitem__(key)
self._reverse.__delitem__(value)
def get(self, key: Key) -> Value:
"""Given a key, efficiently lookup and return the associated value.
Args:
key: The key
Returns:
The value
"""
return self._forward.get(key)
def get_key(self, value: Value) -> Key:
"""Given a value, efficiently lookup and return the associated key.
Args:
value: The value
Returns:
The key
"""
return self._reverse.get(value)
def contains_value(self, value: Value) -> bool:
"""Check if `value` is a value within this TwoWayDict.
Args:
value: The value to check.
Returns:
True if the value is within the values of this dict.
"""
return value in self._reverse
def __len__(self):
return len(self._forward)
def __contains__(self, item: Key) -> bool:
return item in self._forward

View File

@@ -48,7 +48,6 @@ from ._asyncio import create_task
from ._callback import invoke from ._callback import invoke
from ._context import active_app from ._context import active_app
from ._event_broker import NoHandler, extract_handler_actions from ._event_broker import NoHandler, extract_handler_actions
from ._filter import LineFilter, Monochrome
from ._path import _make_path_object_relative from ._path import _make_path_object_relative
from ._wait import wait_for_idle from ._wait import wait_for_idle
from .actions import SkipAction from .actions import SkipAction
@@ -62,6 +61,7 @@ from .driver import Driver
from .drivers.headless_driver import HeadlessDriver from .drivers.headless_driver import HeadlessDriver
from .features import FeatureFlag, parse_features from .features import FeatureFlag, parse_features
from .file_monitor import FileMonitor from .file_monitor import FileMonitor
from .filter import LineFilter, Monochrome
from .geometry import Offset, Region, Size from .geometry import Offset, Region, Size
from .keys import REPLACED_KEYS, _get_key_display from .keys import REPLACED_KEYS, _get_key_display
from .messages import CallbackType from .messages import CallbackType

View File

@@ -10,7 +10,6 @@ from .case import camel_to_snake
if TYPE_CHECKING: if TYPE_CHECKING:
from .message_pump import MessagePump from .message_pump import MessagePump
from .widget import Widget
@rich.repr.auto @rich.repr.auto

View File

@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
from rich.cells import cell_len
from rich.console import Console, RenderableType from rich.console import Console, RenderableType
from rich.protocol import rich_cast from rich.protocol import rich_cast
@@ -22,6 +23,9 @@ def measure(
Returns: Returns:
Width in cells Width in cells
""" """
if isinstance(renderable, str):
return cell_len(renderable)
width = default width = default
renderable = rich_cast(renderable) renderable = rich_cast(renderable)
get_console_width = getattr(renderable, "__rich_measure__", None) get_console_width = getattr(renderable, "__rich_measure__", None)

View File

@@ -9,7 +9,7 @@ from rich.segment import Segment
from rich.style import Style, StyleType from rich.style import Style, StyleType
from ._cache import FIFOCache from ._cache import FIFOCache
from ._filter import LineFilter from .filter import LineFilter
from ._segment_tools import index_to_cell_position from ._segment_tools import index_to_cell_position

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,29 @@
"""Make non-widget DataTable support classes available.""" """Make non-widget DataTable support classes available."""
from ._data_table import Column, Row from ._data_table import (
CellDoesNotExist,
CellKey,
CellType,
Column,
ColumnDoesNotExist,
ColumnKey,
CursorType,
DuplicateKey,
Row,
RowDoesNotExist,
RowKey,
)
__all__ = ["Column", "Row"] __all__ = [
"CellDoesNotExist",
"CellKey",
"CellType",
"Column",
"ColumnDoesNotExist",
"ColumnKey",
"CursorType",
"DuplicateKey",
"Row",
"RowDoesNotExist",
"RowKey",
]

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,44 @@
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.widgets import DataTable
# Shuffled around a bit to exercise sorting.
ROWS = [
("lane", "swimmer", "country", "time"),
(5, "Chad le Clos", "South Africa", 51.14),
(4, "Joseph Schooling", "Singapore", 50.39),
(2, "Michael Phelps", "United States", 51.14),
(6, "László Cseh", "Hungary", 51.14),
(3, "Li Zhuhao", "China", 51.26),
(8, "Mehdy Metella", "France", 51.58),
(7, "Tom Shields", "United States", 51.73),
(10, "Darren Burns", "Scotland", 51.84),
(1, "Aleksandr Sadovnikov", "Russia", 51.84),
]
class TableApp(App):
BINDINGS = [
Binding("s", "sort", "Sort"),
]
def compose(self) -> ComposeResult:
yield DataTable()
def on_mount(self) -> None:
table = self.query_one(DataTable)
table.focus()
rows = iter(ROWS)
column_labels = next(rows)
for column in column_labels:
table.add_column(column, key=column)
table.add_rows(rows)
def action_sort(self):
table = self.query_one(DataTable)
table.sort("time", "lane")
app = TableApp()
if __name__ == "__main__":
app.run()

View File

@@ -103,6 +103,11 @@ def test_datatable_column_cursor_render(snap_compare):
assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_column_cursor.py", press=press) assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_column_cursor.py", press=press)
def test_datatable_sort_multikey(snap_compare):
press = ["down", "right", "s"] # Also checks that sort doesn't move cursor.
assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_sort.py", press=press)
def test_footer_render(snap_compare): def test_footer_render(snap_compare):
assert snap_compare(WIDGET_EXAMPLES_DIR / "footer.py") assert snap_compare(WIDGET_EXAMPLES_DIR / "footer.py")

View File

@@ -1,11 +1,32 @@
from __future__ import annotations
import pytest
from rich.style import Style
from rich.text import Text
from textual._wait import wait_for_idle
from textual.actions import SkipAction
from textual.app import App from textual.app import App
from textual.coordinate import Coordinate from textual.coordinate import Coordinate
from textual.events import Click, MouseMove
from textual.message import Message from textual.message import Message
from textual.message_pump import MessagePump
from textual.widgets import DataTable from textual.widgets import DataTable
from textual.widgets.data_table import (
CellDoesNotExist,
CellKey,
ColumnDoesNotExist,
ColumnKey,
DuplicateKey,
Row,
RowDoesNotExist,
RowKey,
)
ROWS = [["0/0", "0/1"], ["1/0", "1/1"], ["2/0", "2/1"]]
class DataTableApp(App): class DataTableApp(App):
messages = []
messages_to_record = { messages_to_record = {
"CellHighlighted", "CellHighlighted",
"CellSelected", "CellSelected",
@@ -13,8 +34,13 @@ class DataTableApp(App):
"RowSelected", "RowSelected",
"ColumnHighlighted", "ColumnHighlighted",
"ColumnSelected", "ColumnSelected",
"HeaderSelected",
} }
def __init__(self):
super().__init__()
self.messages = []
def compose(self): def compose(self):
table = DataTable() table = DataTable()
table.focus() table.focus()
@@ -23,7 +49,11 @@ class DataTableApp(App):
def record_data_table_event(self, message: Message) -> None: def record_data_table_event(self, message: Message) -> None:
name = message.__class__.__name__ name = message.__class__.__name__
if name in self.messages_to_record: if name in self.messages_to_record:
self.messages.append(name) self.messages.append(message)
@property
def message_names(self) -> list[str]:
return [message.__class__.__name__ for message in self.messages]
async def _on_message(self, message: Message) -> None: async def _on_message(self, message: Message) -> None:
await super()._on_message(message) await super()._on_message(message)
@@ -32,68 +62,66 @@ class DataTableApp(App):
async def test_datatable_message_emission(): async def test_datatable_message_emission():
app = DataTableApp() app = DataTableApp()
messages = app.messages
expected_messages = [] expected_messages = []
async with app.run_test() as pilot: async with app.run_test() as pilot:
table = app.query_one(DataTable) table = app.query_one(DataTable)
assert messages == expected_messages assert app.message_names == expected_messages
table.add_columns("Column0", "Column1") table.add_columns("Column0", "Column1")
table.add_rows([["0/0", "0/1"], ["1/0", "1/1"], ["2/0", "2/1"]]) table.add_rows(ROWS)
# A CellHighlighted is emitted because there were no rows (and # A CellHighlighted is emitted because there were no rows (and
# therefore no highlighted cells), but then a row was added, and # therefore no highlighted cells), but then a row was added, and
# so the cell at (0, 0) became highlighted. # so the cell at (0, 0) became highlighted.
expected_messages.append("CellHighlighted") expected_messages.append("CellHighlighted")
await pilot.pause(2 / 100) await wait_for_idle(0)
assert messages == expected_messages assert app.message_names == expected_messages
# Pressing Enter when the cursor is on a cell emits a CellSelected # Pressing Enter when the cursor is on a cell emits a CellSelected
await pilot.press("enter") await pilot.press("enter")
await wait_for_idle(0)
expected_messages.append("CellSelected") expected_messages.append("CellSelected")
await pilot.pause(2 / 100) assert app.message_names == expected_messages
assert messages == expected_messages
# Moving the cursor left and up when the cursor is at origin # Moving the cursor left and up when the cursor is at origin
# emits no events, since the cursor doesn't move at all. # emits no events, since the cursor doesn't move at all.
await pilot.press("left", "up") await pilot.press("left", "up")
await pilot.pause(2 / 100) assert app.message_names == expected_messages
assert messages == expected_messages
# ROW CURSOR # ROW CURSOR
# Switch over to the row cursor... should emit a `RowHighlighted` # Switch over to the row cursor... should emit a `RowHighlighted`
table.cursor_type = "row" table.cursor_type = "row"
expected_messages.append("RowHighlighted") expected_messages.append("RowHighlighted")
await pilot.pause(2 / 100) await wait_for_idle(0)
assert messages == expected_messages assert app.message_names == expected_messages
# Select the row... # Select the row...
await pilot.press("enter") await pilot.press("enter")
await wait_for_idle(0)
expected_messages.append("RowSelected") expected_messages.append("RowSelected")
await pilot.pause(2 / 100) assert app.message_names == expected_messages
assert messages == expected_messages
# COLUMN CURSOR # COLUMN CURSOR
# Switching to the column cursor emits a `ColumnHighlighted` # Switching to the column cursor emits a `ColumnHighlighted`
table.cursor_type = "column" table.cursor_type = "column"
expected_messages.append("ColumnHighlighted") expected_messages.append("ColumnHighlighted")
await pilot.pause(2 / 100) await wait_for_idle(0)
assert messages == expected_messages assert app.message_names == expected_messages
# Select the column... # Select the column...
await pilot.press("enter") await pilot.press("enter")
expected_messages.append("ColumnSelected") expected_messages.append("ColumnSelected")
await pilot.pause(2 / 100) await wait_for_idle(0)
assert messages == expected_messages assert app.message_names == expected_messages
# NONE CURSOR # NONE CURSOR
# No messages get emitted at all... # No messages get emitted at all...
table.cursor_type = "none" table.cursor_type = "none"
await pilot.press("up", "down", "left", "right", "enter") await pilot.press("up", "down", "left", "right", "enter")
await pilot.pause(2 / 100) await wait_for_idle(0)
# No new messages since cursor not visible # No new messages since cursor not visible
assert messages == expected_messages assert app.message_names == expected_messages
# Edge case - if show_cursor is False, and the cursor type # Edge case - if show_cursor is False, and the cursor type
# is changed back to a visible type, then no messages should # is changed back to a visible type, then no messages should
@@ -101,49 +129,798 @@ async def test_datatable_message_emission():
table.show_cursor = False table.show_cursor = False
table.cursor_type = "cell" table.cursor_type = "cell"
await pilot.press("up", "down", "left", "right", "enter") await pilot.press("up", "down", "left", "right", "enter")
await pilot.pause(2 / 100) await wait_for_idle(0)
# No new messages since show_cursor = False # No new messages since show_cursor = False
assert messages == expected_messages assert app.message_names == expected_messages
# Now when show_cursor is set back to True, the appropriate # Now when show_cursor is set back to True, the appropriate
# message should be emitted for highlighting the cell. # message should be emitted for highlighting the cell.
table.show_cursor = True table.show_cursor = True
expected_messages.append("CellHighlighted") expected_messages.append("CellHighlighted")
await pilot.pause(2 / 100) await wait_for_idle(0)
assert messages == expected_messages assert app.message_names == expected_messages
# Similarly for showing the cursor again when row or column
# cursor was active before the cursor was hidden.
table.show_cursor = False
table.cursor_type = "row"
table.show_cursor = True
expected_messages.append("RowHighlighted")
await wait_for_idle(0)
assert app.message_names == expected_messages
table.show_cursor = False
table.cursor_type = "column"
table.show_cursor = True
expected_messages.append("ColumnHighlighted")
await wait_for_idle(0)
assert app.message_names == expected_messages
# Likewise, if the cursor_type is "none", and we change the # Likewise, if the cursor_type is "none", and we change the
# show_cursor to True, then no events should be raised since # show_cursor to True, then no events should be raised since
# the cursor is still not visible to the user. # the cursor is still not visible to the user.
table.cursor_type = "none" table.cursor_type = "none"
await pilot.press("up", "down", "left", "right", "enter") await pilot.press("up", "down", "left", "right", "enter")
await pilot.pause(2 / 100) await wait_for_idle(0)
assert messages == expected_messages assert app.message_names == expected_messages
async def test_add_rows():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
table.add_columns("A", "B")
row_keys = table.add_rows(ROWS)
# We're given a key for each row
assert len(row_keys) == len(ROWS)
assert len(row_keys) == len(table._data)
assert table.row_count == len(ROWS)
# Each key can be used to fetch a row from the DataTable
assert all(key in table._data for key in row_keys)
async def test_add_rows_user_defined_keys():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
key_a, key_b = table.add_columns("A", "B")
algernon_key = table.add_row(*ROWS[0], key="algernon")
table.add_row(*ROWS[1], key="charlie")
auto_key = table.add_row(*ROWS[2])
assert algernon_key == "algernon"
# We get a RowKey object back, but we can use our own string *or* this object
# to find the row we're looking for, they're considered equivalent for lookups.
assert isinstance(algernon_key, RowKey)
# Ensure the data in the table is mapped as expected
first_row = {key_a: ROWS[0][0], key_b: ROWS[0][1]}
assert table._data[algernon_key] == first_row
assert table._data["algernon"] == first_row
second_row = {key_a: ROWS[1][0], key_b: ROWS[1][1]}
assert table._data["charlie"] == second_row
third_row = {key_a: ROWS[2][0], key_b: ROWS[2][1]}
assert table._data[auto_key] == third_row
first_row = Row(algernon_key, height=1)
assert table.rows[algernon_key] == first_row
assert table.rows["algernon"] == first_row
async def test_add_row_duplicate_key():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
table.add_column("A")
table.add_row("1", key="1")
with pytest.raises(DuplicateKey):
table.add_row("2", key="1") # Duplicate row key
async def test_add_column_duplicate_key():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
table.add_column("A", key="A")
with pytest.raises(DuplicateKey):
table.add_column("B", key="A") # Duplicate column key
async def test_add_column_with_width():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
column = table.add_column("ABC", width=10, key="ABC")
row = table.add_row("123")
assert table.get_cell(row, column) == "123"
assert table.columns[column].width == 10
assert table.columns[column].render_width == 12 # 10 + (2 padding)
async def test_add_columns():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
column_keys = table.add_columns("1", "2", "3")
assert len(column_keys) == 3
assert len(table.columns) == 3
async def test_add_columns_user_defined_keys():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
key = table.add_column("Column", key="donut")
assert key == "donut"
assert key == key
async def test_clear(): async def test_clear():
app = DataTableApp() app = DataTableApp()
async with app.run_test(): async with app.run_test():
table = app.query_one(DataTable) table = app.query_one(DataTable)
assert table.cursor_cell == Coordinate(0, 0) assert table.cursor_coordinate == Coordinate(0, 0)
assert table.hover_cell == Coordinate(0, 0) assert table.hover_coordinate == Coordinate(0, 0)
# Add some data and update cursor positions # Add some data and update cursor positions
table.add_column("Column0") table.add_column("Column0")
table.add_rows([["Row0"], ["Row1"], ["Row2"]]) table.add_rows([["Row0"], ["Row1"], ["Row2"]])
table.cursor_cell = Coordinate(1, 0) table.cursor_coordinate = Coordinate(1, 0)
table.hover_cell = Coordinate(2, 0) table.hover_coordinate = Coordinate(2, 0)
# Ensure the cursor positions are reset to origin on clear() # Ensure the cursor positions are reset to origin on clear()
table.clear() table.clear()
assert table.cursor_cell == Coordinate(0, 0) assert table.cursor_coordinate == Coordinate(0, 0)
assert table.hover_cell == Coordinate(0, 0) assert table.hover_coordinate == Coordinate(0, 0)
# Ensure that the table has been cleared # Ensure that the table has been cleared
assert table.data == {} assert table._data == {}
assert table.rows == {} assert table.rows == {}
assert table.row_count == 0
assert len(table.columns) == 1 assert len(table.columns) == 1
# Clearing the columns too # Clearing the columns too
table.clear(columns=True) table.clear(columns=True)
assert len(table.columns) == 0 assert len(table.columns) == 0
async def test_column_labels() -> None:
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
table.add_columns("1", "2", "3")
actual_labels = [col.label.plain for col in table.columns.values()]
expected_labels = ["1", "2", "3"]
assert actual_labels == expected_labels
async def test_initial_column_widths() -> None:
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
foo, bar = table.add_columns("foo", "bar")
assert table.columns[foo].width == 3
assert table.columns[bar].width == 3
table.add_row("Hello", "World!")
await wait_for_idle()
assert table.columns[foo].content_width == 5
assert table.columns[bar].content_width == 6
table.add_row("Hello World!!!", "fo")
await wait_for_idle()
assert table.columns[foo].content_width == 14
assert table.columns[bar].content_width == 6
async def test_get_cell_returns_value_at_cell():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
table.add_column("Column1", key="C1")
table.add_row("TargetValue", key="R1")
assert table.get_cell("R1", "C1") == "TargetValue"
async def test_get_cell_invalid_row_key():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
table.add_column("Column1", key="C1")
table.add_row("TargetValue", key="R1")
with pytest.raises(CellDoesNotExist):
table.get_cell("INVALID_ROW", "C1")
async def test_get_cell_invalid_column_key():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
table.add_column("Column1", key="C1")
table.add_row("TargetValue", key="R1")
with pytest.raises(CellDoesNotExist):
table.get_cell("R1", "INVALID_COLUMN")
async def test_get_cell_at_returns_value_at_cell():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
table.add_columns("A", "B")
table.add_rows(ROWS)
assert table.get_cell_at(Coordinate(0, 0)) == "0/0"
async def test_get_cell_at_exception():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
table.add_columns("A", "B")
table.add_rows(ROWS)
with pytest.raises(CellDoesNotExist):
table.get_cell_at(Coordinate(9999, 0))
async def test_get_row():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
a, b, c = table.add_columns("A", "B", "C")
first_row = table.add_row(2, 4, 1)
second_row = table.add_row(3, 2, 1)
assert table.get_row(first_row) == [2, 4, 1]
assert table.get_row(second_row) == [3, 2, 1]
# Even if row positions change, keys should always refer to same rows.
table.sort(b)
assert table.get_row(first_row) == [2, 4, 1]
assert table.get_row(second_row) == [3, 2, 1]
async def test_get_row_invalid_row_key():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
with pytest.raises(RowDoesNotExist):
table.get_row("INVALID")
async def test_get_row_at():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
a, b, c = table.add_columns("A", "B", "C")
table.add_row(2, 4, 1)
table.add_row(3, 2, 1)
assert table.get_row_at(0) == [2, 4, 1]
assert table.get_row_at(1) == [3, 2, 1]
# If we sort, then the rows present at the indices *do* change!
table.sort(b)
# Since we sorted on column "B", the rows at indices 0 and 1 are swapped.
assert table.get_row_at(0) == [3, 2, 1]
assert table.get_row_at(1) == [2, 4, 1]
@pytest.mark.parametrize("index", (-1, 2))
async def test_get_row_at_invalid_index(index):
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
table.add_columns("A", "B", "C")
table.add_row(2, 4, 1)
table.add_row(3, 2, 1)
with pytest.raises(RowDoesNotExist):
table.get_row_at(index)
async def test_get_column():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
a, b = table.add_columns("A", "B")
table.add_rows(ROWS)
cells = table.get_column(a)
assert next(cells) == ROWS[0][0]
assert next(cells) == ROWS[1][0]
assert next(cells) == ROWS[2][0]
with pytest.raises(StopIteration):
next(cells)
async def test_get_column_invalid_key():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
with pytest.raises(ColumnDoesNotExist):
list(table.get_column("INVALID"))
async def test_get_column_at():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
table.add_columns("A", "B")
table.add_rows(ROWS)
first_column = list(table.get_column_at(0))
assert first_column == [ROWS[0][0], ROWS[1][0], ROWS[2][0]]
second_column = list(table.get_column_at(1))
assert second_column == [ROWS[0][1], ROWS[1][1], ROWS[2][1]]
@pytest.mark.parametrize("index", [-1, 5])
async def test_get_column_at_invalid_index(index):
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
with pytest.raises(ColumnDoesNotExist):
list(table.get_column_at(index))
async def test_update_cell_cell_exists():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
table.add_column("A", key="A")
table.add_row("1", key="1")
table.update_cell("1", "A", "NEW_VALUE")
assert table.get_cell("1", "A") == "NEW_VALUE"
async def test_update_cell_cell_doesnt_exist():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
table.add_column("A", key="A")
table.add_row("1", key="1")
with pytest.raises(CellDoesNotExist):
table.update_cell("INVALID", "CELL", "Value")
async def test_update_cell_at_coordinate_exists():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
column_0, column_1 = table.add_columns("A", "B")
row_0, *_ = table.add_rows(ROWS)
table.update_cell_at(Coordinate(0, 1), "newvalue")
assert table.get_cell(row_0, column_1) == "newvalue"
async def test_update_cell_at_coordinate_doesnt_exist():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
table.add_columns("A", "B")
table.add_rows(ROWS)
with pytest.raises(CellDoesNotExist):
table.update_cell_at(Coordinate(999, 999), "newvalue")
@pytest.mark.parametrize(
"label,new_value,new_content_width",
[
# Shorter than initial cell value, larger than label => width remains same
("A", "BB", 3),
# Larger than cell value, shorter than label => width remains that of label
("1234567", "1234", 7),
# Shorter than cell value, shorter than label => width remains same
("12345", "123", 5),
# Larger than cell value, larger than label => width updates to new cell value
("12345", "123456789", 9),
],
)
async def test_update_cell_at_column_width(label, new_value, new_content_width):
# Initial cell values are length 3. Let's update cell content and ensure
# that the width of the column is correct given the new cell content widths
# and the label of the column the cell is in.
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
key, _ = table.add_columns(label, "Column2")
table.add_rows(ROWS)
first_column = table.columns.get(key)
table.update_cell_at(Coordinate(0, 0), new_value, update_width=True)
await wait_for_idle()
assert first_column.content_width == new_content_width
assert first_column.render_width == new_content_width + 2
async def test_coordinate_to_cell_key():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
column_key, _ = table.add_columns("Column0", "Column1")
row_key = table.add_row("A", "B")
cell_key = table.coordinate_to_cell_key(Coordinate(0, 0))
assert cell_key == CellKey(row_key, column_key)
async def test_coordinate_to_cell_key_invalid_coordinate():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
with pytest.raises(CellDoesNotExist):
table.coordinate_to_cell_key(Coordinate(9999, 9999))
def make_click_event(sender: MessagePump):
return Click(
sender=sender,
x=1,
y=2,
delta_x=0,
delta_y=0,
button=0,
shift=False,
meta=False,
ctrl=False,
)
async def test_datatable_on_click_cell_cursor():
"""When the cell cursor is used, and we click, we emit a CellHighlighted
*and* a CellSelected message for the cell that was clicked.
Regression test for https://github.com/Textualize/textual/issues/1723"""
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
click = make_click_event(app)
column_key = table.add_column("ABC")
table.add_row("123")
row_key = table.add_row("456")
table.on_click(event=click)
await wait_for_idle(0)
# There's two CellHighlighted events since a cell is highlighted on initial load,
# then when we click, another cell is highlighted (and selected).
assert app.message_names == [
"CellHighlighted",
"CellHighlighted",
"CellSelected",
]
cell_highlighted_event: DataTable.CellHighlighted = app.messages[1]
assert cell_highlighted_event.sender is table
assert cell_highlighted_event.value == "456"
assert cell_highlighted_event.cell_key == CellKey(row_key, column_key)
assert cell_highlighted_event.coordinate == Coordinate(1, 0)
cell_selected_event: DataTable.CellSelected = app.messages[2]
assert cell_selected_event.sender is table
assert cell_selected_event.value == "456"
assert cell_selected_event.cell_key == CellKey(row_key, column_key)
assert cell_selected_event.coordinate == Coordinate(1, 0)
async def test_on_click_row_cursor():
"""When the row cursor is used, and we click, we emit a RowHighlighted
*and* a RowSelected message for the row that was clicked."""
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
table.cursor_type = "row"
click = make_click_event(app)
table.add_column("ABC")
table.add_row("123")
row_key = table.add_row("456")
table.on_click(event=click)
await wait_for_idle(0)
assert app.message_names == ["RowHighlighted", "RowHighlighted", "RowSelected"]
row_highlighted: DataTable.RowHighlighted = app.messages[1]
assert row_highlighted.sender is table
assert row_highlighted.row_key == row_key
assert row_highlighted.cursor_row == 1
row_selected: DataTable.RowSelected = app.messages[2]
assert row_selected.sender is table
assert row_selected.row_key == row_key
assert row_highlighted.cursor_row == 1
async def test_on_click_column_cursor():
"""When the column cursor is used, and we click, we emit a ColumnHighlighted
*and* a ColumnSelected message for the column that was clicked."""
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
table.cursor_type = "column"
column_key = table.add_column("ABC")
table.add_row("123")
table.add_row("456")
click = make_click_event(app)
table.on_click(event=click)
await wait_for_idle(0)
assert app.message_names == [
"ColumnHighlighted",
"ColumnHighlighted",
"ColumnSelected",
]
column_highlighted: DataTable.ColumnHighlighted = app.messages[1]
assert column_highlighted.sender is table
assert column_highlighted.column_key == column_key
assert column_highlighted.cursor_column == 0
column_selected: DataTable.ColumnSelected = app.messages[2]
assert column_selected.sender is table
assert column_selected.column_key == column_key
assert column_highlighted.cursor_column == 0
async def test_hover_coordinate():
"""Ensure that the hover_coordinate reactive is updated as expected."""
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
table.add_column("ABC")
table.add_row("123")
table.add_row("456")
assert table.hover_coordinate == Coordinate(0, 0)
mouse_move = MouseMove(
sender=app,
x=1,
y=2,
delta_x=0,
delta_y=0,
button=0,
shift=False,
meta=False,
ctrl=False,
style=Style(meta={"row": 1, "column": 2}),
)
table.on_mouse_move(mouse_move)
await wait_for_idle(0)
assert table.hover_coordinate == Coordinate(1, 2)
async def test_header_selected():
"""Ensure that a HeaderSelected event gets posted when we click
on the header in the DataTable."""
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
column = table.add_column("number")
table.add_row(3)
click_event = Click(
sender=table,
x=3,
y=0,
delta_x=0,
delta_y=0,
button=0,
shift=False,
meta=False,
ctrl=False,
)
table.on_click(click_event)
await wait_for_idle(0)
message: DataTable.HeaderSelected = app.messages[-1]
assert message.sender is table
assert message.label == Text("number")
assert message.column_index == 0
assert message.column_key == column
# Now hide the header and click in the exact same place - no additional message emitted.
table.show_header = False
table.on_click(click_event)
await wait_for_idle(0)
assert app.message_names.count("HeaderSelected") == 1
async def test_sort_coordinate_and_key_access():
"""Ensure that, after sorting, that coordinates and cell keys
can still be used to retrieve the correct cell."""
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
column = table.add_column("number")
row_three = table.add_row(3)
row_one = table.add_row(1)
row_two = table.add_row(2)
# Items inserted in correct initial positions (before sort)
assert table.get_cell_at(Coordinate(0, 0)) == 3
assert table.get_cell_at(Coordinate(1, 0)) == 1
assert table.get_cell_at(Coordinate(2, 0)) == 2
table.sort(column)
# The keys still refer to the same cells...
assert table.get_cell(row_one, column) == 1
assert table.get_cell(row_two, column) == 2
assert table.get_cell(row_three, column) == 3
# ...even though the values under the coordinates have changed...
assert table.get_cell_at(Coordinate(0, 0)) == 1
assert table.get_cell_at(Coordinate(1, 0)) == 2
assert table.get_cell_at(Coordinate(2, 0)) == 3
assert table.ordered_rows[0].key == row_one
assert table.ordered_rows[1].key == row_two
assert table.ordered_rows[2].key == row_three
async def test_sort_reverse_coordinate_and_key_access():
"""Ensure that, after sorting, that coordinates and cell keys
can still be used to retrieve the correct cell."""
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
column = table.add_column("number")
row_three = table.add_row(3)
row_one = table.add_row(1)
row_two = table.add_row(2)
# Items inserted in correct initial positions (before sort)
assert table.get_cell_at(Coordinate(0, 0)) == 3
assert table.get_cell_at(Coordinate(1, 0)) == 1
assert table.get_cell_at(Coordinate(2, 0)) == 2
table.sort(column, reverse=True)
# The keys still refer to the same cells...
assert table.get_cell(row_one, column) == 1
assert table.get_cell(row_two, column) == 2
assert table.get_cell(row_three, column) == 3
# ...even though the values under the coordinates have changed...
assert table.get_cell_at(Coordinate(0, 0)) == 3
assert table.get_cell_at(Coordinate(1, 0)) == 2
assert table.get_cell_at(Coordinate(2, 0)) == 1
assert table.ordered_rows[0].key == row_three
assert table.ordered_rows[1].key == row_two
assert table.ordered_rows[2].key == row_one
async def test_cell_cursor_highlight_events():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
column_one_key, column_two_key = table.add_columns("A", "B")
_ = table.add_row(0, 1)
row_two_key = table.add_row(2, 3)
# Since initial position is (0, 0), cursor doesn't move so no event posted
table.action_cursor_up()
table.action_cursor_left()
await wait_for_idle(0)
assert table.app.message_names == [
"CellHighlighted"
] # Initial highlight on load
# Move the cursor one cell down, and check the highlighted event posted
table.action_cursor_down()
await wait_for_idle(0)
assert len(table.app.messages) == 2
latest_message: DataTable.CellHighlighted = table.app.messages[-1]
assert isinstance(latest_message, DataTable.CellHighlighted)
assert latest_message.value == 2
assert latest_message.coordinate == Coordinate(1, 0)
assert latest_message.cell_key == CellKey(row_two_key, column_one_key)
# Now move the cursor to the right, and check highlighted event posted
table.action_cursor_right()
await wait_for_idle(0)
assert len(table.app.messages) == 3
latest_message = table.app.messages[-1]
assert latest_message.coordinate == Coordinate(1, 1)
assert latest_message.cell_key == CellKey(row_two_key, column_two_key)
async def test_row_cursor_highlight_events():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
table.cursor_type = "row"
table.add_columns("A", "B")
row_one_key = table.add_row(0, 1)
row_two_key = table.add_row(2, 3)
# Since initial position is row_index=0, the following actions do nothing.
with pytest.raises(SkipAction):
table.action_cursor_up()
table.action_cursor_left()
table.action_cursor_right()
await wait_for_idle(0)
assert table.app.message_names == ["RowHighlighted"] # Initial highlight
# Move the row cursor from row 0 to row 1, check the highlighted event posted
table.action_cursor_down()
await wait_for_idle(0)
assert len(table.app.messages) == 2
latest_message: DataTable.RowHighlighted = table.app.messages[-1]
assert isinstance(latest_message, DataTable.RowHighlighted)
assert latest_message.row_key == row_two_key
assert latest_message.cursor_row == 1
# Move the row cursor back up to row 0, check the highlighted event posted
table.action_cursor_up()
await wait_for_idle(0)
assert len(table.app.messages) == 3
latest_message = table.app.messages[-1]
assert latest_message.row_key == row_one_key
assert latest_message.cursor_row == 0
async def test_column_cursor_highlight_events():
app = DataTableApp()
async with app.run_test():
table = app.query_one(DataTable)
table.cursor_type = "column"
column_one_key, column_two_key = table.add_columns("A", "B")
table.add_row(0, 1)
table.add_row(2, 3)
# Since initial position is column_index=0, the following actions do nothing.
with pytest.raises(SkipAction):
table.action_cursor_left()
table.action_cursor_up()
table.action_cursor_down()
await wait_for_idle(0)
assert table.app.message_names == ["ColumnHighlighted"] # Initial highlight
# Move the column cursor from column 0 to column 1,
# check the highlighted event posted
table.action_cursor_right()
await wait_for_idle(0)
assert len(table.app.messages) == 2
latest_message: DataTable.ColumnHighlighted = table.app.messages[-1]
assert isinstance(latest_message, DataTable.ColumnHighlighted)
assert latest_message.column_key == column_two_key
assert latest_message.cursor_column == 1
# Move the column cursor left, back to column 0,
# check the highlighted event posted again.
table.action_cursor_left()
await wait_for_idle(0)
assert len(table.app.messages) == 3
latest_message = table.app.messages[-1]
assert latest_message.column_key == column_one_key
assert latest_message.cursor_column == 0
def test_key_equals_equivalent_string():
text = "Hello"
key = RowKey(text)
assert key == text
assert hash(key) == hash(text)
def test_key_doesnt_match_non_equal_string():
key = ColumnKey("123")
text = "laksjdlaskjd"
assert key != text
assert hash(key) != hash(text)
def test_key_equals_self():
row_key = RowKey()
column_key = ColumnKey()
assert row_key == row_key
assert column_key == column_key
assert row_key != column_key
def test_key_string_lookup():
# Indirectly covered by other tests, but let's explicitly document
# in tests how we intend for the keys to work for cache lookups.
dictionary = {
"foo": "bar",
RowKey("hello"): "world",
}
assert dictionary["foo"] == "bar"
assert dictionary[RowKey("foo")] == "bar"
assert dictionary["hello"] == "world"
assert dictionary[RowKey("hello")] == "world"

View File

@@ -2,8 +2,8 @@ import pytest
from rich.segment import Segment from rich.segment import Segment
from rich.style import Style from rich.style import Style
from textual._filter import Monochrome
from textual._segment_tools import NoCellPositionForIndex from textual._segment_tools import NoCellPositionForIndex
from textual.filter import Monochrome
from textual.strip import Strip from textual.strip import Strip

View File

@@ -1,66 +0,0 @@
import asyncio
from rich.text import Text
from textual.app import App, ComposeResult
from textual.widgets import DataTable
class TableApp(App):
def compose(self) -> ComposeResult:
yield DataTable()
async def test_table_clear() -> None:
"""Check DataTable.clear"""
app = TableApp()
async with app.run_test() as pilot:
table = app.query_one(DataTable)
table.add_columns("foo", "bar")
assert table.row_count == 0
table.add_row("Hello", "World!")
assert [col.label for col in table.columns] == [Text("foo"), Text("bar")]
assert table.data == {0: ["Hello", "World!"]}
assert table.row_count == 1
table.clear()
assert [col.label for col in table.columns] == [Text("foo"), Text("bar")]
assert table.data == {}
assert table.row_count == 0
async def test_table_clear_with_columns() -> None:
"""Check DataTable.clear(columns=True)"""
app = TableApp()
async with app.run_test() as pilot:
table = app.query_one(DataTable)
table.add_columns("foo", "bar")
assert table.row_count == 0
table.add_row("Hello", "World!")
assert [col.label for col in table.columns] == [Text("foo"), Text("bar")]
assert table.data == {0: ["Hello", "World!"]}
assert table.row_count == 1
table.clear(columns=True)
assert [col.label for col in table.columns] == []
assert table.data == {}
assert table.row_count == 0
async def test_table_add_row() -> None:
app = TableApp()
async with app.run_test():
table = app.query_one(DataTable)
table.add_columns("foo", "bar")
assert table.columns[0].width == 3
assert table.columns[1].width == 3
table.add_row("Hello", "World!")
await asyncio.sleep(0)
assert table.columns[0].content_width == 5
assert table.columns[1].content_width == 6
table.add_row("Hello World!!!", "fo")
await asyncio.sleep(0)
assert table.columns[0].content_width == 14
assert table.columns[1].content_width == 6

View File

@@ -0,0 +1,45 @@
import pytest
from textual._two_way_dict import TwoWayDict
@pytest.fixture
def two_way_dict():
return TwoWayDict(
{
1: 10,
2: 20,
3: 30,
}
)
def test_get(two_way_dict):
assert two_way_dict.get(1) == 10
def test_get_key(two_way_dict):
assert two_way_dict.get_key(30) == 3
def test_set_item(two_way_dict):
two_way_dict[40] = 400
assert two_way_dict.get(40) == 400
assert two_way_dict.get_key(400) == 40
def test_len(two_way_dict):
assert len(two_way_dict) == 3
def test_delitem(two_way_dict):
assert two_way_dict.get(3) == 30
assert two_way_dict.get_key(30) == 3
del two_way_dict[3]
assert two_way_dict.get(3) is None
assert two_way_dict.get_key(30) is None
def test_contains(two_way_dict):
assert 1 in two_way_dict
assert 10 not in two_way_dict