Skip to content

Commit fab4d8c

Browse files
committed
Merge branch 'render_df_bugs' into data_view_meta
* render_df_bugs: Update _renderer.py Restore `input.<ID>_selected_rows()`. Add tests Make sure the index being subsetted is less than the row length, not contained in the index Update CHANGELOG.md Only subset the selected data if the index row exists!
2 parents d6c0d39 + e023993 commit fab4d8c

File tree

10 files changed

+177
-16
lines changed

10 files changed

+177
-16
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2525

2626
* Fixed an issue that prevented Shiny from serving the `font.css` file referenced in Shiny's Bootstrap CSS file. (#1342)
2727

28+
* Removed temporary state where a data frame renderer would try to subset to selected rows that did not exist. (#1351)
29+
30+
* Restored `@render.data_frame`'s (prematurely removed) input value `input.<ID>_selected_rows()`. This value is to be considered deprecated. Please use `<ID>.input_cell_selection()["rows"]` moving forward. (#1345)
31+
2832
### Other changes
2933

3034
* `Session` is now an abstract base class, and `AppSession` is a concrete subclass of it. Also, `ExpressMockSession` has been renamed `ExpressStubSession` and is a concrete subclass of `Session`. (#1331)

js/data-frame/index.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,18 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
362362
columnFilters,
363363
]);
364364

365+
// Restored for legacy purposes. Only send selected rows to Shiny when row selection is performed.
366+
useEffect(() => {
367+
if (!id) return;
368+
let shinyValue: number[] | null = null;
369+
if (rowSelectionModes.row !== SelectionModes._rowEnum.NONE) {
370+
const rowSelectionKeys = rowSelection.keys().toList();
371+
const rowsById = table.getSortedRowModel().rowsById;
372+
shinyValue = rowSelectionKeys.map((key) => rowsById[key].index).sort();
373+
}
374+
Shiny.setInputValue!(`${id}_selected_rows`, shinyValue);
375+
}, [id, rowSelection, rowSelectionModes, table]);
376+
365377
// ### End row selection ############################################################
366378

367379
// ### Editable cells ###############################################################

shiny/render/_data_frame.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -542,10 +542,14 @@ def _subset_data_view(selected: bool) -> pd.DataFrame:
542542
if cell_selection is not None and cell_selection["type"] == "row":
543543
# Use a `set` for faster lookups
544544
selected_row_set = set(cell_selection["rows"])
545+
nrow = data.shape[0]
545546

546-
# Subset the data view indices to only include the selected rows
547+
# Subset the data view indices to only include the selected rows that are in the data
547548
data_view_rows = [
548-
row for row in data_view_rows if row in selected_row_set
549+
row
550+
for row in data_view_rows
551+
# Make sure the row is not larger than the number of rows
552+
if row in selected_row_set and row < nrow
549553
]
550554

551555
return data.iloc[data_view_rows]

shiny/render/renderer/_renderer.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,16 +88,15 @@ class Renderer(Generic[IT]):
8888
used!)
8989
9090
There are two methods that must be implemented by the subclasses:
91-
`.auto_output_ui(self, id: str)` and either `.transform(self, value: IT)` or
92-
`.render(self)`.
91+
`.auto_output_ui(self)` and either `.transform(self, value: IT)` or `.render(self)`.
9392
9493
* In Express mode, the output renderer will automatically render its UI via
95-
`.auto_output_ui(self, id: str)`. This helper method allows App authors to skip
96-
adding a `ui.output_*` function to their UI, making Express mode even more
97-
concise. If more control is needed over the UI, `@ui.hold` can be used to suppress
98-
the auto rendering of the UI. When using `@ui.hold` on a renderer, the renderer's
99-
UI will need to be added to the app to connect the rendered output to Shiny's
100-
reactive graph.
94+
`.auto_output_ui(self)`. This helper method allows App authors to skip adding a
95+
`ui.output_*` function to their UI, making Express mode even more concise. If more
96+
control is needed over the UI, `@ui.hold` can be used to suppress the auto
97+
rendering of the UI. When using `@ui.hold` on a renderer, the renderer's UI will
98+
need to be added to the app to connect the rendered output to Shiny's reactive
99+
graph.
101100
* The `render` method is responsible for executing the value function and performing
102101
any transformations for the output value to be JSON-serializable (`None` is a
103102
valid value!). To avoid the boilerplate of resolving the value function and

shiny/www/shared/py-shiny/data-frame/data-frame.js

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

shiny/www/shared/py-shiny/data-frame/data-frame.js.map

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import pandas as pd
2+
3+
from shiny import App, Inputs, reactive, render, req, ui
4+
5+
app_ui = ui.page_fluid(
6+
ui.output_data_frame("df1"),
7+
ui.output_text_verbatim("selected_rows", placeholder=True),
8+
ui.output_text_verbatim("cell_selection", placeholder=True),
9+
)
10+
11+
12+
def server(input: Inputs):
13+
df = reactive.Value(pd.DataFrame([[1, 2], [3, 4], [5, 6]], columns=["A", "B"]))
14+
15+
@render.data_frame
16+
def df1():
17+
return render.DataGrid(df(), selection_mode="rows")
18+
19+
@render.text
20+
def selected_rows():
21+
return f"Input selected rows: {input.df1_selected_rows()}"
22+
23+
@render.text
24+
def cell_selection():
25+
cell_selection = df1.input_cell_selection()
26+
if cell_selection is None:
27+
req(cell_selection)
28+
raise ValueError("Cell selection is None")
29+
if cell_selection["type"] != "row":
30+
raise ValueError(
31+
f"Cell selection type is not 'row': {cell_selection['type']}"
32+
)
33+
rows = cell_selection["rows"]
34+
return f"Cell selection rows: {rows}"
35+
36+
37+
app = App(app_ui, server)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from __future__ import annotations
2+
3+
from conftest import ShinyAppProc
4+
from controls import OutputDataFrame, OutputTextVerbatim
5+
from playwright.sync_api import Page
6+
7+
8+
def test_row_selection(page: Page, local_app: ShinyAppProc) -> None:
9+
page.goto(local_app.url)
10+
11+
df = OutputDataFrame(page, "df1")
12+
selected_rows = OutputTextVerbatim(page, "selected_rows")
13+
cell_selection = OutputTextVerbatim(page, "cell_selection")
14+
15+
df.expect_n_row(3)
16+
selected_rows.expect_value("Input selected rows: ()")
17+
cell_selection.expect_value("Cell selection rows: ()")
18+
19+
df.select_rows([0, 2])
20+
21+
selected_rows.expect_value("Input selected rows: (0, 2)")
22+
cell_selection.expect_value("Cell selection rows: (0, 2)")
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import pandas as pd
2+
3+
from shiny import App, Inputs, reactive, render, ui
4+
5+
app_ui = ui.page_fluid(
6+
ui.markdown(
7+
"""
8+
## Description
9+
10+
When you add a row, click on it and click the clear button you get:
11+
12+
13+
"""
14+
),
15+
ui.input_action_button("add_row", "Add row"),
16+
ui.input_action_button("clear_table", "Clear table"),
17+
ui.output_text_verbatim("number_of_selected_rows"),
18+
ui.output_data_frame("df1"),
19+
)
20+
21+
22+
def server(input: Inputs):
23+
df = reactive.Value(pd.DataFrame(columns=["A", "B"]))
24+
25+
@render.data_frame
26+
def df1():
27+
return render.DataGrid(df(), selection_mode="rows")
28+
29+
@reactive.effect
30+
@reactive.event(input.add_row)
31+
def _():
32+
old_df = df()
33+
new_df = pd.concat( # pyright: ignore[reportUnknownMemberType]
34+
[old_df, pd.DataFrame([[1, 2]], columns=["A", "B"])]
35+
)
36+
df.set(new_df)
37+
38+
@render.text
39+
def number_of_selected_rows():
40+
df_selected = df1.data_view(selected=True)
41+
return f"Selected rows: {len(df_selected)}"
42+
43+
@reactive.effect
44+
@reactive.event(input.clear_table)
45+
def _():
46+
df.set(pd.DataFrame(columns=["A", "B"]))
47+
48+
49+
app = App(app_ui, server)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from __future__ import annotations
2+
3+
from conftest import ShinyAppProc
4+
from controls import InputActionButton, OutputDataFrame, OutputTextVerbatim
5+
from playwright.sync_api import Page
6+
7+
8+
def test_row_selection(page: Page, local_app: ShinyAppProc) -> None:
9+
page.goto(local_app.url)
10+
11+
df = OutputDataFrame(page, "df1")
12+
add_row = InputActionButton(page, "add_row")
13+
clear_table = InputActionButton(page, "clear_table")
14+
selected_rows = OutputTextVerbatim(page, "number_of_selected_rows")
15+
16+
df.expect_n_row(0)
17+
selected_rows.expect_value("Selected rows: 0")
18+
19+
add_row.click()
20+
21+
df.expect_n_row(1)
22+
selected_rows.expect_value("Selected rows: 0")
23+
24+
df.cell_locator(0, 0).click()
25+
df.select_rows([0])
26+
27+
df.expect_n_row(1)
28+
selected_rows.expect_value("Selected rows: 1")
29+
30+
clear_table.click()
31+
selected_rows.expect_value("Selected rows: 0")
32+
33+
bad_error_lines = [line for line in local_app.stderr._lines if "INFO:" not in line]
34+
assert len(bad_error_lines) == 0, bad_error_lines

0 commit comments

Comments
 (0)