Skip to content

Commit fe4c0e8

Browse files
committed
docs
1 parent 8fbd8f2 commit fe4c0e8

File tree

8 files changed

+180
-11
lines changed

8 files changed

+180
-11
lines changed

docs/_quartodoc-core.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ quartodoc:
113113
desc: "Decorators to set save and restore directories."
114114
flatten: true
115115
contents:
116-
- bookmark.set_global_save_dir
117-
- bookmark.set_global_restore_dir
116+
- bookmark.set_global_save_dir_fn
117+
- bookmark.set_global_restore_dir_fn
118118
- title: Chat interface
119119
desc: Build a chatbot interface
120120
contents:
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
from starlette.requests import Request
2+
3+
from shiny import App, Inputs, Outputs, Session, reactive, render, ui
4+
from shiny.bookmark import BookmarkState
5+
6+
7+
# App UI **must** be a function to ensure that each user restores their own UI values.
8+
def app_ui(request: Request):
9+
return ui.page_fluid(
10+
ui.markdown(
11+
"Directions: "
12+
"\n1. Change the radio buttons below"
13+
"\n2. Refresh your browser."
14+
"\n3. Only the first radio button will be restored."
15+
"\n4. Check the console messages for bookmarking events."
16+
),
17+
ui.hr(),
18+
ui.input_radio_buttons(
19+
"letter",
20+
"Choose a letter (Store in Bookmark 'input')",
21+
choices=["A", "B", "C"],
22+
),
23+
ui.input_radio_buttons(
24+
"letter_values",
25+
"Choose a letter (Stored in Bookmark 'values' as lowercase)",
26+
choices=["A", "B", "C"],
27+
),
28+
"Selection:",
29+
ui.output_code("letters"),
30+
)
31+
32+
33+
def server(input: Inputs, output: Outputs, session: Session):
34+
35+
# Exclude `"letter_values"` from being saved in the bookmark as we'll store it manually for example's sake
36+
# Append or adjust this list as needed.
37+
session.bookmark.exclude.append("letter_values")
38+
39+
lowercase_letter = reactive.value()
40+
41+
@reactive.effect
42+
@reactive.event(input.letter_values)
43+
async def _():
44+
lowercase_letter.set(input.letter_values().lower())
45+
46+
@render.code
47+
def letters():
48+
return str(
49+
[
50+
input.letter(),
51+
lowercase_letter(),
52+
]
53+
)
54+
55+
# When the user interacts with the input, we will bookmark the state.
56+
@reactive.effect
57+
@reactive.event(input.letter, lowercase_letter, ignore_init=True)
58+
async def _():
59+
await session.bookmark()
60+
61+
# Before saving state, we can adjust the bookmark state values object
62+
@session.bookmark.on_bookmark
63+
async def _(state: BookmarkState):
64+
print("Bookmark state:", state.input, state.values, state.dir)
65+
with reactive.isolate():
66+
state.values["lowercase"] = lowercase_letter()
67+
68+
# After saving state, we will update the query string with the bookmark URL.
69+
@session.bookmark.on_bookmarked
70+
async def _(url: str):
71+
print("Bookmarked url:", url)
72+
await session.bookmark.update_query_string(url)
73+
74+
@session.bookmark.on_restore
75+
def _(state: BookmarkState):
76+
print("Restore state:", state.input, state.values, state.dir)
77+
78+
# Update the radio button selection based on the restored state.
79+
if "lowercase" in state.values:
80+
uppercase = state.values["lowercase"].upper()
81+
# This may produce a small blip in the UI as the original value was restored on the client's HTML request, _then_ a message is received by the client to update the value.
82+
ui.update_radio_buttons("letter_values", selected=uppercase)
83+
84+
@session.bookmark.on_restored
85+
def _(state: BookmarkState):
86+
# For rare cases, you can update the UI after the session has been fully restored.
87+
print("Restored state:", state.input, state.values, state.dir)
88+
89+
90+
# Make sure to set the bookmark_store to `"url"` (or `"server"`)
91+
# to store the bookmark information/key in the URL query string.
92+
app = App(app_ui, server, bookmark_store="url")

shiny/bookmark/_bookmark.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from pathlib import Path
66
from typing import TYPE_CHECKING, Awaitable, Callable, Literal
77

8+
from .._docstring import add_example
89
from .._utils import AsyncCallbacks, CancelCallback, wrap_async
910
from ._button import BOOKMARK_ID
1011
from ._restore_state import RestoreState
@@ -85,6 +86,7 @@ class Bookmark(ABC):
8586
_on_restore_callbacks: AsyncCallbacks
8687
_on_restored_callbacks: AsyncCallbacks
8788

89+
@add_example("input_bookmark_button")
8890
async def __call__(self) -> None:
8991
await self.do_bookmark()
9092

@@ -136,6 +138,7 @@ def _restore_context(self) -> RestoreContext | None:
136138

137139
# await session.insert_ui(modal_with_url(url))
138140

141+
@add_example("bookmark_callbacks")
139142
def on_bookmark(
140143
self,
141144
callback: (
@@ -157,6 +160,7 @@ def on_bookmark(
157160
"""
158161
return self._on_bookmark_callbacks.register(wrap_async(callback))
159162

163+
@add_example("bookmark_callbacks")
160164
def on_bookmarked(
161165
self,
162166
callback: Callable[[str], None] | Callable[[str], Awaitable[None]],

shiny/bookmark/_global.py

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,89 @@ def as_bookmark_dir_fn(fn: BookmarkDirFn | None) -> BookmarkDirFnAsync | None:
3535
return wrap_async(fn)
3636

3737

38-
# TODO: Barret - Integrate Set / Restore for Connect. Ex: Connect https://github.com/posit-dev/connect/blob/8de330aec6a61cf21e160b5081d08a1d3d7e8129/R/connect.R#L915
38+
def set_global_save_dir_fn(fn: BookmarkDirFn):
39+
"""
40+
Set the global bookmark save directory function.
3941
42+
This function is NOT intended to be used by app authors. Instead, it is a last resort option for hosted invironments to adjust how bookmarks are saved.
4043
41-
def set_global_save_dir_fn(fn: BookmarkDirFn):
42-
"""TODO: Barret document"""
44+
Parameters
45+
----------
46+
fn : BookmarkDirFn
47+
The function that will be used to determine the directory where bookmarks are saved. This function should create the directory (`pathlib.Path` object) that is returned.
48+
49+
Examples
50+
--------
51+
```python
52+
from pathlib import Path
53+
from shiny.bookmark import set_global_save_dir_fn, set_global_restore_dir_fn
54+
55+
bookmark_dir = Path(__file__).parent / "bookmarks"
56+
57+
def save_bookmark_dir(id: str) -> Path:
58+
save_dir = bookmark_dir / id
59+
save_dir.mkdir(parents=True, exist_ok=True)
60+
return save_dir
61+
62+
def restore_bookmark_dir(id: str) -> Path:
63+
return bookmark_dir / id
64+
65+
# Set global defaults for bookmark saving and restoring.
66+
set_global_restore_dir_fn(restore_bookmark_dir)
67+
set_global_save_dir_fn(save_bookmark_dir)
68+
69+
app = App(app_ui, server, bookmark_store="server")
70+
```
71+
72+
73+
See Also
74+
--------
75+
* `~shiny.bookmark.set_global_restore_dir_fn` : Set the global bookmark restore directory function
76+
"""
4377
global bookmark_save_dir
4478

4579
bookmark_save_dir = as_bookmark_dir_fn(fn)
4680
return fn
4781

4882

4983
def set_global_restore_dir_fn(fn: BookmarkDirFn):
50-
"""TODO: Barret document"""
84+
"""
85+
Set the global bookmark restore directory function.
86+
87+
This function is NOT intended to be used by app authors. Instead, it is a last resort option for hosted invironments to adjust how bookmarks are restored.
88+
89+
Parameters
90+
----------
91+
fn : BookmarkDirFn
92+
The function that will be used to determine the directory (`pathlib.Path` object) where bookmarks are restored from.
93+
94+
Examples
95+
--------
96+
```python
97+
from pathlib import Path
98+
from shiny.bookmark import set_global_save_dir_fn, set_global_restore_dir_fn
99+
100+
bookmark_dir = Path(__file__).parent / "bookmarks"
101+
102+
def save_bookmark_dir(id: str) -> Path:
103+
save_dir = bookmark_dir / id
104+
save_dir.mkdir(parents=True, exist_ok=True)
105+
return save_dir
106+
107+
def restore_bookmark_dir(id: str) -> Path:
108+
return bookmark_dir / id
109+
110+
# Set global defaults for bookmark saving and restoring.
111+
set_global_restore_dir_fn(restore_bookmark_dir)
112+
set_global_save_dir_fn(save_bookmark_dir)
113+
114+
app = App(app_ui, server, bookmark_store="server")
115+
```
116+
117+
See Also
118+
--------
119+
* `~shiny.bookmark.set_global_save_dir_fn` : Set the global bookmark save directory function.
120+
"""
51121
global bookmark_restore_dir
52122

53123
bookmark_restore_dir = as_bookmark_dir_fn(fn)

shiny/bookmark/_restore_state.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import TYPE_CHECKING, Any, Literal, Optional
88
from urllib.parse import parse_qs, parse_qsl
99

10+
from .._docstring import add_example
1011
from ..module import ResolvedId
1112
from ._bookmark_state import local_restore_dir
1213
from ._types import BookmarkRestoreDirFn
@@ -205,7 +206,6 @@ async def _load_state_qs(self, query_string: str, *, app: App) -> None:
205206
if not self.dir.exists():
206207
raise RuntimeError("Bookmarked state directory does not exist.")
207208

208-
# TODO: Barret; Store/restore as JSON
209209
input_values = from_json_file(self.dir / "input.json")
210210
self.input = RestoreInputSet(input_values)
211211

@@ -387,6 +387,7 @@ def get_current_restore_context() -> RestoreContext | None:
387387
return ctx
388388

389389

390+
@add_example()
390391
def restore_input(resolved_id: ResolvedId, default: Any) -> Any:
391392
"""
392393
Restore an input value

shiny/bookmark/_save_state.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ async def _save_state(self, *, app: App) -> str:
7272

7373
if save_bookmark_fn is None:
7474
if in_shiny_server():
75-
# TODO: Barret; Implement `bookmark_save_dir` for Connect
7675
raise NotImplementedError(
7776
"The hosting environment does not support server-side bookmarking."
7877
)

shiny/session/_session.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,8 +1201,8 @@ class UpdateProgressMessage(TypedDict):
12011201

12021202
class SessionProxy(Session):
12031203
def __init__(self, parent: Session, ns: ResolvedId) -> None:
1204-
# TODO: Barret - Q: Why are we storing `parent`? It really feels like all `._parent` should be replaced with `.root_scope()` or `._root`, really
1205-
# TODO: Barret - Q: Why is there no super().__init__()? Why don't we proxy to the root on get/set?
1204+
super().__init__()
1205+
12061206
self._parent = parent
12071207
self.app = parent.app
12081208
self.id = parent.id

tests/playwright/shiny/bookmark/modules/app-core.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ def mod_btn(idx: int):
1515
ui.layout_column_wrap(
1616
ui.TagList(
1717
ui.input_radio_buttons(
18-
"btn1", "Button Input", choices=["a", "b", "c"], selected="a"
18+
"btn1",
19+
"Button Input",
20+
choices=["a", "b", "c"],
21+
selected="a",
1922
),
2023
ui.input_radio_buttons(
2124
"btn2",

0 commit comments

Comments
 (0)