66from contextlib import AsyncExitStack , asynccontextmanager
77from inspect import signature
88from pathlib import Path
9- from typing import Any , Callable , Mapping , Optional , TypeVar , cast
9+ from typing import Any , Callable , Literal , Mapping , Optional , TypeVar , cast
1010
1111import starlette .applications
1212import starlette .exceptions
3030from ._error import ErrorMiddleware
3131from ._shinyenv import is_pyodide
3232from ._utils import guess_mime_type , is_async_callable , sort_keys_length
33+ from .bookmark import _global as bookmark_global_state
34+ from .bookmark ._global import as_bookmark_dir_fn
35+ from .bookmark ._restore_state import RestoreContext , restore_context
36+ from .bookmark ._types import (
37+ BookmarkDirFn ,
38+ BookmarkRestoreDirFn ,
39+ BookmarkSaveDirFn ,
40+ BookmarkStore ,
41+ )
3342from .html_dependencies import jquery_deps , require_deps , shiny_deps
3443from .http_staticfiles import FileResponse , StaticFiles
3544from .session ._session import AppSession , Inputs , Outputs , Session , session_context
@@ -106,6 +115,10 @@ def server(input: Inputs, output: Outputs, session: Session):
106115 ui : RenderedHTML | Callable [[Request ], Tag | TagList ]
107116 server : Callable [[Inputs , Outputs , Session ], None ]
108117
118+ _bookmark_save_dir_fn : BookmarkSaveDirFn | None
119+ _bookmark_restore_dir_fn : BookmarkRestoreDirFn | None
120+ _bookmark_store : BookmarkStore
121+
109122 def __init__ (
110123 self ,
111124 ui : Tag | TagList | Callable [[Request ], Tag | TagList ] | Path ,
@@ -114,6 +127,8 @@ def __init__(
114127 ),
115128 * ,
116129 static_assets : Optional [str | Path | Mapping [str , str | Path ]] = None ,
130+ # Document type as Literal to have clearer type hints to App author
131+ bookmark_store : Literal ["url" , "server" , "disable" ] = "disable" ,
117132 debug : bool = False ,
118133 ) -> None :
119134 # Used to store callbacks to be called when the app is shutting down (according
@@ -133,6 +148,8 @@ def __init__(
133148 "`server` must have 1 (Inputs) or 3 parameters (Inputs, Outputs, Session)"
134149 )
135150
151+ self ._init_bookmarking (bookmark_store = bookmark_store , ui = ui )
152+
136153 self ._debug : bool = debug
137154
138155 # Settings that the user can change after creating the App object.
@@ -167,7 +184,7 @@ def __init__(
167184
168185 self ._sessions : dict [str , AppSession ] = {}
169186
170- self ._sessions_needing_flush : dict [int , AppSession ] = {}
187+ # self._sessions_needing_flush: dict[int, AppSession] = {}
171188
172189 self ._registered_dependencies : dict [str , HTMLDependency ] = {}
173190 self ._dependency_handler = starlette .routing .Router ()
@@ -353,8 +370,18 @@ async def _on_root_request_cb(self, request: Request) -> Response:
353370 request for / occurs.
354371 """
355372 ui : RenderedHTML
373+ if self .bookmark_store == "disable" :
374+ restore_ctx = RestoreContext ()
375+ else :
376+ restore_ctx = await RestoreContext .from_query_string (
377+ request .url .query , app = self
378+ )
379+
356380 if callable (self .ui ):
357- ui = self ._render_page (self .ui (request ), self .lib_prefix )
381+ # At this point, if `app.bookmark_store != "disable"`, then we've already
382+ # checked that `ui` is a function (in `App._init_bookmarking()`). No need to throw warning if `ui` is _not_ a function.
383+ with restore_context (restore_ctx ):
384+ ui = self ._render_page (self .ui (request ), self .lib_prefix )
358385 else :
359386 ui = self .ui
360387 return HTMLResponse (content = ui ["html" ])
@@ -466,6 +493,30 @@ def _render_page_from_file(self, file: Path, lib_prefix: str) -> RenderedHTML:
466493
467494 return rendered
468495
496+ # ==========================================================================
497+ # Bookmarking
498+ # ==========================================================================
499+
500+ def _init_bookmarking (self , * , bookmark_store : BookmarkStore , ui : Any ) -> None :
501+ self ._bookmark_save_dir_fn = bookmark_global_state .bookmark_save_dir
502+ self ._bookmark_restore_dir_fn = bookmark_global_state .bookmark_restore_dir
503+ self ._bookmark_store = bookmark_store
504+
505+ if bookmark_store != "disable" and not callable (ui ):
506+ raise TypeError (
507+ "App(ui=) must be a function that accepts a request object to allow the UI to be properly reconstructed from a bookmarked state."
508+ )
509+
510+ @property
511+ def bookmark_store (self ) -> BookmarkStore :
512+ return self ._bookmark_store
513+
514+ def set_bookmark_save_dir_fn (self , bookmark_save_dir_fn : BookmarkDirFn ):
515+ self ._bookmark_save_dir_fn = as_bookmark_dir_fn (bookmark_save_dir_fn )
516+
517+ def set_bookmark_restore_dir_fn (self , bookmark_restore_dir_fn : BookmarkDirFn ):
518+ self ._bookmark_restore_dir_fn = as_bookmark_dir_fn (bookmark_restore_dir_fn )
519+
469520
470521def is_uifunc (x : Path | Tag | TagList | Callable [[Request ], Tag | TagList ]) -> bool :
471522 if (
0 commit comments