Skip to content

Commit 63333bb

Browse files
committed
Test for recursive module bookmarking
1 parent 5192884 commit 63333bb

File tree

4 files changed

+181
-55
lines changed

4 files changed

+181
-55
lines changed

shiny/bookmark/_bookmark.py

Lines changed: 27 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575

7676
class Bookmark(ABC):
7777

78-
_proxy_exclude_fns: list[Callable[[], list[str]]]
78+
_on_get_exclude: list[Callable[[], list[str]]]
7979
"""Callbacks that BookmarkProxy classes utilize to help determine the list of inputs to exclude from bookmarking."""
8080
exclude: list[str]
8181
"""A list of scoped Input names to exclude from bookmarking."""
@@ -85,6 +85,21 @@ class Bookmark(ABC):
8585
_on_restore_callbacks: AsyncCallbacks
8686
_on_restored_callbacks: AsyncCallbacks
8787

88+
async def __call__(self) -> None:
89+
await self.do_bookmark()
90+
91+
def __init__(self):
92+
93+
super().__init__()
94+
95+
self._on_get_exclude = []
96+
self.exclude = []
97+
98+
self._on_bookmark_callbacks = AsyncCallbacks()
99+
self._on_bookmarked_callbacks = AsyncCallbacks()
100+
self._on_restore_callbacks = AsyncCallbacks()
101+
self._on_restored_callbacks = AsyncCallbacks()
102+
88103
# Making this a read only property as app authors will not be able to change how the session is restored as the server function will run after the session has been restored.
89104
@property
90105
@abstractmethod
@@ -116,20 +131,16 @@ def _set_restore_context(self, restore_context: RestoreContext):
116131
"""
117132
...
118133

119-
async def __call__(self) -> None:
120-
await self.do_bookmark()
121-
122-
def __init__(self):
123-
124-
super().__init__()
125-
126-
self._proxy_exclude_fns = []
127-
self.exclude = []
134+
def _get_bookmark_exclude(self) -> list[str]:
135+
"""
136+
Get the list of inputs excluded from being bookmarked.
137+
"""
128138

129-
self._on_bookmark_callbacks = AsyncCallbacks()
130-
self._on_bookmarked_callbacks = AsyncCallbacks()
131-
self._on_restore_callbacks = AsyncCallbacks()
132-
self._on_restored_callbacks = AsyncCallbacks()
139+
scoped_excludes: list[str] = []
140+
for proxy_exclude_fn in self._on_get_exclude:
141+
scoped_excludes.extend(proxy_exclude_fn())
142+
# Remove duplicates
143+
return list(set([*self.exclude, *scoped_excludes]))
133144

134145
# # TODO: Barret - Implement this?!?
135146
# @abstractmethod
@@ -154,13 +165,6 @@ def _create_effects(self) -> None:
154165
"""
155166
...
156167

157-
@abstractmethod
158-
def _get_bookmark_exclude(self) -> list[str]:
159-
"""
160-
Retrieve the list of inputs excluded from being bookmarked.
161-
"""
162-
...
163-
164168
@abstractmethod
165169
def on_bookmark(
166170
self,
@@ -396,17 +400,6 @@ async def invoke_on_restored_callbacks():
396400

397401
return
398402

399-
def _get_bookmark_exclude(self) -> list[str]:
400-
"""
401-
Get the list of inputs excluded from being bookmarked.
402-
"""
403-
404-
scoped_excludes: list[str] = []
405-
for proxy_exclude_fn in self._proxy_exclude_fns:
406-
scoped_excludes.extend(proxy_exclude_fn())
407-
# Remove duplicates
408-
return list(set([*self.exclude, *scoped_excludes]))
409-
410403
def on_bookmark(
411404
self,
412405
callback: (
@@ -548,9 +541,8 @@ def __init__(self, session_proxy: SessionProxy):
548541
self._ns = session_proxy.ns
549542
self._session = session_proxy
550543

551-
# TODO: Barret - This isn't getting to the root
552544
# Maybe `._get_bookmark_exclude()` should be used instead of`proxy_exclude_fns`?
553-
self._session._parent.bookmark._proxy_exclude_fns.append(
545+
self._session._parent.bookmark._on_get_exclude.append(
554546
lambda: [str(self._ns(name)) for name in self.exclude]
555547
)
556548

@@ -667,12 +659,6 @@ def on_bookmarked(
667659
) -> CancelCallback:
668660
return self._on_bookmarked_callbacks.register(wrap_async(callback))
669661

670-
def _get_bookmark_exclude(self) -> NoReturn:
671-
672-
raise NotImplementedError(
673-
"Please call `._get_bookmark_exclude()` from the root session only."
674-
)
675-
676662
async def update_query_string(
677663
self, query_string: str, mode: Literal["replace", "push"] = "replace"
678664
) -> None:
@@ -706,7 +692,7 @@ def __init__(self, session: ExpressStubSession) -> None:
706692
from ..express._stub_session import ExpressStubSession
707693

708694
assert isinstance(session, ExpressStubSession)
709-
self._session = session
695+
# self._session = session
710696

711697
@property
712698
def store(self) -> BookmarkStore:
@@ -724,11 +710,6 @@ def _create_effects(self) -> NoReturn:
724710
"Please call `._create_effects()` only from a real session object"
725711
)
726712

727-
def _get_bookmark_exclude(self) -> NoReturn:
728-
raise NotImplementedError(
729-
"Please call `._get_bookmark_exclude()` only from a real session object"
730-
)
731-
732713
def on_bookmark(
733714
self,
734715
callback: (
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import os
2+
from typing import Literal
3+
4+
from starlette.requests import Request
5+
6+
from shiny import App, Inputs, Outputs, Session, module, reactive, render, ui
7+
from shiny.bookmark import BookmarkState
8+
from shiny.bookmark._restore_state import RestoreState
9+
10+
11+
@module.ui
12+
def mod_btn(idx: int = 1):
13+
return ui.TagList(
14+
ui.h3(f"Module {idx}"),
15+
ui.layout_column_wrap(
16+
ui.TagList(
17+
ui.input_radio_buttons(
18+
"btn1", "Button Input", choices=["a", "b", "c"], selected="a"
19+
),
20+
ui.input_radio_buttons(
21+
"btn2",
22+
"Button Value",
23+
choices=["a", "b", "c"],
24+
selected="a",
25+
),
26+
),
27+
ui.output_ui("ui_html"),
28+
ui.output_code("value"),
29+
width="200px",
30+
),
31+
ui.hr(),
32+
mod_btn(f"sub{idx}", idx - 1) if idx > 0 else None,
33+
)
34+
35+
36+
@module.server
37+
def btn_server(input: Inputs, output: Outputs, session: Session, idx: int = 1):
38+
39+
@render.ui
40+
def ui_html():
41+
return ui.TagList(
42+
ui.input_radio_buttons(
43+
"dyn1", "Dynamic Input", choices=["a", "b", "c"], selected="a"
44+
),
45+
ui.input_radio_buttons(
46+
"dyn2", "Dynamic Value", choices=["a", "b", "c"], selected="a"
47+
),
48+
)
49+
50+
@render.code
51+
def value():
52+
value_arr = [input.btn1(), input.btn2(), input.dyn1(), input.dyn2()]
53+
return f"{value_arr}"
54+
55+
@reactive.effect
56+
@reactive.event(input.btn1, input.btn2, input.dyn1, input.dyn2, ignore_init=True)
57+
async def _():
58+
# print("app-Bookmarking!")
59+
await session.bookmark()
60+
61+
session.bookmark.exclude.append("btn2")
62+
session.bookmark.exclude.append("dyn2")
63+
64+
@session.bookmark.on_bookmark
65+
def _(state: BookmarkState) -> None:
66+
state.values["btn2"] = input.btn2()
67+
state.values["dyn2"] = input.dyn2()
68+
69+
@session.bookmark.on_restore
70+
def _(restore_state: RestoreState) -> None:
71+
# print("app-Restore state:", restore_state.values)
72+
73+
if "btn2" in restore_state.values:
74+
75+
ui.update_radio_buttons("btn2", selected=restore_state.values["btn2"])
76+
77+
if "dyn2" in restore_state.values:
78+
79+
ui.update_radio_buttons("dyn2", selected=restore_state.values["dyn2"])
80+
81+
if idx > 0:
82+
btn_server(f"sub{idx}", idx - 1)
83+
84+
85+
k = 2
86+
87+
88+
def app_ui(request: Request) -> ui.Tag:
89+
# print("app-Making UI")
90+
return ui.page_fixed(
91+
ui.output_code("bookmark_store"),
92+
"Click Buttons to update bookmark",
93+
mod_btn(f"mod{k-1}", k - 1),
94+
)
95+
96+
97+
# Needs access to the restore context to the dynamic UI
98+
def server(input: Inputs, output: Outputs, session: Session):
99+
100+
btn_server(f"mod{k-1}", k - 1)
101+
102+
@render.code
103+
def bookmark_store():
104+
return f"{session.bookmark.store}"
105+
106+
@session.bookmark.on_bookmark
107+
async def on_bookmark(state: BookmarkState) -> None:
108+
print(
109+
"app-On Bookmark",
110+
"\nInputs: ",
111+
await state.input._serialize(exclude=state.exclude, state_dir=None),
112+
"\nValues: ",
113+
state.values,
114+
"\n\n",
115+
)
116+
# session.bookmark.update_query_string()
117+
118+
pass
119+
120+
session.bookmark.on_bookmarked(session.bookmark.update_query_string)
121+
# session.bookmark.on_bookmarked(session.bookmark.show_modal)
122+
123+
124+
SHINY_BOOKMARK_STORE: Literal["url", "server"] = os.getenv(
125+
"SHINY_BOOKMARK_STORE", "url"
126+
) # pyright: ignore[reportAssignmentType]
127+
if SHINY_BOOKMARK_STORE not in ["url", "server"]:
128+
raise ValueError("SHINY_BOOKMARK_STORE must be either 'url' or 'server'")
129+
app = App(app_ui, server, bookmark_store=SHINY_BOOKMARK_STORE, debug=False)

tests/playwright/shiny/bookmark/modules/test_bookmark_modules.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,25 @@
88
from shiny.run import ShinyAppProc, run_shiny_app
99

1010

11-
@pytest.mark.parametrize("app_name", ["app-express.py", "app.py"])
11+
@pytest.mark.parametrize(
12+
"app_name,mod0_key,mod1_key",
13+
[
14+
# Express mode
15+
("app-express.py", "mod0", "mod1"),
16+
# Core mode
17+
("app-core.py", "mod0", "mod1"),
18+
# Recursive modules within core mode
19+
("app-core-recursive.py", "mod1-sub1", "mod1"),
20+
],
21+
)
1222
@pytest.mark.parametrize("bookmark_store", ["url", "server"])
13-
def test_bookmark_modules(page: Page, bookmark_store: str, app_name: str):
23+
def test_bookmark_modules(
24+
page: Page,
25+
bookmark_store: str,
26+
app_name: str,
27+
mod0_key: str,
28+
mod1_key: str,
29+
) -> None:
1430

1531
# Set environment variable before the app starts
1632
os.environ["SHINY_BOOKMARK_STORE"] = bookmark_store
@@ -41,18 +57,18 @@ def set_mod(mod_key: str, values: list[str]):
4157
InputRadioButtons(page, f"{mod_key}-dyn1").set(values[2])
4258
InputRadioButtons(page, f"{mod_key}-dyn2").set(values[3])
4359

44-
expect_mod("mod0", ["a", "a", "a", "a"])
45-
expect_mod("mod1", ["a", "a", "a", "a"])
60+
expect_mod(mod0_key, ["a", "a", "a", "a"])
61+
expect_mod(mod1_key, ["a", "a", "a", "a"])
4662

47-
set_mod("mod0", ["b", "b", "c", "c"])
63+
set_mod(mod0_key, ["b", "b", "c", "c"])
4864

49-
expect_mod("mod0", ["b", "b", "c", "c"])
50-
expect_mod("mod1", ["a", "a", "a", "a"])
65+
expect_mod(mod0_key, ["b", "b", "c", "c"])
66+
expect_mod(mod1_key, ["a", "a", "a", "a"])
5167

5268
page.reload()
5369

54-
expect_mod("mod0", ["b", "b", "c", "c"])
55-
expect_mod("mod1", ["a", "a", "a", "a"])
70+
expect_mod(mod0_key, ["b", "b", "c", "c"])
71+
expect_mod(mod1_key, ["a", "a", "a", "a"])
5672

5773
if bookmark_store == "url":
5874
assert "_inputs_" in page.url

0 commit comments

Comments
 (0)