Skip to content

Commit

Permalink
feat: Table data hooks allow None (#463)
Browse files Browse the repository at this point in the history
Fixes #385 

Table data hooks now allow `None`, and return `None` in that case. Also
changed default `sentinel` value to be an empty tuple to ensure there is
a difference by default but still falsy.

BREAKING CHANGE: Default sentinel value for table data hooks is now an
empty tuple, ()
  • Loading branch information
jnumainville authored May 15, 2024
1 parent ce1a019 commit 910a57c
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 34 deletions.
10 changes: 6 additions & 4 deletions plugins/ui/src/deephaven/ui/hooks/use_cell_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
from ..types import Sentinel


def _cell_data(data: pd.DataFrame | Sentinel, is_sentinel: bool) -> Any | Sentinel:
def _cell_data(
data: pd.DataFrame | Sentinel | None, is_sentinel: bool
) -> Any | Sentinel:
"""
Return the first cell of the table.
Expand All @@ -21,19 +23,19 @@ def _cell_data(data: pd.DataFrame | Sentinel, is_sentinel: bool) -> Any | Sentin
The first cell of the table.
"""
try:
return data if is_sentinel else data.iloc[0, 0]
return data if is_sentinel or data is None else data.iloc[0, 0]
except IndexError:
# if there is a static table with no rows, we will get an IndexError
raise IndexError("Cannot get row list from an empty table")


def use_cell_data(table: Table, sentinel: Sentinel = None) -> Any:
def use_cell_data(table: Table | None, sentinel: Sentinel = ()) -> Any | Sentinel:
"""
Return the first cell of the table. The table should already be filtered to only have a single cell.
Args:
table: The table to extract the cell from.
sentinel: The sentinel value to return if the table is ticking but empty. Defaults to None.
sentinel: The sentinel value to return if the table is ticking but empty. Defaults to ().
Returns:
Any: The first cell of the table.
Expand Down
12 changes: 7 additions & 5 deletions plugins/ui/src/deephaven/ui/hooks/use_column_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@


def _column_data(
data: pd.DataFrame | Sentinel, is_sentinel: bool
) -> ColumnData | Sentinel:
data: pd.DataFrame | Sentinel | None, is_sentinel: bool
) -> ColumnData | Sentinel | None:
"""
Return the first column of the table as a list.
Expand All @@ -22,19 +22,21 @@ def _column_data(
The first column of the table as a list.
"""
try:
return data if is_sentinel else data.iloc[:, 0].tolist()
return data if is_sentinel or data is None else data.iloc[:, 0].tolist()
except IndexError:
# if there is a static table with no columns, we will get an IndexError
raise IndexError("Cannot get column data from an empty table")


def use_column_data(table: Table, sentinel: Sentinel = None) -> ColumnData | Sentinel:
def use_column_data(
table: Table | None, sentinel: Sentinel = ()
) -> ColumnData | Sentinel | None:
"""
Return the first column of the table as a list. The table should already be filtered to only have a single column.
Args:
table: The table to extract the column from.
sentinel: The sentinel value to return if the table is ticking but empty. Defaults to None.
sentinel: The sentinel value to return if the table is ticking but empty. Defaults to ().
Returns:
The first column of the table as a list or the sentinel value.
Expand Down
12 changes: 8 additions & 4 deletions plugins/ui/src/deephaven/ui/hooks/use_row_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
from ..types import Sentinel, RowData


def _row_data(data: pd.DataFrame | Sentinel, is_sentinel: bool) -> RowData | Sentinel:
def _row_data(
data: pd.DataFrame | Sentinel | None, is_sentinel: bool
) -> RowData | Sentinel | None:
"""
Return the first row of the table as a dictionary.
Expand All @@ -20,19 +22,21 @@ def _row_data(data: pd.DataFrame | Sentinel, is_sentinel: bool) -> RowData | Sen
The first row of the table as a dictionary.
"""
try:
return data if is_sentinel else data.iloc[0].to_dict()
return data if is_sentinel or data is None else data.iloc[0].to_dict()
except IndexError:
# if there is a static table with no rows, we will get an IndexError
raise IndexError("Cannot get row data from an empty table")


def use_row_data(table: Table, sentinel: Sentinel = None) -> RowData | Sentinel:
def use_row_data(
table: Table | None, sentinel: Sentinel = ()
) -> RowData | Sentinel | None:
"""
Return the first row of the table as a dictionary. The table should already be filtered to only have a single row.
Args:
table: The table to extract the row from.
sentinel: The sentinel value to return if the table is ticking but empty. Defaults to None.
sentinel: The sentinel value to return if the table is ticking but empty. Defaults to ().
Returns:
The first row of the table as a dictionary or the sentinel value.
Expand Down
12 changes: 8 additions & 4 deletions plugins/ui/src/deephaven/ui/hooks/use_row_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
from ..types import Sentinel


def _row_list(data: pd.DataFrame | Sentinel, is_sentinel: bool) -> list[Any] | Sentinel:
def _row_list(
data: pd.DataFrame | Sentinel | None, is_sentinel: bool
) -> list[Any] | Sentinel | None:
"""
Return the first row of the table as a list.
Expand All @@ -21,19 +23,21 @@ def _row_list(data: pd.DataFrame | Sentinel, is_sentinel: bool) -> list[Any] | S
The first row of the table as a list.
"""
try:
return data if is_sentinel else data.iloc[0].values.tolist()
return data if is_sentinel or data is None else data.iloc[0].values.tolist()
except IndexError:
# if there is a static table with no rows, we will get an IndexError
raise IndexError("Cannot get row list from an empty table")


def use_row_list(table: Table, sentinel: Sentinel = None) -> list[Any] | Sentinel:
def use_row_list(
table: Table | None, sentinel: Sentinel = ()
) -> list[Any] | Sentinel | None:
"""
Return the first row of the table as a list. The table should already be filtered to only have a single row.
Args:
table: The table to extract the row from.
sentinel: The sentinel value to return if the table is ticking but empty. Defaults to None.
sentinel: The sentinel value to return if the table is ticking but empty. Defaults to ().
Returns:
The first row of the table as a list or the sentinel value.
Expand Down
42 changes: 27 additions & 15 deletions plugins/ui/src/deephaven/ui/hooks/use_table_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
from deephaven.server.executors import submit_task
from deephaven.update_graph import has_exclusive_lock

import deephaven.ui as ui
from .use_callback import use_callback
from .use_effect import use_effect
from .use_state import use_state
from .use_table_listener import use_table_listener

from ..types import Sentinel, TableData, TransformedData

Expand Down Expand Up @@ -49,9 +52,14 @@ def _on_update(
submit_task(executor_name, partial(_deferred_update, ctx, func))


def _get_data_values(table: Table, sentinel: Sentinel):
def _get_data_values(
table: Table | None, sentinel: Sentinel
) -> tuple[pd.DataFrame | None, bool]:
"""
Called to get the new data and is_sentinel values when the table updates.
None is returned if the table is None.
The sentinel value is returned if the table is empty and refreshing.
Otherwise, the table data is returned.
Args:
table: The table that updated.
Expand All @@ -60,6 +68,8 @@ def _get_data_values(table: Table, sentinel: Sentinel):
Returns:
The table data and whether the sentinel value was returned.
"""
if table is None:
return None, False
data = to_pandas(table)
if table.is_refreshing:
if data.empty:
Expand All @@ -71,7 +81,7 @@ def _get_data_values(table: Table, sentinel: Sentinel):


def _set_new_data(
table: Table,
table: Table | None,
sentinel: Sentinel,
set_data: Callable[[pd.DataFrame | Sentinel], None],
set_is_sentinel: Callable[[bool], None],
Expand Down Expand Up @@ -103,31 +113,33 @@ def _table_data(
Returns:
The table data.
"""
return data if is_sentinel else data.to_dict(orient="list")
return data if is_sentinel or data is None else data.to_dict(orient="list")


def use_table_data(
table: Table,
sentinel: Sentinel | None = None,
transform: Callable[[pd.DataFrame | Sentinel, bool], TransformedData | Sentinel]
table: Table | None,
sentinel: Sentinel = (),
transform: Callable[
[pd.DataFrame | Sentinel | None, bool], TransformedData | Sentinel
]
| None = None,
) -> TableData | Sentinel | TransformedData:
"""
Returns a dictionary with the contents of the table. Component will redraw if the table
changes, resulting in an updated frame.
Args:
table: The table to listen to.
sentinel: The sentinel value to return if the table is ticking but empty. Defaults to None.
table: The table to listen to. If None, None will be returned, not the sentinel value.
sentinel: The sentinel value to return if the table is ticking but empty. Defaults to an empty tuple.
transform: A function to transform the table data and is_sentinel values. Defaults to None, which will
return the data as TableData.
Returns:
The table data or the sentinel value.
"""
initial_data, initial_is_sentinel = _get_data_values(table, sentinel)
data, set_data = ui.use_state(initial_data)
is_sentinel, set_is_sentinel = ui.use_state(initial_is_sentinel)
data, set_data = use_state(initial_data)
is_sentinel, set_is_sentinel = use_state(initial_is_sentinel)

if not transform:
transform = _table_data
Expand All @@ -141,19 +153,19 @@ def use_table_data(
executor_name = "concurrent"

# memoize table_updated (and listener) so that they don't cause a start and stop of the listener
table_updated = ui.use_callback(
table_updated = use_callback(
lambda: _set_new_data(table, sentinel, set_data, set_is_sentinel),
[table, sentinel],
)

# call table_updated in the case of new table or sentinel
ui.use_effect(table_updated, [table, sentinel])
listener = ui.use_callback(
use_effect(table_updated, [table, sentinel])
listener = use_callback(
partial(_on_update, ctx, table_updated, executor_name),
[table_updated, executor_name, ctx],
)

# call table_updated every time the table updates
ui.use_table_listener(table, listener, [])
use_table_listener(table, listener, [])

return transform(data, is_sentinel)
4 changes: 2 additions & 2 deletions plugins/ui/src/deephaven/ui/hooks/use_table_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def wrap_listener(


def use_table_listener(
table: Table,
table: Table | None,
listener: Callable[[TableUpdate, bool], None] | TableListener,
dependencies: Dependencies,
description: str | None = None,
Expand Down Expand Up @@ -95,7 +95,7 @@ def start_listener() -> Callable[[], None]:
Returns:
A function that can be called to stop the listener by the use_effect hook.
"""
if not table.is_refreshing and not do_replay:
if table is None or (not table.is_refreshing and not do_replay):
return lambda: None

handle = listen(
Expand Down
Loading

0 comments on commit 910a57c

Please sign in to comment.