From 6352ceb61b19b2ee01980bb6dea3571085a6b8ae Mon Sep 17 00:00:00 2001 From: darrenburns Date: Tue, 11 Apr 2023 18:48:58 +0100 Subject: [PATCH] Datatable remove row (#2253) * Checking in remove_row progress * Ensuring structures updated correctly when row deleted * Clamping index * Failed attempt * Removing rows * Update a type hint in DataTable * Remove some code that wasnt required * Use index syntax instead of get * Add DataTable remove row test * Snapshot tests for removing rows * Add a docstring for DataTable.remove_row method * Update changelog regarding DataTable.remove_row * Add check_idle call to remove_row --- CHANGELOG.md | 2 +- src/textual/_two_way_dict.py | 3 + src/textual/widgets/_data_table.py | 43 ++++- .../__snapshots__/test_snapshots.ambr | 158 ++++++++++++++++++ .../snapshot_apps/data_table_remove_row.py | 51 ++++++ tests/snapshot_tests/test_snapshots.py | 5 + tests/test_data_table.py | 13 ++ 7 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 tests/snapshot_tests/snapshot_apps/data_table_remove_row.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2124f2dd3c..6bc2f69e9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). - ## Unreleased ### Added +- Added `DataTable.remove_row` method https://github.com/Textualize/textual/pull/2253 - `Widget.scroll_to_center` now scrolls the widget to the center of the screen https://github.com/Textualize/textual/pull/2255 ## [0.19.1] - 2023-04-10 diff --git a/src/textual/_two_way_dict.py b/src/textual/_two_way_dict.py index d733edcdcc..123e1848d8 100644 --- a/src/textual/_two_way_dict.py +++ b/src/textual/_two_way_dict.py @@ -29,6 +29,9 @@ def __delitem__(self, key: Key) -> None: self._forward.__delitem__(key) self._reverse.__delitem__(value) + def __iter__(self): + return iter(self._forward) + def get(self, key: Key) -> Value: """Given a key, efficiently lookup and return the associated value. diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index 369c11778e..5e9a0321e4 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -308,7 +308,9 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): cursor_coordinate: Reactive[Coordinate] = Reactive( Coordinate(0, 0), repaint=False, always_update=True ) - hover_coordinate: Reactive[Coordinate] = Reactive(Coordinate(0, 0), repaint=False) + hover_coordinate: Reactive[Coordinate] = Reactive( + Coordinate(0, 0), repaint=False, always_update=True + ) class CellHighlighted(Message, bubble=True): """Posted when the cursor moves to highlight a new cell. @@ -1186,6 +1188,10 @@ def clear(self, columns: bool = False) -> Self: self._label_column = Column(self._label_column_key, Text(), auto_width=True) self._labelled_row_exists = False self.refresh() + self.scroll_x = 0 + self.scroll_y = 0 + self.scroll_target_x = 0 + self.scroll_target_y = 0 return self def add_column( @@ -1322,6 +1328,41 @@ def add_rows(self, rows: Iterable[Iterable[CellType]]) -> list[RowKey]: row_keys.append(row_key) return row_keys + def remove_row(self, row_key: RowKey | str) -> None: + """Remove a row (identified by a key) from the DataTable. + + Args: + row_key: The key identifying the row to remove. + + Raises: + RowDoesNotExist: If the row key does not exist. + """ + if row_key not in self._row_locations: + raise RowDoesNotExist(f"Row key {row_key!r} is not valid.") + + self._require_update_dimensions = True + self.check_idle() + + index_to_delete = self._row_locations.get(row_key) + new_row_locations = TwoWayDict({}) + for row_location_key in self._row_locations: + row_index = self._row_locations.get(row_location_key) + if row_index > index_to_delete: + new_row_locations[row_location_key] = row_index - 1 + elif row_index < index_to_delete: + new_row_locations[row_location_key] = row_index + + self._row_locations = new_row_locations + + del self.rows[row_key] + del self._data[row_key] + + self.cursor_coordinate = self.cursor_coordinate + self.hover_coordinate = self.hover_coordinate + + self._update_count += 1 + self.refresh(layout=True) + def on_idle(self) -> None: """Runs when the message pump is empty. diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index ab9dc85c3d..2f9c7534b8 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -12593,6 +12593,164 @@ ''' # --- +# name: test_datatable_remove_row + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableApp + + + + + + + + + +  lane  swimmer               country        time   +  5     Chad le Clos          South Africa   51.14  +  4     Joseph Schooling      Singapore      50.39  +  6     László Cseh           Hungary        51.14  +  3     Li Zhuhao             China          51.26  +  7     Tom Shields           United States  51.73  +  10    Darren Burns          Scotland       51.84  + + + + + + + + + + + + + + + + + + + + + + ''' +# --- # name: test_datatable_render ''' diff --git a/tests/snapshot_tests/snapshot_apps/data_table_remove_row.py b/tests/snapshot_tests/snapshot_apps/data_table_remove_row.py new file mode 100644 index 0000000000..0fd6b06d02 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/data_table_remove_row.py @@ -0,0 +1,51 @@ +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual.widgets import DataTable + +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): + """Snapshot app for testing removal of rows. + Removes several rows, so we can check that the display of the + DataTable updates as expected.""" + + BINDINGS = [ + Binding("r", "remove_row", "Remove Row"), + ] + + 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) + + for row in rows: + table.add_row(*row, key=str(row[0])) + + def action_remove_row(self): + table = self.query_one(DataTable) + table.remove_row("2") + table.remove_row("8") + table.remove_row("1") + + +app = TableApp() +if __name__ == "__main__": + app.run() diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index edc151e28d..d7d839a9e6 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -116,6 +116,11 @@ def test_datatable_sort_multikey(snap_compare): assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_sort.py", press=press) +def test_datatable_remove_row(snap_compare): + press = ["r"] + assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_remove_row.py", press=press) + + def test_datatable_labels_and_fixed_data(snap_compare): # Ensure that we render correctly when there are fixed rows/cols and labels. assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_row_labels.py") diff --git a/tests/test_data_table.py b/tests/test_data_table.py index a7ae484a4a..56b148d562 100644 --- a/tests/test_data_table.py +++ b/tests/test_data_table.py @@ -289,6 +289,19 @@ async def test_add_columns_user_defined_keys(): assert key == key +async def test_remove_row(): + app = DataTableApp() + async with app.run_test(): + table = app.query_one(DataTable) + table.add_columns("A", "B") + for row in ROWS: + table.add_row(row, key=row[0]) + + assert len(table.rows) == 3 + table.remove_row(ROWS[0][0]) + assert len(table.rows) == 2 + + async def test_clear(): app = DataTableApp() async with app.run_test():