Skip to content

Commit 83c02b7

Browse files
authored
bug(data frame): Reduce usage of CellPatchProcessed in favor of CellPatch (#1526)
1 parent 73e60a2 commit 83c02b7

File tree

8 files changed

+102
-54
lines changed

8 files changed

+102
-54
lines changed

CHANGELOG.md

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

4848
* Fixed #1440: When a Shiny Express app with a `www/` subdirectory was deployed to shinyapps.io or a Connect server, it would not start correctly. (#1442)
4949

50+
* The return type for the data frame patch function now returns a list of `render.CellPatch` objects (which support `htmltools.TagNode` for the `value` attribute). These values will be set inside the data frame's `.data_view()` result. This also means that `.cell_patches()` will be a list of `render.CellPatch` objects. (#1526)
51+
5052
### Other changes
5153

5254
## [0.10.2] - 2024-05-28

shiny/playwright/controller/_controls.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717

1818
# Import `shiny`'s typing extentions.
1919
# Since this is a private file, tell pyright to ignore the import
20-
from ..._typing_extensions import TypeGuard, assert_type
21-
from ...types import MISSING, MISSING_TYPE
20+
from ..._typing_extensions import TypeGuard
21+
from ...types import MISSING, MISSING_TYPE, ListOrTuple
2222
from .._types import (
2323
AttrValue,
2424
ListPatternOrStr,
@@ -817,7 +817,7 @@ def __init__(
817817

818818
def set(
819819
self,
820-
selected: str | list[str],
820+
selected: str | ListOrTuple[str],
821821
*,
822822
timeout: Timeout = None,
823823
) -> None:
@@ -1558,8 +1558,11 @@ def expect_locator_contains_values_in_list(
15581558
The key. Defaults to `"value"`.
15591559
"""
15601560
# Make sure the locator contains all of `arr`
1561-
1562-
assert_type(arr, typing.List[str])
1561+
if not isinstance(arr, list):
1562+
raise TypeError(f"`{arr_name}` must be a list")
1563+
for item in arr:
1564+
if not isinstance(item, str):
1565+
raise TypeError(f"`{arr_name}` must be a list of strings")
15631566

15641567
# Make sure the locator has len(uniq_arr) input elements
15651568
_MultipleDomItems.assert_arr_is_unique(arr, f"`{arr_name}` must be unique")
@@ -1814,7 +1817,7 @@ def __init__(
18141817
def set(
18151818
self,
18161819
# TODO-future: Allow `selected` to be a single Pattern to perform matching against many items
1817-
selected: list[str],
1820+
selected: ListOrTuple[str],
18181821
*,
18191822
timeout: Timeout = None,
18201823
**kwargs: object,
@@ -1830,7 +1833,11 @@ def set(
18301833
The timeout for the action. Defaults to `None`.
18311834
"""
18321835
# Having an arr of size 0 is allowed. Will uncheck everything
1833-
assert_type(selected, typing.List[str])
1836+
if not isinstance(selected, list):
1837+
raise TypeError("`selected` must be a list or tuple")
1838+
for item in selected:
1839+
if not isinstance(item, str):
1840+
raise TypeError("`selected` must be a list of strings")
18341841

18351842
# Make sure the selected items exist
18361843
# Similar to `self.expect_choices(choices = selected)`, but with
@@ -1999,7 +2006,9 @@ def set(
19992006
timeout
20002007
The timeout for the action. Defaults to `None`.
20012008
"""
2002-
assert_type(selected, str)
2009+
if not isinstance(selected, str):
2010+
raise TypeError("`selected` must be a string")
2011+
20032012
# Only need to set.
20042013
# The Browser will _unset_ the previously selected radio button
20052014
self.loc_container.locator(
@@ -3938,8 +3947,10 @@ def expect_cell(
39383947
timeout
39393948
The maximum time to wait for the text to appear. Defaults to `None`.
39403949
"""
3941-
assert_type(row, int)
3942-
assert_type(col, int)
3950+
if not isinstance(row, int):
3951+
raise TypeError("`row` must be an integer")
3952+
if not isinstance(col, int):
3953+
raise TypeError("`col` must be an integer")
39433954
playwright_expect(
39443955
self.loc.locator(
39453956
f"xpath=./table/tbody/tr[{row}]/td[{col}] | ./table/tbody/tr[{row}]/th[{col}]"
@@ -3994,7 +4005,8 @@ def expect_column_text(
39944005
timeout
39954006
The maximum time to wait for the text to appear. Defaults to `None`.
39964007
"""
3997-
assert_type(col, int)
4008+
if not isinstance(col, int):
4009+
raise TypeError("`col` must be an integer")
39984010
playwright_expect(
39994011
self.loc.locator(f"xpath=./table/tbody/tr/td[{col}]")
40004012
).to_have_text(
@@ -5981,8 +5993,10 @@ def expect_cell(
59815993
timeout
59825994
The maximum time to wait for the expectation to pass. Defaults to `None`.
59835995
"""
5984-
assert_type(row, int)
5985-
assert_type(col, int)
5996+
if not isinstance(row, int):
5997+
raise TypeError("`row` must be an integer.")
5998+
if not isinstance(col, int):
5999+
raise TypeError("`col` must be an integer.")
59866000
self._cell_scroll_if_needed(row=row, col=col, timeout=timeout)
59876001
playwright_expect(self.cell_locator(row, col)).to_have_text(
59886002
value, timeout=timeout
@@ -6078,7 +6092,8 @@ def _expect_column_label(
60786092
timeout
60796093
The maximum time to wait for the expectation to pass. Defaults to `None`.
60806094
"""
6081-
assert_type(col, int)
6095+
if not isinstance(col, int):
6096+
raise TypeError("`col` must be an integer.")
60826097
# It's zero based, nth(0) selects the first element.
60836098
playwright_expect(self.loc_column_label.nth(col - 1)).to_have_text(
60846099
value,

shiny/render/_data_frame.py

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
SelectionModes,
2929
as_cell_selection,
3030
assert_patches_shape,
31-
wrap_shiny_html,
3231
)
32+
from ._data_frame_utils._html import maybe_as_cell_html
3333
from ._data_frame_utils._styles import as_browser_style_infos
3434
from ._data_frame_utils._tbl_data import (
3535
apply_frame_patches__typed,
@@ -195,7 +195,7 @@ class data_frame(Renderer[DataFrameResult[DataFrameLikeT]]):
195195
patches with updated values.
196196
"""
197197

198-
_cell_patch_map: reactive.Value[dict[tuple[int, int], CellPatchProcessed]]
198+
_cell_patch_map: reactive.Value[dict[tuple[int, int], CellPatch]]
199199
"""
200200
Reactive dictionary of patches to be applied to the data frame.
201201
@@ -204,7 +204,7 @@ class data_frame(Renderer[DataFrameResult[DataFrameLikeT]]):
204204
205205
The key is defined as `(row_index, column_index)`.
206206
"""
207-
cell_patches: reactive.Calc_[list[CellPatchProcessed]]
207+
cell_patches: reactive.Calc_[list[CellPatch]]
208208
"""
209209
Reactive value of the data frame's edits provided by the user.
210210
"""
@@ -349,7 +349,7 @@ def _init_reactives(self) -> None:
349349
self._cell_patch_map = reactive.Value({})
350350

351351
@reactive.calc
352-
def self_cell_patches() -> list[CellPatchProcessed]:
352+
def self_cell_patches() -> list[CellPatch]:
353353
return list(self._cell_patch_map().values())
354354

355355
self.cell_patches = self_cell_patches
@@ -537,8 +537,8 @@ async def patch_fn(
537537

538538
async def patches_fn(
539539
*,
540-
patches: list[CellPatch],
541-
):
540+
patches: tuple[CellPatch, ...],
541+
) -> ListOrTuple[CellPatch]:
542542
ret_patches: list[CellPatch] = []
543543
for patch in patches:
544544

@@ -583,7 +583,7 @@ def _set_patches_handler(self) -> str:
583583
return self._set_patches_handler_impl(self._patches_handler)
584584

585585
# Do not change this method name unless you update corresponding code in `/js/dataframe/`!!
586-
async def _patches_handler(self, patches: list[CellPatch]) -> Jsonifiable:
586+
async def _patches_handler(self, patches: tuple[CellPatch, ...]) -> Jsonifiable:
587587
"""
588588
Accepts edit patches requests from the client and returns the processed patches.
589589
@@ -602,7 +602,8 @@ async def _patches_handler(self, patches: list[CellPatch]) -> Jsonifiable:
602602

603603
with session_context(self._get_session()):
604604
# Call user's cell update method to retrieve formatted values
605-
patches = await self._patches_fn(patches=patches)
605+
val = await self._patches_fn(patches=patches)
606+
patches = tuple(val)
606607

607608
# Check to make sure `updated_infos` is a list of dicts with the correct keys
608609
bad_patches_format = not isinstance(patches, list)
@@ -625,29 +626,45 @@ async def _patches_handler(self, patches: list[CellPatch]) -> Jsonifiable:
625626
)
626627

627628
# Add (or overwrite) new cell patches by setting each patch into the cell patch map
628-
processed_patches: list[Jsonifiable] = []
629629
for patch in patches:
630-
processed_patch = self._set_cell_patch_map_value(
630+
self._set_cell_patch_map_value(
631631
value=patch["value"],
632632
row_index=patch["row_index"],
633633
column_index=patch["column_index"],
634634
)
635-
processed_patches.append(
636-
cell_patch_processed_to_jsonifiable(processed_patch)
637-
)
635+
636+
# Upgrade any HTML-like content to `CellHtml` json objects
637+
processed_patches: list[CellPatchProcessed] = [
638+
{
639+
"row_index": patch["row_index"],
640+
"column_index": patch["column_index"],
641+
# Only upgrade the value if it is necessary
642+
"value": maybe_as_cell_html(
643+
patch["value"],
644+
session=self._get_session(),
645+
),
646+
}
647+
for patch in patches
648+
]
649+
650+
# Prep the processed patches for sending to the client
651+
jsonifiable_patches: list[Jsonifiable] = [
652+
cell_patch_processed_to_jsonifiable(ret_processed_patch)
653+
for ret_processed_patch in processed_patches
654+
]
638655

639656
await self._attempt_update_cell_style()
640657

641658
# Return the processed patches to the client
642-
return processed_patches
659+
return jsonifiable_patches
643660

644661
def _set_cell_patch_map_value(
645662
self,
646663
value: CellValue,
647664
*,
648665
row_index: int,
649666
column_index: int,
650-
) -> CellPatchProcessed:
667+
):
651668
"""
652669
Set the value within the cell patch map.
653670
@@ -671,18 +688,16 @@ def _set_cell_patch_map_value(
671688
# TODO-barret-render.data_frame; The `value` should be coerced by pandas to the correct type
672689
# TODO-barret; See https://pandas.pydata.org/pandas-docs/stable/user_guide/basics.html#object-conversion
673690

674-
cell_patch_processed: CellPatchProcessed = {
691+
cell_patch: CellPatch = {
675692
"row_index": row_index,
676693
"column_index": column_index,
677-
"value": wrap_shiny_html(value, session=self._get_session()),
694+
"value": value,
678695
}
679696
# Use copy to set the new value
680697
cell_patch_map = self._cell_patch_map().copy()
681-
cell_patch_map[(row_index, column_index)] = cell_patch_processed
698+
cell_patch_map[(row_index, column_index)] = cell_patch
682699
self._cell_patch_map.set(cell_patch_map)
683700

684-
return cell_patch_processed
685-
686701
async def _attempt_update_cell_style(self) -> None:
687702
with session_context(self._get_session()):
688703

@@ -704,7 +719,7 @@ async def _attempt_update_cell_style(self) -> None:
704719
# TODO-barret-render.data_frame; Add `update_cell_value()` method
705720
# def _update_cell_value(
706721
# self, value: CellValue, *, row_index: int, column_index: int
707-
# ) -> CellPatchProcessed:
722+
# ) -> CellPatch:
708723
# """
709724
# Update the value of a cell in the data frame.
710725
#

shiny/render/_data_frame_utils/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
DataGrid,
44
DataTable,
55
)
6-
from ._html import wrap_shiny_html
6+
from ._html import maybe_as_cell_html
77
from ._patch import (
88
CellPatch,
99
CellValue,
@@ -27,7 +27,7 @@
2727
"AbstractTabularData",
2828
"DataGrid",
2929
"DataTable",
30-
"wrap_shiny_html",
30+
"maybe_as_cell_html",
3131
"CellHtml",
3232
"CellPatch",
3333
"CellPatchProcessed",

shiny/render/_data_frame_utils/_html.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,32 @@
1212
from ...session import Session
1313

1414

15+
def as_cell_html(x: TagNode, *, session: Session) -> CellHtml:
16+
return {"isShinyHtml": True, "obj": session._process_ui(x)}
17+
18+
19+
# def is_cell_html(val: Any) -> TypeGuard[CellHtml]:
20+
# return isinstance(val, dict) and (
21+
# val.get("isShinyHtml", False) # pyright: ignore[reportUnknownMemberType]
22+
# is True
23+
# )
24+
25+
26+
@overload
27+
def maybe_as_cell_html( # pyright: ignore[reportOverlappingOverload]
28+
x: TagNode, *, session: Session
29+
) -> CellHtml: ...
1530
@overload
16-
def wrap_shiny_html( # pyright: ignore[reportOverlappingOverload]
31+
def maybe_as_cell_html( # pyright: ignore[reportOverlappingOverload]
1732
x: TagNode, *, session: Session
1833
) -> CellHtml: ...
1934
@overload
20-
def wrap_shiny_html(x: Jsonifiable, *, session: Session) -> Jsonifiable: ...
21-
def wrap_shiny_html(
35+
def maybe_as_cell_html(x: Jsonifiable, *, session: Session) -> Jsonifiable: ...
36+
def maybe_as_cell_html(
2237
x: Jsonifiable | TagNode, *, session: Session
2338
) -> Jsonifiable | CellHtml:
2439
if is_shiny_html(x):
25-
return {"isShinyHtml": True, "obj": session._process_ui(x)}
40+
return as_cell_html(x, session=session)
2641
return cast(Jsonifiable, x)
2742

2843

shiny/render/_data_frame_utils/_pandas.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from htmltools import TagNode
77

88
from ...session._utils import require_active_session
9-
from ._html import col_contains_shiny_html, wrap_shiny_html
9+
from ._html import col_contains_shiny_html, maybe_as_cell_html
1010
from ._tbl_data import PdDataFrame, frame_column_names
1111
from ._types import FrameDtype, FrameJson
1212

@@ -55,7 +55,7 @@ def serialize_frame_pd(df: "pd.DataFrame") -> FrameJson:
5555
session = require_active_session(None)
5656

5757
def wrap_shiny_html_with_session(x: TagNode):
58-
return wrap_shiny_html(x, session=session)
58+
return maybe_as_cell_html(x, session=session)
5959

6060
for html_column in html_columns:
6161
# _upgrade_ all the HTML columns to `CellHtml` json objects

shiny/render/_data_frame_utils/_patch.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# TODO-barret-render.data_frame; Add examples of patch!
55
from typing import Protocol, Sequence
66

7+
from ...types import ListOrTuple
78
from ._types import CellPatch, CellValue
89

910

@@ -19,8 +20,8 @@ class PatchesFn(Protocol):
1920
async def __call__(
2021
self,
2122
*,
22-
patches: list[CellPatch],
23-
) -> list[CellPatch]: ...
23+
patches: tuple[CellPatch, ...],
24+
) -> ListOrTuple[CellPatch]: ...
2425

2526

2627
class PatchFnSync(Protocol):
@@ -35,8 +36,8 @@ class PatchesFnSync(Protocol):
3536
def __call__(
3637
self,
3738
*,
38-
patches: list[CellPatch],
39-
) -> list[CellPatch]: ...
39+
patches: tuple[CellPatch, ...],
40+
) -> ListOrTuple[CellPatch]: ...
4041

4142

4243
def assert_patches_shape(x: Sequence[CellPatch]) -> None:

0 commit comments

Comments
 (0)