Skip to content

Commit c7a3a3b

Browse files
committed
Get express mode to work! 🎉
Inspired #1895
1 parent edaf393 commit c7a3a3b

File tree

3 files changed

+46
-39
lines changed

3 files changed

+46
-39
lines changed

‎shiny/_app.py‎

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -371,14 +371,6 @@ async def _on_root_request_cb(self, request: Request) -> Response:
371371
else:
372372
restore_ctx = await RestoreContext.from_query_string(request.url.query)
373373

374-
print(
375-
"Restored state",
376-
{
377-
"values": restore_ctx.as_state().values,
378-
"input": restore_ctx.as_state().input,
379-
},
380-
)
381-
382374
with restore_context(restore_ctx):
383375
if callable(self.ui):
384376
ui = self._render_page(self.ui(request), self.lib_prefix)

‎shiny/bookmark/_bookmark.py‎

Lines changed: 16 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -662,11 +662,6 @@ class BookmarkExpressStub(Bookmark):
662662

663663
def __init__(self, session_root: ExpressStubSession) -> None:
664664
super().__init__(session_root)
665-
self._proxy_exclude_fns = []
666-
self._on_bookmark_callbacks = AsyncCallbacks()
667-
self._on_bookmarked_callbacks = AsyncCallbacks()
668-
self._on_restore_callbacks = AsyncCallbacks()
669-
self._on_restored_callbacks = AsyncCallbacks()
670665

671666
def _create_effects(self) -> NoReturn:
672667
raise NotImplementedError(
@@ -683,50 +678,42 @@ def on_bookmark(
683678
callback: (
684679
Callable[[BookmarkState], None] | Callable[[BookmarkState], Awaitable[None]]
685680
),
686-
) -> NoReturn:
687-
raise NotImplementedError(
688-
"Please call `.on_bookmark()` only from a real session object"
689-
)
681+
) -> CancelCallback:
682+
# Provide a no-op function within ExpressStub
683+
return lambda: None
690684

691685
def on_bookmarked(
692686
self,
693687
callback: Callable[[str], None] | Callable[[str], Awaitable[None]],
694-
) -> NoReturn:
695-
raise NotImplementedError(
696-
"Please call `.on_bookmarked()` only from a real session object"
697-
)
688+
) -> CancelCallback:
689+
# Provide a no-op function within ExpressStub
690+
return lambda: None
698691

699692
async def update_query_string(
700693
self, query_string: str, mode: Literal["replace", "push"] = "replace"
701-
) -> NoReturn:
702-
raise NotImplementedError(
703-
"Please call `.update_query_string()` only from a real session object"
704-
)
694+
) -> None:
695+
return None
705696

706-
async def do_bookmark(self) -> NoReturn:
707-
raise NotImplementedError(
708-
"Please call `.do_bookmark()` only from a real session object"
709-
)
697+
async def do_bookmark(self) -> None:
698+
return None
710699

711700
def on_restore(
712701
self,
713702
callback: (
714703
Callable[[RestoreState], None] | Callable[[RestoreState], Awaitable[None]]
715704
),
716-
) -> NoReturn:
717-
raise NotImplementedError(
718-
"Please call `.on_restore()` only from a real session object"
719-
)
705+
) -> CancelCallback:
706+
# Provide a no-op function within ExpressStub
707+
return lambda: None
720708

721709
def on_restored(
722710
self,
723711
callback: (
724712
Callable[[RestoreState], None] | Callable[[RestoreState], Awaitable[None]]
725713
),
726-
) -> NoReturn:
727-
raise NotImplementedError(
728-
"Please call `.on_restored()` only from a real session object"
729-
)
714+
) -> CancelCallback:
715+
# Provide a no-op function within ExpressStub
716+
return lambda: None
730717

731718

732719
# #' Generate a modal dialog that displays a URL

‎shiny/express/_run.py‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@
77
import types
88
from importlib.machinery import ModuleSpec
99
from pathlib import Path
10-
from typing import Mapping, Sequence, cast
10+
from typing import Literal, Mapping, Sequence, cast
1111

1212
from htmltools import Tag, TagList
13+
from starlette.requests import Request
1314

1415
from .._app import App
1516
from .._docstring import no_example
1617
from .._typing_extensions import NotRequired, TypedDict
1718
from .._utils import import_module_from_path
19+
from ..bookmark._types import BookmarkStore
1820
from ..session import Inputs, Outputs, Session, get_current_session, session_context
1921
from ..types import MISSING, MISSING_TYPE
2022
from ._is_express import find_magic_comment_mode
@@ -115,13 +117,13 @@ def create_express_app(file: Path, package_name: str) -> App:
115117

116118
file = file.resolve()
117119

120+
stub_session = ExpressStubSession()
118121
try:
119122
globals_file = file.parent / "globals.py"
120123
if globals_file.is_file():
121124
with session_context(None):
122125
import_module_from_path("globals", globals_file)
123126

124-
stub_session = ExpressStubSession()
125127
with session_context(stub_session):
126128
# We tagify here, instead of waiting for the App object to do it when it wraps
127129
# the UI in a HTMLDocument and calls render() on it. This is because
@@ -134,6 +136,17 @@ def create_express_app(file: Path, package_name: str) -> App:
134136
except AttributeError as e:
135137
raise RuntimeError(e) from e
136138

139+
express_bookmark_store = stub_session.app_opts.get("bookmark_store", "disable")
140+
if express_bookmark_store != "disable":
141+
# If bookmarking is enabled, wrap UI in function to automatically leverage UI
142+
# functions to restore their values
143+
def app_ui_wrapper(request: Request):
144+
# Stub session used to pass `app_opts()` checks.
145+
with session_context(ExpressStubSession()):
146+
return run_express(file, package_name).tagify()
147+
148+
app_ui = app_ui_wrapper
149+
137150
def express_server(input: Inputs, output: Outputs, session: Session):
138151
try:
139152
run_express(file, package_name)
@@ -290,12 +303,15 @@ def __getattr__(self, name: str):
290303

291304
class AppOpts(TypedDict):
292305
static_assets: NotRequired[dict[str, Path]]
306+
bookmark_store: NotRequired[BookmarkStore]
293307
debug: NotRequired[bool]
294308

295309

296310
@no_example()
297311
def app_opts(
312+
*,
298313
static_assets: str | Path | Mapping[str, str | Path] | MISSING_TYPE = MISSING,
314+
bookmark_store: Literal["url", "server", "disable"] | MISSING_TYPE = MISSING,
299315
debug: bool | MISSING_TYPE = MISSING,
300316
):
301317
"""
@@ -313,6 +329,12 @@ def app_opts(
313329
that mount point. In Shiny Express, if there is a `www` subdirectory of the
314330
directory containing the app file, it will automatically be mounted at `/`, even
315331
without needing to set the option here.
332+
bookmark_store
333+
Where to store the bookmark state.
334+
335+
* `"url"`: Encode the bookmark state in the URL.
336+
* `"server"`: Store the bookmark state on the server.
337+
* `"disable"`: Disable bookmarking.
316338
debug
317339
Whether to enable debug mode.
318340
"""
@@ -339,6 +361,9 @@ def app_opts(
339361

340362
stub_session.app_opts["static_assets"] = static_assets_paths
341363

364+
if not isinstance(bookmark_store, MISSING_TYPE):
365+
stub_session.app_opts["bookmark_store"] = bookmark_store
366+
342367
if not isinstance(debug, MISSING_TYPE):
343368
stub_session.app_opts["debug"] = debug
344369

@@ -357,6 +382,9 @@ def _merge_app_opts(app_opts: AppOpts, app_opts_new: AppOpts) -> AppOpts:
357382
elif "static_assets" in app_opts_new:
358383
app_opts["static_assets"] = app_opts_new["static_assets"].copy()
359384

385+
if "bookmark_store" in app_opts_new:
386+
app_opts["bookmark_store"] = app_opts_new["bookmark_store"]
387+
360388
if "debug" in app_opts_new:
361389
app_opts["debug"] = app_opts_new["debug"]
362390

0 commit comments

Comments
 (0)