Skip to content

Commit c12e649

Browse files
cpsievertschloerke
andauthored
feat(clientdata): id is now optional on clientdata.output_*() methods (#1978)
Co-authored-by: Barret Schloerke <[email protected]>
1 parent 5653bdb commit c12e649

File tree

4 files changed

+113
-14
lines changed

4 files changed

+113
-14
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515

1616
* `ui.update_*()` functions now accept `ui.TagChild` (i.e., HTML) as input to the `label` and `icon` arguments. (#2020)
1717

18+
* The `.output_*()` methods of the `ClientData` class (e.g., `session.clientdata.output_height()`) can now be called without an `id` when called inside a `@render` function. (#1978)
19+
1820
* `playwright.controller.InputActionButton` gains a `expect_icon()` method. As a result, the already existing `expect_label()` no longer includes the icon. (#2020)
1921

2022
### Improvements
@@ -25,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2527

2628
* Added `timeout_secs` parameter to `create_app_fixture` to allow testing apps with longer startup times. (#2033)
2729

30+
* Added module support for `session.clientdata` methods. This allows you to access client data values in Shiny modules without needing to namespace the keys explicitly. (#1978)
31+
2832
### Bug fixes
2933

3034
* Fixed an issue with `ui.Chat()` sometimes wanting to scroll a parent element. (#1996)
@@ -38,7 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3842

3943
## [1.4.0] - 2025-04-08
4044

41-
## New features
45+
### New features
4246

4347
* Added support for bookmarking Shiny applications. Bookmarking allows users to save the current state of an application and return to it later. This feature is available in both Shiny Core and Shiny Express. (#1870, #1915, #1919, #1920, #1922, #1934, #1938, #1945, #1955)
4448
* To enable bookmarking in Express mode, set `shiny.express.app_opts(bookmark_store=)` during the app's initial construction.

shiny/session/_session.py

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
AsyncIterable,
2323
Awaitable,
2424
Callable,
25+
Generator,
2526
Iterable,
2627
Literal,
2728
Optional,
@@ -540,6 +541,7 @@ def __init__(
540541

541542
self.user: str | None = None
542543
self.groups: list[str] | None = None
544+
543545
credentials_json: str = ""
544546
if "shiny-server-credentials" in self.http_conn.headers:
545547
credentials_json = self.http_conn.headers["shiny-server-credentials"]
@@ -1218,6 +1220,7 @@ def __init__(self, root_session: Session, ns: ResolvedId) -> None:
12181220
ns=ns,
12191221
outputs=root_session.output._outputs,
12201222
)
1223+
self.clientdata = ClientData(self)
12211224
self._outbound_message_queues = root_session._outbound_message_queues
12221225
self._downloads = root_session._downloads
12231226

@@ -1507,6 +1510,7 @@ class ClientData:
15071510

15081511
def __init__(self, session: Session) -> None:
15091512
self._session: Session = session
1513+
self._current_output_name: ResolvedId | None = None
15101514

15111515
def url_hash(self) -> str:
15121516
"""
@@ -1556,7 +1560,7 @@ def pixelratio(self) -> float:
15561560
"""
15571561
return cast(int, self._read_input("pixelratio"))
15581562

1559-
def output_height(self, id: str) -> float | None:
1563+
def output_height(self, id: Optional[Id] = None) -> float | None:
15601564
"""
15611565
Reactively read the height of an output.
15621566
@@ -1573,7 +1577,7 @@ def output_height(self, id: str) -> float | None:
15731577
"""
15741578
return cast(float, self._read_output(id, "height"))
15751579

1576-
def output_width(self, id: str) -> float | None:
1580+
def output_width(self, id: Optional[Id] = None) -> float | None:
15771581
"""
15781582
Reactively read the width of an output.
15791583
@@ -1590,7 +1594,7 @@ def output_width(self, id: str) -> float | None:
15901594
"""
15911595
return cast(float, self._read_output(id, "width"))
15921596

1593-
def output_hidden(self, id: str) -> bool | None:
1597+
def output_hidden(self, id: Optional[Id] = None) -> bool | None:
15941598
"""
15951599
Reactively read whether an output is hidden.
15961600
@@ -1606,7 +1610,7 @@ def output_hidden(self, id: str) -> bool | None:
16061610
"""
16071611
return cast(bool, self._read_output(id, "hidden"))
16081612

1609-
def output_bg_color(self, id: str) -> str | None:
1613+
def output_bg_color(self, id: Optional[Id] = None) -> str | None:
16101614
"""
16111615
Reactively read the background color of an output.
16121616
@@ -1623,7 +1627,7 @@ def output_bg_color(self, id: str) -> str | None:
16231627
"""
16241628
return cast(str, self._read_output(id, "bg"))
16251629

1626-
def output_fg_color(self, id: str) -> str | None:
1630+
def output_fg_color(self, id: Optional[Id] = None) -> str | None:
16271631
"""
16281632
Reactively read the foreground color of an output.
16291633
@@ -1640,7 +1644,7 @@ def output_fg_color(self, id: str) -> str | None:
16401644
"""
16411645
return cast(str, self._read_output(id, "fg"))
16421646

1643-
def output_accent_color(self, id: str) -> str | None:
1647+
def output_accent_color(self, id: Optional[Id] = None) -> str | None:
16441648
"""
16451649
Reactively read the accent color of an output.
16461650
@@ -1657,7 +1661,7 @@ def output_accent_color(self, id: str) -> str | None:
16571661
"""
16581662
return cast(str, self._read_output(id, "accent"))
16591663

1660-
def output_font(self, id: str) -> str | None:
1664+
def output_font(self, id: Optional[Id] = None) -> str | None:
16611665
"""
16621666
Reactively read the font(s) of an output.
16631667
@@ -1678,22 +1682,51 @@ def _read_input(self, key: str) -> str:
16781682
self._check_current_context(key)
16791683

16801684
id = ResolvedId(f".clientdata_{key}")
1681-
if id not in self._session.input:
1685+
if id not in self._session.root_scope().input:
16821686
raise ValueError(
16831687
f"ClientData value '{key}' not found. Please report this issue."
16841688
)
16851689

1686-
return self._session.input[id]()
1690+
return self._session.root_scope().input[id]()
16871691

1688-
def _read_output(self, id: str, key: str) -> str | None:
1692+
def _read_output(self, id: Id | None, key: str) -> str | None:
16891693
self._check_current_context(f"output_{key}")
16901694

1695+
# No `id` provided support
1696+
if id is None and self._current_output_name is not None:
1697+
id = self._current_output_name
1698+
1699+
if id is None:
1700+
raise ValueError(
1701+
"session.clientdata.output_*() requires an id when not called within "
1702+
"an output renderer."
1703+
)
1704+
1705+
# Module support
1706+
if not isinstance(id, ResolvedId):
1707+
id = self._session.ns(id)
1708+
16911709
input_id = ResolvedId(f".clientdata_output_{id}_{key}")
1692-
if input_id in self._session.input:
1693-
return self._session.input[input_id]()
1710+
if input_id in self._session.root_scope().input:
1711+
return self._session.root_scope().input[input_id]()
16941712
else:
16951713
return None
16961714

1715+
@contextlib.contextmanager
1716+
def _output_name_ctx(self, output_name: ResolvedId) -> Generator[None, None, None]:
1717+
"""
1718+
Context manager to temporarily set the output name.
1719+
1720+
This is used to allow `session.clientdata.output_*()` methods to access the
1721+
current output name without needing to pass it explicitly.
1722+
"""
1723+
old_output_name = self._current_output_name
1724+
try:
1725+
self._current_output_name = output_name
1726+
yield
1727+
finally:
1728+
self._current_output_name = old_output_name
1729+
16971730
@staticmethod
16981731
def _check_current_context(key: str) -> None:
16991732
try:
@@ -1798,8 +1831,12 @@ async def output_obs():
17981831
)
17991832

18001833
try:
1801-
value = await renderer.render()
1834+
with session.clientdata._output_name_ctx(output_name):
1835+
# Call the app's renderer function
1836+
value = await renderer.render()
1837+
18021838
session._outbound_message_queues.set_value(output_name, value)
1839+
18031840
except SilentOperationInProgressException:
18041841
session._send_progress(
18051842
"binding", {"id": output_name, "persistent": True}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from shiny import App, Inputs, Outputs, Session, module, render, ui
2+
3+
4+
@module.ui
5+
def mod_ui():
6+
return ui.output_text("info2").add_class("shiny-report-theme")
7+
8+
9+
@module.server
10+
def mod_server(input: Inputs, output: Outputs, session: Session):
11+
@render.text
12+
def info2():
13+
bg_color = session.clientdata.output_bg_color()
14+
return f"BG color: {bg_color}"
15+
16+
17+
app_ui = ui.page_fluid(
18+
ui.input_dark_mode(mode="light", id="dark_mode"),
19+
ui.output_text("info").add_class("shiny-report-theme"),
20+
mod_ui("mod1"),
21+
)
22+
23+
24+
def server(input: Inputs, output: Outputs, session: Session):
25+
mod_server("mod1")
26+
27+
@render.text
28+
def info():
29+
bg_color = session.clientdata.output_bg_color()
30+
return f"BG color: {bg_color}"
31+
32+
33+
app = App(app_ui, server)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from playwright.sync_api import Page
2+
3+
from shiny.playwright import controller
4+
from shiny.run import ShinyAppProc
5+
6+
7+
def test_current_output_info(page: Page, local_app: ShinyAppProc) -> None:
8+
9+
page.goto(local_app.url)
10+
11+
# Check that we can get background color from clientdata
12+
info = controller.OutputText(page, "info")
13+
mod_info2 = controller.OutputText(page, "mod1-info2")
14+
info.expect_value("BG color: rgb(255, 255, 255)")
15+
mod_info2.expect_value("BG color: rgb(255, 255, 255)")
16+
17+
# Click the dark mode button to change the background color
18+
dark_mode = controller.InputDarkMode(page, "dark_mode")
19+
dark_mode.expect_mode("light")
20+
dark_mode.click()
21+
dark_mode.expect_mode("dark")
22+
23+
# Check that the background color has changed
24+
info.expect_value("BG color: rgb(29, 31, 33)")
25+
mod_info2.expect_value("BG color: rgb(29, 31, 33)")

0 commit comments

Comments
 (0)