1
1
# 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
2
3
3
- import os
4
4
import pickle
5
- from abc import ABC , abstractmethod
6
5
from pathlib import Path
7
6
from typing import TYPE_CHECKING , Any , Awaitable , Callable
8
7
from urllib .parse import urlencode as urllib_urlencode
9
8
10
9
from .._utils import private_random_id
11
10
from ..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
13
13
14
14
if TYPE_CHECKING :
15
15
from .. import Inputs
16
16
else :
17
17
Inputs = Any
18
18
19
19
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
-
113
20
class ShinySaveState :
114
21
# session: ?
115
22
# * 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:
163
70
"""
164
71
id = private_random_id (prefix = "" , bytes = 8 )
165
72
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
-
190
73
# Pass the saveState function to the save interface function, which will
191
74
# invoke saveState after preparing the directory.
192
75
193
76
# TODO: FUTURE - Get the save interface from the session object?
194
77
# Look for a save.interface function. This will be defined by the hosting
195
78
# environment if it supports bookmarking.
196
- save_interface_loaded : SaveState | None = None
79
+ save_interface_loaded : BookmarkState | None = None
197
80
198
81
if save_interface_loaded is None :
199
82
if is_hosted ():
@@ -203,15 +86,33 @@ async def save_state_to_dir(state_dir: Path) -> None:
203
86
)
204
87
else :
205
88
# We're running Shiny locally.
206
- save_interface_loaded = SaveStateLocal ()
89
+ save_interface_loaded = BookmarkStateLocal ()
207
90
208
- if not isinstance (save_interface_loaded , SaveState ):
91
+ if not isinstance (save_interface_loaded , BookmarkState ):
209
92
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 `."
211
94
)
212
95
213
96
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
215
116
216
117
# No need to encode URI component as it is only ascii characters.
217
118
return f"_state_id_={ id } "
@@ -243,7 +144,12 @@ async def _encode_state(self) -> str:
243
144
244
145
# If any input values are present, add them.
245
146
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
+ )
247
153
248
154
qs_str_parts .append ("_inputs_&" )
249
155
qs_str_parts .append (input_qs )
@@ -252,7 +158,10 @@ async def _encode_state(self) -> str:
252
158
if len (qs_str_parts ) > 0 :
253
159
qs_str_parts .append ("&" )
254
160
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
+ )
256
165
257
166
qs_str_parts .append ("_values_&" )
258
167
qs_str_parts .append (values_qs )
0 commit comments