Skip to content

Commit 30c8dd8

Browse files
committed
Add shiny.bookmark.input_bookmark_button(); Use globals to set bookmark save and restore methods
1 parent 80f6091 commit 30c8dd8

File tree

11 files changed

+213
-182
lines changed

11 files changed

+213
-182
lines changed

shiny/_app.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from contextlib import AsyncExitStack, asynccontextmanager
88
from inspect import signature
99
from pathlib import Path
10-
from typing import Any, Callable, Mapping, Optional, TypeVar, cast
10+
from typing import Any, Callable, Literal, Mapping, Optional, TypeVar, cast
1111

1212
import starlette.applications
1313
import starlette.exceptions
@@ -31,6 +31,7 @@
3131
from ._error import ErrorMiddleware
3232
from ._shinyenv import is_pyodide
3333
from ._utils import guess_mime_type, is_async_callable, sort_keys_length
34+
from .bookmark import _globals as bookmark_globals
3435
from .bookmark._restore_state import (
3536
RestoreContext,
3637
get_current_restore_context,
@@ -120,6 +121,7 @@ def __init__(
120121
),
121122
*,
122123
static_assets: Optional[str | Path | Mapping[str, str | Path]] = None,
124+
bookmarking: Optional[Literal["url", "query", "disable"]] = None,
123125
debug: bool = False,
124126
) -> None:
125127
# Used to store callbacks to be called when the app is shutting down (according
@@ -139,6 +141,8 @@ def __init__(
139141
"`server` must have 1 (Inputs) or 3 parameters (Inputs, Outputs, Session)"
140142
)
141143

144+
if bookmarking is not None:
145+
bookmark_globals.bookmark_store = bookmarking
142146
self._debug: bool = debug
143147

144148
# Settings that the user can change after creating the App object.
@@ -359,35 +363,29 @@ async def _on_root_request_cb(self, request: Request) -> Response:
359363
request for / occurs.
360364
"""
361365
ui: RenderedHTML
362-
# Create a restore context using query string
363-
# TODO: Barret implement how to get bookmark_store value
364-
# bookmarkStore <- getShinyOption("bookmarkStore", default = "disable")
365-
print("TODO: Figure this out")
366-
bookmark_store: str = str("disable")
367-
bookmark_store: str = str("query")
368-
369-
if bookmark_store == "disable":
366+
if bookmark_globals.bookmark_store == "disable":
370367
restore_ctx = RestoreContext()
371368
else:
372369
restore_ctx = await RestoreContext.from_query_string(request.url.query)
373370

374371
print(
372+
"Restored state",
375373
{
376374
"values": restore_ctx.as_state().values,
377375
"input": restore_ctx.as_state().input,
378-
}
376+
},
379377
)
380378

381379
with restore_context(restore_ctx):
382380
if callable(self.ui):
383381
ui = self._render_page(self.ui(request), self.lib_prefix)
384382
else:
385-
# TODO: Why is this here as there's a with restore_context above?
386-
# TODO: Why not `if restore_ctx.active:`?
383+
# TODO: Barret - Q: Why is this here as there's a with restore_context above?
384+
# TODO: Barret - Q: Why not `if restore_ctx.active:`?
387385
cur_restore_ctx = get_current_restore_context()
388386
print("cur_restore_ctx", cur_restore_ctx)
389387
if cur_restore_ctx is not None and cur_restore_ctx.active:
390-
# TODO: See ?enableBookmarking
388+
# TODO: Barret - Docs: See ?enableBookmarking
391389
warnings.warn(
392390
"Trying to restore saved app state, but UI code must be a function for this to work!",
393391
stacklevel=1,

shiny/bookmark/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
BookmarkProxy,
77
ShinySaveState,
88
)
9-
from ._bookmark_state import BookmarkState
9+
from ._button import input_bookmark_button
1010
from ._restore_state import RestoreContext, RestoreContextState
1111

1212
__all__ = (
@@ -18,8 +18,8 @@
1818
"BookmarkApp",
1919
"BookmarkProxy",
2020
"BookmarkExpressStub",
21-
# _bookmark_state
22-
"BookmarkState",
21+
# _button
22+
"input_bookmark_button",
2323
# _restore_state
2424
"RestoreContext",
2525
"RestoreContextState",

shiny/bookmark/_bookmark.py

Lines changed: 46 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
from __future__ import annotations
2+
3+
from abc import ABC, abstractmethod
4+
from pathlib import Path
5+
from typing import TYPE_CHECKING, Awaitable, Callable, Literal, NoReturn
6+
7+
from .._utils import AsyncCallbacks, CancelCallback, wrap_async
8+
from ..types import MISSING, MISSING_TYPE
9+
from . import _globals as bookmark_globals
10+
from ._globals import BookmarkStore
11+
from ._restore_state import RestoreContextState
12+
from ._save_state import ShinySaveState
13+
114
# TODO: bookmark button
215

316
# TODO:
@@ -46,13 +59,6 @@
4659
# * May need to escape (all?) the parameters to avoid collisions with `h=` or `code=`.
4760
# Set query string to parent frame / tab
4861

49-
from abc import ABC, abstractmethod
50-
from pathlib import Path
51-
from typing import TYPE_CHECKING, Awaitable, Callable, Literal, NoReturn
52-
53-
from .._utils import AsyncCallbacks, CancelCallback, wrap_async
54-
from ._restore_state import RestoreContextState
55-
from ._save_state import ShinySaveState
5662

5763
if TYPE_CHECKING:
5864
from .._namespaces import ResolvedId
@@ -70,9 +76,6 @@
7076
ExpressStubSession = Any
7177

7278

73-
BookmarkStore = Literal["url", "server", "disable"]
74-
75-
7679
# TODO: future - Local storage Bookmark class!
7780
# * Needs a consistent id for storage.
7881
# * Needs ways to clean up other storage
@@ -84,7 +87,20 @@ class Bookmark(ABC):
8487
# TODO: Barret - This feels like it needs to be a weakref
8588
_session_root: Session
8689

87-
store: BookmarkStore
90+
_store: BookmarkStore | MISSING_TYPE
91+
92+
@property
93+
def store(self) -> BookmarkStore:
94+
# Should we allow for this?
95+
# Allows a per-session override of the global bookmark store
96+
if isinstance(self._session_root.bookmark._store, MISSING_TYPE):
97+
return bookmark_globals.bookmark_store
98+
return self._session_root.bookmark._store
99+
100+
@store.setter
101+
def store(self, value: BookmarkStore) -> None:
102+
self._session_root.bookmark._store = value
103+
self._store = value
88104

89105
_proxy_exclude_fns: list[Callable[[], list[str]]]
90106
exclude: list[str]
@@ -107,8 +123,18 @@ def __init__(self, session_root: Session):
107123
# from ._restore_state import RestoreContext
108124

109125
super().__init__()
126+
# TODO: Barret - Q: Should this be a weakref; Session -> Bookmark -> Session
110127
self._session_root = session_root
111128
self._restore_context = None
129+
self._store = MISSING
130+
131+
self._proxy_exclude_fns = []
132+
self.exclude = []
133+
134+
self._on_bookmark_callbacks = AsyncCallbacks()
135+
self._on_bookmarked_callbacks = AsyncCallbacks()
136+
self._on_restore_callbacks = AsyncCallbacks()
137+
self._on_restored_callbacks = AsyncCallbacks()
112138

113139
# # TODO: Barret - Implement this?!?
114140
# @abstractmethod
@@ -247,25 +273,14 @@ def on_restored(
247273

248274
class BookmarkApp(Bookmark):
249275
def __init__(self, session_root: Session):
250-
251276
super().__init__(session_root)
252277

253-
self.store = "disable"
254-
self.store = "url"
255-
self.exclude = []
256-
self._proxy_exclude_fns = []
257-
self._on_bookmark_callbacks = AsyncCallbacks()
258-
self._on_bookmarked_callbacks = AsyncCallbacks()
259-
self._on_restore_callbacks = AsyncCallbacks()
260-
self._on_restored_callbacks = AsyncCallbacks()
261-
262278
def _create_effects(self) -> None:
263279
# Get bookmarking config
264280
if self.store == "disable":
265281
return
266282

267283
print("Creating effects")
268-
269284
session = self._session_root
270285

271286
from .. import reactive
@@ -481,17 +496,9 @@ def __init__(self, session_proxy: SessionProxy):
481496
super().__init__(session_proxy.root_scope())
482497

483498
self._ns = session_proxy.ns
484-
# TODO: Barret - This feels like it needs to be a weakref
499+
# TODO: Barret - Q: Should this be a weakref
485500
self._session_proxy = session_proxy
486501

487-
self.exclude = []
488-
self._proxy_exclude_fns = []
489-
self._on_bookmark_callbacks = AsyncCallbacks()
490-
self._on_bookmarked_callbacks = AsyncCallbacks()
491-
self._on_restore_callbacks = AsyncCallbacks()
492-
self._on_restored_callbacks = AsyncCallbacks()
493-
494-
# TODO: Barret - Double check that this works with nested modules!
495502
self._session_root.bookmark._proxy_exclude_fns.append(
496503
lambda: [str(self._ns(name)) for name in self.exclude]
497504
)
@@ -508,6 +515,14 @@ async def scoped_on_bookmark(root_state: ShinySaveState) -> None:
508515

509516
from ..session import session_context
510517

518+
@self._root_bookmark.on_bookmarked
519+
async def scoped_on_bookmarked(url: str) -> None:
520+
if self._on_bookmarked_callbacks.count() == 0:
521+
return
522+
523+
with session_context(self._session_proxy):
524+
await self._on_bookmarked_callbacks.invoke(url)
525+
511526
ns_prefix = str(self._ns + self._ns._sep)
512527

513528
@self._root_bookmark.on_restore

shiny/bookmark/_bookmark_state.py

Lines changed: 13 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,21 @@
1+
from __future__ import annotations
2+
13
import os
2-
from abc import ABC, abstractmethod
34
from pathlib import Path
45

56

6-
class BookmarkState(ABC):
7-
"""
8-
Class for saving and restoring state to/from disk.
9-
"""
10-
11-
@abstractmethod
12-
async def save_dir(
13-
self,
14-
id: str,
15-
# write_files: Callable[[Path], Awaitable[None]],
16-
) -> Path:
17-
"""
18-
Construct directory for saving state.
19-
20-
Parameters
21-
----------
22-
id
23-
The unique identifier for the state.
24-
25-
Returns
26-
-------
27-
Path
28-
Directory location for saving state. This directory must exist.
29-
"""
30-
# write_files
31-
# A async function that writes the state to a serializable location. The method receives a path object and
32-
...
33-
34-
@abstractmethod
35-
async def load_dir(
36-
self,
37-
id: str,
38-
# read_files: Callable[[Path], Awaitable[None]],
39-
) -> Path:
40-
"""
41-
Construct directory for loading state.
42-
43-
Parameters
44-
----------
45-
id
46-
The unique identifier for the state.
47-
48-
Returns
49-
-------
50-
Path | None
51-
Directory location for loading state. If `None`, state loading will be ignored. If a `Path`, the directory must exist.
52-
"""
53-
...
54-
55-
56-
class BookmarkStateLocal(BookmarkState):
57-
"""
58-
Function wrappers for saving and restoring state to/from disk when running Shiny
59-
locally.
60-
"""
61-
62-
def _local_dir(self, id: str) -> Path:
63-
# Try to save/load from current working directory as we do not know where the
64-
# app file is located
65-
return Path(os.getcwd()) / "shiny_bookmarks" / id
66-
67-
async def save_dir(self, id: str) -> Path:
68-
state_dir = self._local_dir(id)
69-
if not state_dir.exists():
70-
state_dir.mkdir(parents=True)
71-
return state_dir
7+
def _local_dir(id: str) -> Path:
8+
# Try to save/load from current working directory as we do not know where the
9+
# app file is located
10+
return Path(os.getcwd()) / "shiny_bookmarks" / id
7211

73-
async def load_dir(self, id: str) -> Path:
74-
return self._local_dir(id)
7512

76-
# async def save(
77-
# self,
78-
# id: str,
79-
# write_files: Callable[[Path], Awaitable[None]],
80-
# ) -> None:
81-
# state_dir = self._local_dir(id)
82-
# if not state_dir.exists():
83-
# state_dir.mkdir(parents=True)
13+
async def local_save_dir(id: str) -> Path:
14+
state_dir = _local_dir(id)
15+
if not state_dir.exists():
16+
state_dir.mkdir(parents=True)
17+
return state_dir
8418

85-
# await write_files(state_dir)
8619

87-
# async def load(
88-
# self,
89-
# id: str,
90-
# read_files: Callable[[Path], Awaitable[None]],
91-
# ) -> None:
92-
# await read_files(self._local_dir(id))
93-
# await read_files(self._local_dir(id))
20+
async def local_load_dir(id: str) -> Path:
21+
return _local_dir(id)

0 commit comments

Comments
 (0)