Repository: Textualize/textual

Issue:
No way to get an index from `DataTable`'s row key
I would like a public API equivalent to `table._row_locations.get(row_key)`.

One use case, for instance, is adding content from some other data structure, then select a row/cell. `move_cursor` accepts indices, so how to I use that?
An option is to use `enumerate` and count the rows I'm adding, but that's not very nice and doesn't work in conjuction with `sort` which makes the indices I got from `enumerate` useless.

<!-- This is valid Markdown, do not quote! -->
# Textual Diagnostics

## Versions

| Name    | Value  |
|---------|--------|
| Textual | 0.24.1 |
| Rich    | 13.3.5 |

## Python

| Name           | Value                                                                     |
|----------------|---------------------------------------------------------------------------|
| Version        | 3.10.1                                                                    |
| Implementation | CPython                                                                   |
| Compiler       | Clang 13.0.0 (clang-1300.0.29.30)                                         |
| Executable     | /Users/bari/.pyenv/versions/3.10.1/envs/undo-core-venv3.10/bin/python3.10 |

## Operating System

| Name    | Value                                                                                             |
|---------|---------------------------------------------------------------------------------------------------|
| System  | Darwin                                                                                            |
| Release | 21.6.0                                                                                            |
| Version | Darwin Kernel Version 21.6.0: Mon Aug 22 20:17:10 PDT 2022; root:xnu-8020.140.49~2/RELEASE_X86_64 |

## Terminal

| Name                 | Value                   |
|----------------------|-------------------------|
| Terminal Application | iTerm.app (3.5.0beta10) |
| TERM                 | xterm-256color          |
| COLORTERM            | truecolor               |
| FORCE_COLOR          | *Not set*               |
| NO_COLOR             | *Not set*               |

## Rich Console options

| Name           | Value                |
|----------------|----------------------|
| size           | width=178, height=45 |
| legacy_windows | False                |
| min_width      | 1                    |
| max_width      | 178                  |
| is_terminal    | False                |
| encoding       | utf-8                |
| max_height     | 45                   |
| justify        | None                 |
| overflow       | None                 |
| no_wrap        | False                |
| highlight      | None                 |
| markup         | None                 |
| height         | None                 |




Discussion:
We found the following entries in the [FAQ](https://github.com/textualize/textual/blob/main/FAQ.md) which you may find helpful:


- [Why doesn't the `DataTable` scroll programmatically?](https://github.com/textualize/textual/blob/main/FAQ.md#why-doesn't-the-`datatable`-scroll-programmatically)
- [How do I center a widget in a screen?](https://github.com/textualize/textual/blob/main/FAQ.md#how-do-i-center-a-widget-in-a-screen)

Feel free to close this issue if you found an answer in the FAQ. Otherwise, please give us a little time to review.

This is an automated reply, generated by [FAQtory](https://github.com/willmcgugan/faqtory)

Resolving patch:
diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py
--- a/src/textual/widgets/_data_table.py
+++ b/src/textual/widgets/_data_table.py
@@ -772,7 +772,7 @@ def update_cell_at(
         row_key, column_key = self.coordinate_to_cell_key(coordinate)
         self.update_cell(row_key, column_key, value, update_width=update_width)
 
-    def get_cell(self, row_key: RowKey, column_key: ColumnKey) -> CellType:
+    def get_cell(self, row_key: RowKey | str, column_key: ColumnKey | str) -> CellType:
         """Given a row key and column key, return the value of the corresponding cell.
 
         Args:
@@ -805,6 +805,32 @@ def get_cell_at(self, coordinate: Coordinate) -> CellType:
         row_key, column_key = self.coordinate_to_cell_key(coordinate)
         return self.get_cell(row_key, column_key)
 
+    def get_cell_coordinate(
+        self, row_key: RowKey | str, column_key: Column | str
+    ) -> Coordinate:
+        """Given a row key and column key, return the corresponding cell coordinate.
+
+        Args:
+            row_key: The row key of the cell.
+            column_key: The column key of the cell.
+
+        Returns:
+            The current coordinate of the cell identified by the row and column keys.
+
+        Raises:
+            CellDoesNotExist: If the specified cell does not exist.
+        """
+        if (
+            row_key not in self._row_locations
+            or column_key not in self._column_locations
+        ):
+            raise CellDoesNotExist(
+                f"No cell exists for row_key={row_key!r}, column_key={column_key!r}."
+            )
+        row_index = self._row_locations.get(row_key)
+        column_index = self._column_locations.get(column_key)
+        return Coordinate(row_index, column_index)
+
     def get_row(self, row_key: RowKey | str) -> list[CellType]:
         """Get the values from the row identified by the given row key.
 
@@ -844,6 +870,22 @@ def get_row_at(self, row_index: int) -> list[CellType]:
         row_key = self._row_locations.get_key(row_index)
         return self.get_row(row_key)
 
+    def get_row_index(self, row_key: RowKey | str) -> int:
+        """Return the current index for the row identified by row_key.
+
+        Args:
+            row_key: The row key to find the current index of.
+
+        Returns:
+            The current index of the specified row key.
+
+        Raises:
+            RowDoesNotExist: If the row key does not exist.
+        """
+        if row_key not in self._row_locations:
+            raise RowDoesNotExist(f"No row exists for row_key={row_key!r}")
+        return self._row_locations.get(row_key)
+
     def get_column(self, column_key: ColumnKey | str) -> Iterable[CellType]:
         """Get the values from the column identified by the given column key.
 
@@ -882,6 +924,22 @@ def get_column_at(self, column_index: int) -> Iterable[CellType]:
         column_key = self._column_locations.get_key(column_index)
         yield from self.get_column(column_key)
 
+    def get_column_index(self, column_key: ColumnKey | str) -> int:
+        """Return the current index for the column identified by column_key.
+
+        Args:
+            column_key: The column key to find the current index of.
+
+        Returns:
+            The current index of the specified column key.
+
+        Raises:
+            ColumnDoesNotExist: If the column key does not exist.
+        """
+        if column_key not in self._column_locations:
+            raise ColumnDoesNotExist(f"No column exists for column_key={column_key!r}")
+        return self._column_locations.get(column_key)
+
     def _clear_caches(self) -> None:
         self._row_render_cache.clear()
         self._cell_render_cache.clear()


Test patch:
diff --git a/tests/test_data_table.py b/tests/test_data_table.py
--- a/tests/test_data_table.py
+++ b/tests/test_data_table.py
@@ -392,6 +392,46 @@ async def test_get_cell_invalid_column_key():
             table.get_cell("R1", "INVALID_COLUMN")
 
 
+async def test_get_cell_coordinate_returns_coordinate():
+    app = DataTableApp()
+    async with app.run_test():
+        table = app.query_one(DataTable)
+        table.add_column("Column1", key="C1")
+        table.add_column("Column2", key="C2")
+        table.add_column("Column3", key="C3")
+        table.add_row("ValR1C1", "ValR1C2", "ValR1C3", key="R1")
+        table.add_row("ValR2C1", "ValR2C2", "ValR2C3", key="R2")
+        table.add_row("ValR3C1", "ValR3C2", "ValR3C3", key="R3")
+
+        assert table.get_cell_coordinate('R1', 'C1') == Coordinate(0, 0)
+        assert table.get_cell_coordinate('R2', 'C2') == Coordinate(1, 1)
+        assert table.get_cell_coordinate('R1', 'C3') == Coordinate(0, 2)
+        assert table.get_cell_coordinate('R3', 'C1') == Coordinate(2, 0)
+        assert table.get_cell_coordinate('R3', 'C2') == Coordinate(2, 1)
+
+
+async def test_get_cell_coordinate_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):
+            coordinate = table.get_cell_coordinate('INVALID_ROW', 'C1')
+
+
+async def test_get_cell_coordinate_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):
+            coordinate = table.get_cell_coordinate('R1', 'INVALID_COLUMN')
+
+
 async def test_get_cell_at_returns_value_at_cell():
     app = DataTableApp()
     async with app.run_test():
@@ -465,6 +505,32 @@ async def test_get_row_at_invalid_index(index):
             table.get_row_at(index)
 
 
+async def test_get_row_index_returns_index():
+    app = DataTableApp()
+    async with app.run_test():
+        table = app.query_one(DataTable)
+        table.add_column("Column1", key="C1")
+        table.add_column("Column2", key="C2")
+        table.add_row("ValR1C1", "ValR1C2", key="R1")
+        table.add_row("ValR2C1", "ValR2C2", key="R2")
+        table.add_row("ValR3C1", "ValR3C2", key="R3")
+
+        assert table.get_row_index('R1') == 0
+        assert table.get_row_index('R2') == 1
+        assert table.get_row_index('R3') == 2
+
+
+async def test_get_row_index_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(RowDoesNotExist):
+            index = table.get_row_index('InvalidRow')
+
+
 async def test_get_column():
     app = DataTableApp()
     async with app.run_test():
@@ -509,6 +575,34 @@ async def test_get_column_at_invalid_index(index):
         with pytest.raises(ColumnDoesNotExist):
             list(table.get_column_at(index))
 
+async def test_get_column_index_returns_index():
+    app = DataTableApp()
+    async with app.run_test():
+        table = app.query_one(DataTable)
+        table.add_column("Column1", key="C1")
+        table.add_column("Column2", key="C2")
+        table.add_column("Column3", key="C3")
+        table.add_row("ValR1C1", "ValR1C2", "ValR1C3",  key="R1")
+        table.add_row("ValR2C1", "ValR2C2", "ValR2C3",  key="R2")
+
+        assert table.get_column_index('C1') == 0
+        assert table.get_column_index('C2') == 1
+        assert table.get_column_index('C3') == 2
+
+
+async def test_get_column_index_invalid_column_key():
+    app = DataTableApp()
+    async with app.run_test():
+        table = app.query_one(DataTable)
+        table.add_column("Column1", key="C1")
+        table.add_column("Column2", key="C2")
+        table.add_column("Column3", key="C3")
+        table.add_row("TargetValue1", "TargetValue2", "TargetValue3",  key="R1")
+
+        with pytest.raises(ColumnDoesNotExist):
+            index = table.get_column_index('InvalidCol')
+
+
 
 async def test_update_cell_cell_exists():
     app = DataTableApp()