Cope 2.5.0
My personal "standard library" of all the generally useful code I've written for various projects over the years
Loading...
Searching...
No Matches
streamlit.py
1"""
2Functions & classes that extend the streamlit library
3If ss is not found in this module, it means you don't have streamlit installed.
4"""
5__version__ = '1.0.0'
6
7from .misc import isiterable
8from .imports import lazy_import
9st = lazy_import('streamlit')
10
11# This is "tested" by Mathland, in which it is fully integrated
12
13# TODO add auto-page config options
14# TODO fix query params between switching pages
15# TODO _get_query_params only evaluates the first the first level of the container
16class SS:
17 """ A better streamlit.session_state interface.
18 This class is accessable via it's only instance, `ss`. Do not instantiate this class directly.
19 You can use `ss` as a drop-in replacement for st.session_state, and it will work.
20 Features:
21 - Variables are specified once with defaults, and can be reset and maintained across pages
22 via the reset() and maintain_state() methods, respectively. You can use square brackets or
23 the . operator to access and set variables, and it won't throw an error if it doesn't already
24 exist, instead it will return None.
25 - *_changed (named {varname}_changed) gets updated automatically
26 when the value gets set. The *_changed variable will not get reset upon the script rerunning
27 though, unless you call reset_changed() at the end of the script. It will also not detect
28 if the value was changed by widgets. To check if any variables were changed at *all*,
29 use the check_changed() method.
30 Note that *_changed doesn't ever get reset to False, unless a variable is manually set
31 to the same value it already has. To reset all the variables, call ss.reset_changed().
32 This is usually done at the end of a file and/or before any st.rerun() calls.
33 - `current_page` and `page_changed` variables store the filepath of the
34 page we're currently in, and if the page was just changed, respectively.
35 In order for the pages to work, you have to call either , update(), maintain_state() or
36 note_page() at the top of each page in your project that you want variables to be maintained in.
37 - maintain_state() will keep all the variables the same across pages. If arguements are
38 passed, it only maintains those. kwargs passed sets the given variables to those values.
39 - The `just_loaded` variable gets set to `True` only if it's the first time the page is loaded.
40 All other times it's set to `False`. Resetting the page counts as loading the page for
41 "the first time".
42 - Variables you want to keep track of via query parameters (so they get preserved between
43 page reloads, or for sharing URLs with people), you can do that by either specifying
44 names of the variables positionally to setup(), or by calling the add_query() method.
45 The setup() and add_query() methods will automatically load them on page startup, or
46 you can load them yourself on first run using `just_loaded`.
47 Current Limitations:
48 - Query params will not stay maintained across pages, if the varibles change between
49 pages.
50 - Using containers with query params behaves oddly sometimes. You may have to parse them
51 yourself using eval().
52 - TODO: auto-setting the page config on all pages
53 """
54
55 maintain = True
56 _dict = {}
57 _query = set()
58
59 def __init__(self):
60 """ The constructor. Not to be called by you. Stop it."""
61 st.session_state['ss'] = self
62 st.session_state['just_loaded'] = True
63 self.ensure_exist()
64
65 def __setitem__(self, name:str, value):
66 """ Handles setting the value, the {var}_changes, and the _prev_{var} variables """
67 if name not in st.session_state:
68 st.session_state[name] = value
69 st.session_state[name + '_changed'] = st.session_state[name] != value
70 st.session_state['_prev_' + name] = st.session_state[name]
71 st.session_state[name] = value
72 # If it's a variable we handle with query params
73 if name in self._query:
74 self._set_query_param(name, value)
75
76 def __setattr__(self, name:str, value):
77 """ Shadows __setitem__() """
78 self.__setitem__(name, value)
79
80 def __getitem__(self, name:str):
81 """ Handles getting variables from either session_state or query parameters """
82 queried = None
83 session = None
84
85 if name in self._query and name in st.query_params:
86 queried = self._get_query_param(name)
87
88 if name in st.session_state:
89 session = st.session_state[name]
90
91 # If we have both, we want to overwrite the query parameter with the session value.
92 # This is because:
93 # 1. First run variables are handled by add_query(), which takes all the values in the query
94 # parameters and adds them to the session state immediately
95 # 2. session state values may be edited by widgets without SS knowing. This ensures
96 # the query parameters remain up to date
97 # If we have a value in the session, and somehow don't have a query parameter, and we should,
98 # then add it
99 if session is not None and name in self._query:
100 self._set_query_param(name, session)
101 return session
102 # If we have a query parameter, and it's somehow not in the session_state, add it
103 elif session is None and queried is not None:
104 st.session_state[name] = queried
105 return queried
106
107 # return session or queried
108 return queried or session
109
110 def __getattr__(self, name:str):
111 """ shadows __getitem__() """
112 return self.__getitem__(name)
113
114 def __delitem__(self, name:str):
115 if name in self._dict:
116 del self._dict[name]
117 del st.session_state[name]
118
119 def __delattr__(self, name:str):
120 self.__delitem__(name)
121
122 def __contains__(self, name:str):
123 return name in st.session_state
124
125
126 def _set_query_param(self, name, to):
127 """ Handles how we serialize the _query params """
128 if isinstance(to, dict):
129 to = {repr(key): repr(val) for key, val in to.items()}
130 elif isinstance(to, (list, tuple, set)):
131 to = type(to)([repr(i) for i in to])
132 st.query_params[name] = repr(to)
133
134 def _get_query_param(self, name):
135 """ Handles how we deserialize the query params
136 TODO: this only evaluates the first level of containers
137 """
138 return eval(st.query_params[name])
139
140 def setup(self, *queries, **vars):
141 """ Pass default values of all the variables you're using.
142
143 If a variable is provided via positional args, and not keyword args, this sets it to be watched
144 by query parameters as well.
145 """
146 assert not len(set(queries) - set(vars.keys())), 'A query variable set isnt in the variables given with default values'
147 self._dict.update(vars)
148 self.add_query(*queries)
149 self.ensure_exist()
150
151 def ensure_exist(self):
152 """ Ensures that all the variables we're keeping track of exist in st.session_state, at least
153 with default values. This can be, but shouldn't need to be called by the user
154 """
155 for var, val in self._dict.items():
156 if var not in st.session_state:
157 st.session_state[var] = val
158 if var in self._query and var not in st.query_params:
159 self._set_query_param(var, st.session_state[var])
160 if var + '_changed' not in st.session_state:
161 st.session_state[var + '_changed'] = True
162 if '_prev_' + var not in st.session_state:
163 st.session_state['_prev_' + var] = None
164
165 def update(self, file):
166 """ Maintain variable states across files, and notes the page we're in """
167 self.note_page(file)
168 if self.maintain:
169 self.maintain_state()
170
171 def check_changed(self):
172 """ Checks if any variables have been changed by widgets, and if so, updates their associated
173 *_changed variable.
174 """
175 for var in self._dict.keys():
176 if var not in st.session_state:
177 st.session_state[var] = self._dict[var]
178 if '_prev_' + var not in st.session_state:
179 st.session_state['_prev_' + var] = None
180
181 st.session_state[var + '_changed'] = st.session_state[var] != st.session_state['_prev_' + var]
182 st.session_state['_prev_' + var] = st.session_state[var]
183
184 def maintain_state(self, *args, exclude=[], calls=1, **kwargs):
185 """ Maintain the current variables with the values they currently have across pages """
186 if len(args):
187 for var in args:
188 # Add the new var, if it's new
189 if var not in self._dict:
190 raise ValueError("Variable specified not already being watched. To add it, please use keyword arguments to specify a default.")
191 else:
192 st.session_state[var] = st.session_state[var]
193 else:
194 for var, val in self._dict.items():
195 if var in exclude:
196 continue
197 elif var not in st.session_state:
198 st.session_state[var] = val
199 else:
200 st.session_state[var] = st.session_state[var]
201 for var, val in kwargs.items():
202 # Add the new var, if it's new
203 if var not in self._dict:
204 self._dict[var] = val
205 st.session_state[var] = val
206
207 def note_page(self, file, calls=1):
208 """ Manually take note of what page we're running from """
209 self.check_changed()
210
211 # This causes some very strange loop thing
212 # file = st_javascript("await fetch('').then(r => window.parent.location.href)", key=now())
213
214 # This will *only* work in pages directly, not functions in other files
215 # This doesn't work, because the streamlit stack is what calls these things
216 # file = stack()[-calls].filename
217
218 if 'current_page' not in st.session_state:
219 st.session_state['current_page'] = file
220
221 # A quick alias cause they both make sense
222 st.session_state['current_page_changed'] = st.session_state['page_changed'] = \
223 st.session_state['current_page'] != file
224 st.session_state['current_page'] = file
225
226 def reset(self):
227 """ Reset all variables to their defaults """
228 for var, val in self._dict.items():
229 # The *_changed should stay accurate
230 if var in st.session_state:
231 st.session_state[var + '_changed'] = st.session_state[var] != val
232 else:
233 st.session_state[var + '_changed'] = True
234 # So prev reinitializes
235 # st.session_state[var] = val
236 self[var] = val
237
238 def reset_changed(self):
239 """ Sets all the *_changed variables to False """
240 for var in self._dict.keys():
241 st.session_state[var + '_changed'] = False
242 st.session_state['just_loaded'] = False
243
244 def get(self, name):
245 """ Available just for compatibility """
246 return self[name]
247
248 def watch(self, name:str, default):
249 """ Sets a single variable to be maintained and guarenteed to exist in st.session_state """
250 # If it's not already in st.session_state, add it
251 if name not in st.session_state:
252 st.session_state[name] = default
253 self._dict[name] = default
254
255
256 def add_query(self, *names):
257 """ Adds variables to be monitored with query parameters as well. Names must already be set
258 to be watched. This should only realistically be called once, in the main file.
259 """
260 for name in names:
261 if name not in self._dict:
262 raise ValueError(f"{name} not previously set to be watched, please provide a default.")
263 else:
264 self._query.add(name)
265 # If a query param is given (i.e. on first run via bookmark), we want to use *that* value
266 # and *not* the default.
267 # But we want it to *only* run once, the first time the page loads. Otherwise, we'll be
268 # resetting values set the previous run by widgets, which we don't want.
269 # This shouldn't be necissary?...
270 if 'just_loaded' not in st.session_state:
271 st.session_state['just_loaded'] = True
272
273 if st.session_state['just_loaded']:
274 for name in st.query_params.to_dict().keys():
275 st.session_state[name] = self._get_query_param(name)
276
277if st:
278 ss = SS()
def __setitem__(self, str name, value)
Handles setting the value, the {var}_changes, and the prev{var} variables.
Definition: streamlit.py:65
def __getitem__(self, str name)
Handles getting variables from either session_state or query parameters.
Definition: streamlit.py:80
def maintain_state(self, *args, exclude=[], calls=1, **kwargs)
Maintain the current variables with the values they currently have across pages.
Definition: streamlit.py:184
def reset(self)
Reset all variables to their defaults.
Definition: streamlit.py:226
def note_page(self, file, calls=1)
Manually take note of what page we're running from.
Definition: streamlit.py:207
def update(self, file)
Maintain variable states across files, and notes the page we're in.
Definition: streamlit.py:165
def __setattr__(self, str name, value)
Shadows setitem()
Definition: streamlit.py:76
def check_changed(self)
Checks if any variables have been changed by widgets, and if so, updates their associated *_changed v...
Definition: streamlit.py:171
def add_query(self, *names)
Adds variables to be monitored with query parameters as well.
Definition: streamlit.py:256
def _set_query_param(self, name, to)
Handles how we serialize the _query params.
Definition: streamlit.py:126
def reset_changed(self)
Sets all the *_changed variables to False.
Definition: streamlit.py:238
def __init__(self)
The constructor.
Definition: streamlit.py:59
def watch(self, str name, default)
Sets a single variable to be maintained and guarenteed to exist in st.session_state.
Definition: streamlit.py:248
def setup(self, *queries, **vars)
Pass default values of all the variables you're using.
Definition: streamlit.py:140
def __getattr__(self, str name)
shadows getitem()
Definition: streamlit.py:110
def __delitem__(self, str name)
Definition: streamlit.py:114
def get(self, name)
Available just for compatibility.
Definition: streamlit.py:244
def ensure_exist(self)
Ensures that all the variables we're keeping track of exist in st.session_state, at least with defaul...
Definition: streamlit.py:151
def _get_query_param(self, name)
Handles how we deserialize the query params TODO: this only evaluates the first level of containers.
Definition: streamlit.py:134
A better streamlit.session_state interface.
Definition: streamlit.py:16