diff --git a/CHANGELOG.md b/CHANGELOG.md index 111251c28..b3d9ee8aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## New features -* 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) +* 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) * To enable bookmarking in Express mode, set `shiny.express.app_opts(bookmark_store=)` during the app's initial construction. * To enable bookmarking in Core mode, set `shiny.App(bookmark_store=)` when constructing the `app` object. diff --git a/shiny/_app.py b/shiny/_app.py index 6eeba7b68..a161cb03a 100644 --- a/shiny/_app.py +++ b/shiny/_app.py @@ -30,7 +30,6 @@ from ._error import ErrorMiddleware from ._shinyenv import is_pyodide from ._utils import guess_mime_type, is_async_callable, sort_keys_length -from .bookmark import _global as bookmark_global_state from .bookmark._global import as_bookmark_dir_fn from .bookmark._restore_state import RestoreContext, restore_context from .bookmark._types import ( @@ -42,6 +41,7 @@ from .html_dependencies import jquery_deps, require_deps, shiny_deps from .http_staticfiles import FileResponse, StaticFiles from .session._session import AppSession, Inputs, Outputs, Session, session_context +from .types import MISSING, MISSING_TYPE T = TypeVar("T") @@ -115,8 +115,8 @@ def server(input: Inputs, output: Outputs, session: Session): ui: RenderedHTML | Callable[[Request], Tag | TagList] server: Callable[[Inputs, Outputs, Session], None] - _bookmark_save_dir_fn: BookmarkSaveDirFn | None - _bookmark_restore_dir_fn: BookmarkRestoreDirFn | None + _bookmark_save_dir_fn: BookmarkSaveDirFn | None | MISSING_TYPE + _bookmark_restore_dir_fn: BookmarkRestoreDirFn | None | MISSING_TYPE _bookmark_store: BookmarkStore def __init__( @@ -498,8 +498,8 @@ def _render_page_from_file(self, file: Path, lib_prefix: str) -> RenderedHTML: # ========================================================================== def _init_bookmarking(self, *, bookmark_store: BookmarkStore, ui: Any) -> None: - self._bookmark_save_dir_fn = bookmark_global_state.bookmark_save_dir - self._bookmark_restore_dir_fn = bookmark_global_state.bookmark_restore_dir + self._bookmark_save_dir_fn = MISSING + self._bookmark_restore_dir_fn = MISSING self._bookmark_store = bookmark_store if bookmark_store != "disable" and not callable(ui): diff --git a/shiny/bookmark/_global.py b/shiny/bookmark/_global.py index ed9741c23..d4b3bafc5 100644 --- a/shiny/bookmark/_global.py +++ b/shiny/bookmark/_global.py @@ -3,6 +3,7 @@ from typing import overload from .._utils import wrap_async +from ..types import MISSING_TYPE from ._types import ( BookmarkDirFn, BookmarkDirFnAsync, @@ -10,13 +11,36 @@ BookmarkSaveDirFn, ) -# WARNING! This file contains global state! +# WARNING! This file contains global default values! + + # During App initialization, the save_dir and restore_dir functions are conventionally set # to read-only on the App. +# If nothing is set on the `app` object, the global default bookmark functions are found and used during every save/restore. +_default_bookmark_save_dir_fn: BookmarkSaveDirFn | None = None +_default_bookmark_restore_dir_fn: BookmarkRestoreDirFn | None = None + + +def get_bookmark_save_dir_fn( + save_dir_fn: BookmarkSaveDirFn | None | MISSING_TYPE, +) -> BookmarkSaveDirFn | None: + if isinstance(save_dir_fn, MISSING_TYPE): + # Allow for default bookmark function to be utilized after app initialization. + # Sometimes the app is created before hooks are registered. + return _default_bookmark_save_dir_fn + else: + return save_dir_fn -bookmark_save_dir: BookmarkSaveDirFn | None = None -bookmark_restore_dir: BookmarkRestoreDirFn | None = None +def get_bookmark_restore_dir_fn( + restore_dir_fn: BookmarkRestoreDirFn | None | MISSING_TYPE, +) -> BookmarkRestoreDirFn | None: + if isinstance(restore_dir_fn, MISSING_TYPE): + # Allow for default bookmark function to be utilized after app initialization. + # Sometimes the app is created before hooks are registered. + return _default_bookmark_restore_dir_fn + else: + return restore_dir_fn @overload @@ -74,9 +98,9 @@ def restore_bookmark_dir(id: str) -> Path: -------- * `~shiny.bookmark.set_global_restore_dir_fn` : Set the global bookmark restore directory function """ - global bookmark_save_dir + global _default_bookmark_save_dir_fn - bookmark_save_dir = as_bookmark_dir_fn(fn) + _default_bookmark_save_dir_fn = as_bookmark_dir_fn(fn) return fn @@ -118,7 +142,7 @@ def restore_bookmark_dir(id: str) -> Path: -------- * `~shiny.bookmark.set_global_save_dir_fn` : Set the global bookmark save directory function. """ - global bookmark_restore_dir + global _default_bookmark_restore_dir_fn - bookmark_restore_dir = as_bookmark_dir_fn(fn) + _default_bookmark_restore_dir_fn = as_bookmark_dir_fn(fn) return fn diff --git a/shiny/bookmark/_restore_state.py b/shiny/bookmark/_restore_state.py index 8be1a9110..b76c8a976 100644 --- a/shiny/bookmark/_restore_state.py +++ b/shiny/bookmark/_restore_state.py @@ -10,6 +10,7 @@ from .._docstring import add_example from ..module import ResolvedId from ._bookmark_state import local_restore_dir +from ._global import get_bookmark_restore_dir_fn from ._types import BookmarkRestoreDirFn from ._utils import from_json_file, from_json_str, in_shiny_server @@ -189,7 +190,9 @@ async def _load_state_qs(self, query_string: str, *, app: App) -> None: id = id[0] - load_bookmark_fn: BookmarkRestoreDirFn | None = app._bookmark_restore_dir_fn + load_bookmark_fn: BookmarkRestoreDirFn | None = get_bookmark_restore_dir_fn( + app._bookmark_restore_dir_fn + ) if load_bookmark_fn is None: if in_shiny_server(): diff --git a/shiny/bookmark/_save_state.py b/shiny/bookmark/_save_state.py index 15789ef09..8effbbac6 100644 --- a/shiny/bookmark/_save_state.py +++ b/shiny/bookmark/_save_state.py @@ -7,6 +7,7 @@ from .._utils import private_random_id from ..reactive import isolate from ._bookmark_state import local_save_dir +from ._global import get_bookmark_save_dir_fn from ._types import BookmarkSaveDirFn from ._utils import in_shiny_server, to_json_file, to_json_str @@ -66,7 +67,9 @@ async def _save_state(self, *, app: App) -> str: # to `self.dir`. # This will be defined by the hosting environment if it supports bookmarking. - save_bookmark_fn: BookmarkSaveDirFn | None = app._bookmark_save_dir_fn + save_bookmark_fn: BookmarkSaveDirFn | None = get_bookmark_save_dir_fn( + app._bookmark_save_dir_fn + ) if save_bookmark_fn is None: if in_shiny_server():