11# TODO: barret - Set / Load SaveState for Connect. Ex: Connect https://github.com/posit-dev/connect/blob/8de330aec6a61cf21e160b5081d08a1d3d7e8129/R/connect.R#L915
2+ # Might need to have independent save/load functions to register to avoid a class constructor
23
3- import os
44import pickle
5- from abc import ABC , abstractmethod
65from pathlib import Path
76from typing import TYPE_CHECKING , Any , Awaitable , Callable
87from urllib .parse import urlencode as urllib_urlencode
98
109from .._utils import private_random_id
1110from ..reactive import isolate
12- from ._utils import is_hosted , to_json
11+ from ._bookmark_state import BookmarkState
12+ from ._utils import is_hosted , to_json_str
1313
1414if TYPE_CHECKING :
1515 from .. import Inputs
1616else :
1717 Inputs = Any
1818
1919
20- class SaveState (ABC ):
21- """
22- Class for saving and restoring state to/from disk.
23- """
24-
25- @abstractmethod
26- async def save_dir (
27- self ,
28- id : str ,
29- # write_files: Callable[[Path], Awaitable[None]],
30- ) -> Path :
31- """
32- Construct directory for saving state.
33-
34- Parameters
35- ----------
36- id
37- The unique identifier for the state.
38-
39- Returns
40- -------
41- Path
42- Directory location for saving state. This directory must exist.
43- """
44- # write_files
45- # A async function that writes the state to a serializable location. The method receives a path object and
46- ...
47-
48- @abstractmethod
49- async def load_dir (
50- self ,
51- id : str ,
52- # read_files: Callable[[Path], Awaitable[None]],
53- ) -> Path :
54- """
55- Construct directory for loading state.
56-
57- Parameters
58- ----------
59- id
60- The unique identifier for the state.
61-
62- Returns
63- -------
64- Path | None
65- Directory location for loading state. If `None`, state loading will be ignored. If a `Path`, the directory must exist.
66- """
67- ...
68-
69-
70- class SaveStateLocal (SaveState ):
71- """
72- Function wrappers for saving and restoring state to/from disk when running Shiny
73- locally.
74- """
75-
76- def _local_dir (self , id : str ) -> Path :
77- # Try to save/load from current working directory as we do not know where the
78- # app file is located
79- return Path (os .getcwd ()) / "shiny_bookmarks" / id
80-
81- async def save_dir (self , id : str ) -> Path :
82- state_dir = self ._local_dir (id )
83- if not state_dir .exists ():
84- state_dir .mkdir (parents = True )
85- return state_dir
86-
87- async def load_dir (self , id : str ) -> Path :
88- return self ._local_dir (id )
89-
90- # async def save(
91- # self,
92- # id: str,
93- # write_files: Callable[[Path], Awaitable[None]],
94- # ) -> None:
95- # state_dir = self._local_dir(id)
96- # if not state_dir.exists():
97- # state_dir.mkdir(parents=True)
98-
99- # await write_files(state_dir)
100-
101- # async def load(
102- # self,
103- # id: str,
104- # read_files: Callable[[Path], Awaitable[None]],
105- # ) -> None:
106- # await read_files(self._local_dir(id))
107- # await read_files(self._local_dir(id))
108-
109-
110- # #############################################################################
111-
112-
11320class ShinySaveState :
11421 # session: ?
11522 # * Would get us access to inputs, possibly app dir, registered on save / load classes (?), exclude
@@ -163,37 +70,13 @@ async def _save_state(self) -> str:
16370 """
16471 id = private_random_id (prefix = "" , bytes = 8 )
16572
166- # TODO: barret move code to single call location
167- # A function for saving the state object to disk, given a directory to save
168- # to.
169- async def save_state_to_dir (state_dir : Path ) -> None :
170- self .dir = state_dir
171-
172- await self ._call_on_save ()
173-
174- self ._exclude_bookmark_value ()
175-
176- input_values_json = await self .input ._serialize (
177- exclude = self .exclude ,
178- state_dir = self .dir ,
179- )
180- assert self .dir is not None
181- with open (self .dir / "input.pickle" , "wb" ) as f :
182- pickle .dump (input_values_json , f )
183-
184- if len (self .values ) > 0 :
185- with open (self .dir / "values.pickle" , "wb" ) as f :
186- pickle .dump (self .values , f )
187-
188- return
189-
19073 # Pass the saveState function to the save interface function, which will
19174 # invoke saveState after preparing the directory.
19275
19376 # TODO: FUTURE - Get the save interface from the session object?
19477 # Look for a save.interface function. This will be defined by the hosting
19578 # environment if it supports bookmarking.
196- save_interface_loaded : SaveState | None = None
79+ save_interface_loaded : BookmarkState | None = None
19780
19881 if save_interface_loaded is None :
19982 if is_hosted ():
@@ -203,15 +86,33 @@ async def save_state_to_dir(state_dir: Path) -> None:
20386 )
20487 else :
20588 # We're running Shiny locally.
206- save_interface_loaded = SaveStateLocal ()
89+ save_interface_loaded = BookmarkStateLocal ()
20790
208- if not isinstance (save_interface_loaded , SaveState ):
91+ if not isinstance (save_interface_loaded , BookmarkState ):
20992 raise TypeError (
210- "The save interface retrieved must be an instance of `shiny.bookmark.SaveState `."
93+ "The save interface retrieved must be an instance of `shiny.bookmark.BookmarkStateLocal `."
21194 )
21295
21396 save_dir = Path (await save_interface_loaded .save_dir (id ))
214- await save_state_to_dir (save_dir )
97+
98+ # Save the state to disk.
99+ self .dir = save_dir
100+ await self ._call_on_save ()
101+
102+ self ._exclude_bookmark_value ()
103+
104+ input_values_json = await self .input ._serialize (
105+ exclude = self .exclude ,
106+ state_dir = self .dir ,
107+ )
108+ assert self .dir is not None
109+ with open (self .dir / "input.pickle" , "wb" ) as f :
110+ pickle .dump (input_values_json , f )
111+
112+ if len (self .values ) > 0 :
113+ with open (self .dir / "values.pickle" , "wb" ) as f :
114+ pickle .dump (self .values , f )
115+ # End save to disk
215116
216117 # No need to encode URI component as it is only ascii characters.
217118 return f"_state_id_={ id } "
@@ -243,7 +144,12 @@ async def _encode_state(self) -> str:
243144
244145 # If any input values are present, add them.
245146 if len (input_values_serialized ) > 0 :
246- input_qs = urllib_urlencode (to_json (input_values_serialized ))
147+ input_qs = urllib_urlencode (
148+ {
149+ key : to_json_str (value )
150+ for key , value in input_values_serialized .items ()
151+ }
152+ )
247153
248154 qs_str_parts .append ("_inputs_&" )
249155 qs_str_parts .append (input_qs )
@@ -252,7 +158,10 @@ async def _encode_state(self) -> str:
252158 if len (qs_str_parts ) > 0 :
253159 qs_str_parts .append ("&" )
254160
255- values_qs = urllib_urlencode (to_json (self .values ))
161+ # print("\n\nself.values", self.values)
162+ values_qs = urllib_urlencode (
163+ {key : to_json_str (value ) for key , value in self .values .items ()}
164+ )
256165
257166 qs_str_parts .append ("_values_&" )
258167 qs_str_parts .append (values_qs )
0 commit comments