Skip to content

Commit 44e4811

Browse files
committed
BookmarkProxy classes will not register any handlers until requested
1 parent c7b788f commit 44e4811

File tree

2 files changed

+95
-146
lines changed

2 files changed

+95
-146
lines changed

shiny/bookmark/_bookmark.py

Lines changed: 95 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
import warnings
44
from abc import ABC, abstractmethod
55
from pathlib import Path
6-
from typing import TYPE_CHECKING, Awaitable, Callable, Literal, NoReturn, Optional
7-
8-
from htmltools import TagChild
6+
from typing import TYPE_CHECKING, Awaitable, Callable, Literal
97

108
from .._docstring import add_example
119
from .._utils import AsyncCallbacks, CancelCallback, wrap_async
@@ -34,6 +32,7 @@ class Bookmark(ABC):
3432

3533
_on_get_exclude: list[Callable[[], list[str]]]
3634
"""Callbacks that BookmarkProxy classes utilize to help determine the list of inputs to exclude from bookmarking."""
35+
3736
exclude: list[str]
3837
"""A list of scoped Input names to exclude from bookmarking."""
3938

@@ -129,15 +128,14 @@ def on_bookmarked(
129128
130129
This callback will be executed **after** the bookmark state is saved serverside or in the URL.
131130
132-
Note: `.on_bookmarked()` can only be added to the `AppSession` object, not on a a module's session.
133-
134131
Parameters
135132
----------
136133
callback
137134
The callback function to call when the session is bookmarked. This method
138135
should accept a single argument, the string representing the query parameter
139136
component of the URL.
140137
"""
138+
141139
return self._on_bookmarked_callbacks.register(wrap_async(callback))
142140

143141
def on_restore(
@@ -313,65 +311,6 @@ async def show_bookmark_url_modal(
313311
return
314312

315313

316-
# #' Generate a modal dialog that displays a URL
317-
# #'
318-
# #' The modal dialog generated by `urlModal` will display the URL in a
319-
# #' textarea input, and the URL text will be selected so that it can be easily
320-
# #' copied. The result from `urlModal` should be passed to the
321-
# #' [showModal()] function to display it in the browser.
322-
# #'
323-
# #' @param url A URL to display in the dialog box.
324-
# #' @param title A title for the dialog box.
325-
# #' @param subtitle Text to display underneath URL.
326-
# #' @export
327-
# urlModal <- function(url, title = "Bookmarked application link", subtitle = NULL) {
328-
329-
# subtitleTag <- tagList(
330-
# br(),
331-
# span(class = "text-muted", subtitle),
332-
# span(id = "shiny-bookmark-copy-text", class = "text-muted")
333-
# )
334-
335-
# modalDialog(
336-
# title = title,
337-
# easyClose = TRUE,
338-
# tags$textarea(class = "form-control", rows = "1", style = "resize: none;",
339-
# readonly = "readonly",
340-
# url
341-
# ),
342-
# subtitleTag,
343-
# # Need separate show and shown listeners. The show listener sizes the
344-
# # textarea just as the modal starts to fade in. The 200ms delay is needed
345-
# # because if we try to resize earlier, it can't calculate the text height
346-
# # (scrollHeight will be reported as zero). The shown listener selects the
347-
# # text; it's needed because because selection has to be done after the fade-
348-
# # in is completed.
349-
# tags$script(
350-
# "$('#shiny-modal').
351-
# one('show.bs.modal', function() {
352-
# setTimeout(function() {
353-
# var $textarea = $('#shiny-modal textarea');
354-
# $textarea.innerHeight($textarea[0].scrollHeight);
355-
# }, 200);
356-
# });
357-
# $('#shiny-modal')
358-
# .one('shown.bs.modal', function() {
359-
# $('#shiny-modal textarea').select().focus();
360-
# });
361-
# $('#shiny-bookmark-copy-text')
362-
# .text(function() {
363-
# if (/Mac/i.test(navigator.userAgent)) {
364-
# return 'Press \u2318-C to copy.';
365-
# } else {
366-
# return 'Press Ctrl-C to copy.';
367-
# }
368-
# });
369-
# "
370-
# )
371-
# )
372-
# }
373-
374-
375314
class BookmarkApp(Bookmark):
376315
_root_session: AppSession
377316
"""
@@ -507,38 +446,6 @@ async def invoke_on_restored_callbacks():
507446

508447
return
509448

510-
def on_bookmark(
511-
self,
512-
callback: (
513-
Callable[[BookmarkState], None] | Callable[[BookmarkState], Awaitable[None]]
514-
),
515-
/,
516-
) -> CancelCallback:
517-
return self._on_bookmark_callbacks.register(wrap_async(callback))
518-
519-
def on_bookmarked(
520-
self,
521-
callback: Callable[[str], None] | Callable[[str], Awaitable[None]],
522-
/,
523-
) -> CancelCallback:
524-
return self._on_bookmarked_callbacks.register(wrap_async(callback))
525-
526-
def on_restore(
527-
self,
528-
callback: (
529-
Callable[[RestoreState], None] | Callable[[RestoreState], Awaitable[None]]
530-
),
531-
) -> CancelCallback:
532-
return self._on_restore_callbacks.register(wrap_async(callback))
533-
534-
def on_restored(
535-
self,
536-
callback: (
537-
Callable[[RestoreState], None] | Callable[[RestoreState], Awaitable[None]]
538-
),
539-
) -> CancelCallback:
540-
return self._on_restored_callbacks.register(wrap_async(callback))
541-
542449
async def update_query_string(
543450
self,
544451
query_string: str,
@@ -564,7 +471,7 @@ def _get_bookmark_exclude(self) -> list[str]:
564471
for proxy_exclude_fn in self._on_get_exclude:
565472
scoped_excludes.extend(proxy_exclude_fn())
566473
# Remove duplicates
567-
return list(set([*self.exclude, *scoped_excludes]))
474+
return [str(name) for name in set([*self.exclude, *scoped_excludes])]
568475

569476
async def get_bookmark_url(self) -> str | None:
570477
if self.store == "disable":
@@ -644,7 +551,7 @@ async def do_bookmark(self) -> None:
644551
# Barret:
645552
# This action feels weird. I don't believe it should occur
646553
# Instead, I believe it should update the query string and set the user's clipboard with a UI notification in the corner.
647-
with session_context(self._session):
554+
with session_context(self._root_session):
648555
await self.show_bookmark_url_modal(full_url)
649556

650557
except Exception as e:
@@ -674,56 +581,23 @@ def __init__(self, session_proxy: SessionProxy):
674581
self._proxy_session = session_proxy
675582
self._root_session = session_proxy._root_session
676583

677-
# Maybe `._get_bookmark_exclude()` should be used instead of`proxy_exclude_fns`?
584+
# Be sure root bookmark has access to proxy's excluded values
678585
self._root_bookmark._on_get_exclude.append(
679586
lambda: [str(self._ns(name)) for name in self.exclude]
680587
)
681588

682-
# When scope is created, register these bookmarking callbacks on the main
683-
# session object. They will invoke the scope's own callbacks, if any are
684-
# present.
685-
686-
# The goal of this method is to save the scope's values. All namespaced inputs
687-
# will already exist within the `root_state`.
688-
self._root_bookmark.on_bookmark(self._scoped_on_bookmark)
689-
690-
from ..session import session_context
691-
692-
# # Disabling `on_bookmarked` within BookmarkProxy so we can tell when
693-
# # the user has not registered a `on_bookmarked` callback.
694-
# @self._root_bookmark.on_bookmarked
695-
# async def scoped_on_bookmarked(url: str) -> None:
696-
# if self._on_bookmarked_callbacks.count() == 0:
697-
# return
698-
# with session_context(self._proxy_session):
699-
# await self._on_bookmarked_callbacks.invoke(url)
700-
701-
ns_prefix = str(self._ns + self._ns._sep)
702-
703-
@self._root_bookmark.on_restore
704-
async def scoped_on_restore(restore_state: RestoreState) -> None:
705-
if self._on_restore_callbacks.count() == 0:
706-
return
707-
708-
scoped_restore_state = restore_state._state_within_namespace(ns_prefix)
709-
710-
with session_context(self._proxy_session):
711-
await self._on_restore_callbacks.invoke(scoped_restore_state)
712-
713-
@self._root_bookmark.on_restored
714-
async def scoped_on_restored(restore_state: RestoreState) -> None:
715-
if self._on_restored_callbacks.count() == 0:
716-
return
589+
# Note: This proxy bookmark class will not register a handler (`on_bookmark`,
590+
# `on_bookmarked`, `on_restore`, `on_restored`) until one is requested either by
591+
# the app author or a sub-proxy bookmark class
592+
# These function are utilized
717593

718-
scoped_restore_state = restore_state._state_within_namespace(ns_prefix)
719-
with session_context(self._proxy_session):
720-
await self._on_restored_callbacks.invoke(scoped_restore_state)
594+
@property
595+
def _ns_prefix(self) -> str:
596+
return str(self._ns + self._ns._sep)
721597

598+
# The goal of this method is to save the scope's values. All namespaced inputs
599+
# will already exist within the `root_state`.
722600
async def _scoped_on_bookmark(self, root_state: BookmarkState) -> None:
723-
# Exit if no user-defined callbacks.
724-
if self._on_bookmark_callbacks.count() == 0:
725-
return
726-
727601
from ..bookmark._bookmark import BookmarkState
728602

729603
scoped_state = BookmarkState(
@@ -737,14 +611,19 @@ async def _scoped_on_bookmark(self, root_state: BookmarkState) -> None:
737611
if root_state.dir is not None:
738612
scope_subpath = self._ns
739613
scoped_state.dir = Path(root_state.dir) / scope_subpath
614+
# Barret - 2025-03-17:
615+
# Having Shiny make this directory feels like the wrong thing to do here.
616+
# Feels like we should be using the App's bookmark_save_dir function to
617+
# determine where to save the bookmark state.
618+
# However, R-Shiny currently creates the directory:
619+
# https://github.com/rstudio/shiny/blob/f55c26af4a0493b082d2967aca6d36b90795adf1/R/shiny.R#L940
740620
scoped_state.dir.mkdir(parents=True, exist_ok=True)
741621

742622
if not scoped_state.dir.exists():
743623
raise FileNotFoundError(
744624
f"Scope directory could not be created for {scope_subpath}"
745625
)
746626

747-
# Invoke the callback on the scopeState object
748627
from ..session import session_context
749628

750629
with session_context(self._proxy_session):
@@ -757,11 +636,83 @@ async def _scoped_on_bookmark(self, root_state: BookmarkState) -> None:
757636
raise ValueError("All scope values must be named.")
758637
root_state.values[str(self._ns(key))] = value
759638

639+
def on_bookmark(
640+
self,
641+
callback: (
642+
Callable[[BookmarkState], None] | Callable[[BookmarkState], Awaitable[None]]
643+
),
644+
) -> CancelCallback:
645+
if self._on_bookmark_callbacks.count() == 0:
646+
# Register a proxy callback on the parent session
647+
self._root_bookmark.on_bookmark(self._scoped_on_bookmark)
648+
return super().on_bookmark(callback)
649+
760650
def on_bookmarked(
761651
self,
762652
callback: Callable[[str], None] | Callable[[str], Awaitable[None]],
763-
) -> NoReturn:
764-
raise RuntimeError("`on_bookmarked` is not supported in BookmarkProxy.")
653+
) -> CancelCallback:
654+
if self._on_bookmarked_callbacks.count() == 0:
655+
from ..session import session_context
656+
657+
# Register a proxy callback on the parent session
658+
@self._root_bookmark.on_bookmarked
659+
async def scoped_on_bookmarked(url: str) -> None:
660+
if self._on_bookmarked_callbacks.count() == 0:
661+
return
662+
with session_context(self._proxy_session):
663+
await self._on_bookmarked_callbacks.invoke(url)
664+
665+
return super().on_bookmarked(callback)
666+
667+
def on_restore(
668+
self,
669+
callback: (
670+
Callable[[RestoreState], None] | Callable[[RestoreState], Awaitable[None]]
671+
),
672+
) -> CancelCallback:
673+
674+
if self._on_restore_callbacks.count() == 0:
675+
from ..session import session_context
676+
677+
# Register a proxy callback on the parent session
678+
@self._root_bookmark.on_restore
679+
async def scoped_on_restore(restore_state: RestoreState) -> None:
680+
if self._on_restore_callbacks.count() == 0:
681+
return
682+
683+
scoped_restore_state = restore_state._state_within_namespace(
684+
self._ns_prefix
685+
)
686+
687+
with session_context(self._proxy_session):
688+
await self._on_restore_callbacks.invoke(scoped_restore_state)
689+
690+
return super().on_restore(callback)
691+
692+
def on_restored(
693+
self,
694+
callback: (
695+
Callable[[RestoreState], None] | Callable[[RestoreState], Awaitable[None]]
696+
),
697+
) -> CancelCallback:
698+
699+
if self._on_restored_callbacks.count() == 0:
700+
from ..session import session_context
701+
702+
# Register a proxy callback on the parent session
703+
@self._root_bookmark.on_restored
704+
async def scoped_on_restored(restore_state: RestoreState) -> None:
705+
if self._on_restored_callbacks.count() == 0:
706+
return
707+
708+
scoped_restore_state = restore_state._state_within_namespace(
709+
self._ns_prefix
710+
)
711+
712+
with session_context(self._proxy_session):
713+
await self._on_restored_callbacks.invoke(scoped_restore_state)
714+
715+
return super().on_restored(callback)
765716

766717
@property
767718
def store(self) -> BookmarkStore:

shiny/bookmark/_save_state.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,6 @@ def __init__(
4343
self.dir = None # This will be set by external functions.
4444
self.values = {}
4545

46-
self._always_exclude: list[str] = []
47-
4846
async def _call_on_save(self):
4947
# Allow user-supplied save function to do things like add state$values, or
5048
# save data to state dir.

0 commit comments

Comments
 (0)