streamlit-nightly 1.36.1.dev20240714__py2.py3-none-any.whl → 1.36.1.dev20240716__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- streamlit/__init__.py +27 -6
- streamlit/commands/execution_control.py +71 -10
- streamlit/components/v1/components.py +15 -2
- streamlit/delta_generator.py +3 -3
- streamlit/elements/dialog_decorator.py +59 -25
- streamlit/elements/json.py +11 -1
- streamlit/elements/widgets/chat.py +3 -2
- streamlit/elements/write.py +11 -2
- streamlit/errors.py +15 -0
- streamlit/runtime/app_session.py +15 -13
- streamlit/runtime/context.py +157 -0
- streamlit/runtime/forward_msg_queue.py +1 -0
- streamlit/runtime/fragment.py +129 -44
- streamlit/runtime/metrics_util.py +3 -1
- streamlit/runtime/scriptrunner/exec_code.py +16 -36
- streamlit/runtime/scriptrunner/script_requests.py +63 -22
- streamlit/runtime/scriptrunner/script_run_context.py +3 -3
- streamlit/runtime/scriptrunner/script_runner.py +27 -6
- streamlit/runtime/state/session_state.py +6 -2
- streamlit/runtime/state/widgets.py +20 -8
- streamlit/web/server/websocket_headers.py +9 -0
- {streamlit_nightly-1.36.1.dev20240714.dist-info → streamlit_nightly-1.36.1.dev20240716.dist-info}/METADATA +1 -1
- {streamlit_nightly-1.36.1.dev20240714.dist-info → streamlit_nightly-1.36.1.dev20240716.dist-info}/RECORD +27 -26
- {streamlit_nightly-1.36.1.dev20240714.data → streamlit_nightly-1.36.1.dev20240716.data}/scripts/streamlit.cmd +0 -0
- {streamlit_nightly-1.36.1.dev20240714.dist-info → streamlit_nightly-1.36.1.dev20240716.dist-info}/WHEEL +0 -0
- {streamlit_nightly-1.36.1.dev20240714.dist-info → streamlit_nightly-1.36.1.dev20240716.dist-info}/entry_points.txt +0 -0
- {streamlit_nightly-1.36.1.dev20240714.dist-info → streamlit_nightly-1.36.1.dev20240716.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,157 @@
|
|
1
|
+
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2024)
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
from __future__ import annotations
|
16
|
+
|
17
|
+
from functools import lru_cache
|
18
|
+
from types import MappingProxyType
|
19
|
+
from typing import TYPE_CHECKING, Any, Iterable, Iterator, Mapping, cast
|
20
|
+
|
21
|
+
from streamlit import runtime
|
22
|
+
from streamlit.runtime.metrics_util import gather_metrics
|
23
|
+
from streamlit.runtime.scriptrunner import get_script_run_ctx
|
24
|
+
from streamlit.type_util import is_type
|
25
|
+
|
26
|
+
if TYPE_CHECKING:
|
27
|
+
from http.cookies import Morsel
|
28
|
+
|
29
|
+
from tornado.httputil import HTTPHeaders, HTTPServerRequest
|
30
|
+
from tornado.web import RequestHandler
|
31
|
+
|
32
|
+
|
33
|
+
def _get_request() -> HTTPServerRequest | None:
|
34
|
+
ctx = get_script_run_ctx()
|
35
|
+
if ctx is None:
|
36
|
+
return None
|
37
|
+
|
38
|
+
session_client = runtime.get_instance().get_client(ctx.session_id)
|
39
|
+
if session_client is None:
|
40
|
+
return None
|
41
|
+
|
42
|
+
# We return websocket request only if session_client is an instance of
|
43
|
+
# BrowserWebSocketHandler (which is True for the Streamlit open-source
|
44
|
+
# implementation). For any other implementation, we return None.
|
45
|
+
if not is_type(
|
46
|
+
session_client,
|
47
|
+
"streamlit.web.server.browser_websocket_handler.BrowserWebSocketHandler",
|
48
|
+
):
|
49
|
+
return None
|
50
|
+
return cast("RequestHandler", session_client).request
|
51
|
+
|
52
|
+
|
53
|
+
@lru_cache
|
54
|
+
def _normalize_header(name: str) -> str:
|
55
|
+
"""Map a header name to Http-Header-Case.
|
56
|
+
|
57
|
+
>>> _normalize_header("coNtent-TYPE")
|
58
|
+
'Content-Type'
|
59
|
+
"""
|
60
|
+
return "-".join(w.capitalize() for w in name.split("-"))
|
61
|
+
|
62
|
+
|
63
|
+
class StreamlitHeaders(Mapping[str, str]):
|
64
|
+
def __init__(self, headers: Iterable[tuple[str, str]]):
|
65
|
+
dict_like_headers: dict[str, list[str]] = {}
|
66
|
+
|
67
|
+
for key, value in headers:
|
68
|
+
header_value = dict_like_headers.setdefault(_normalize_header(key), [])
|
69
|
+
header_value.append(value)
|
70
|
+
|
71
|
+
self._headers = dict_like_headers
|
72
|
+
|
73
|
+
@classmethod
|
74
|
+
def from_tornado_headers(cls, tornado_headers: HTTPHeaders) -> StreamlitHeaders:
|
75
|
+
return cls(tornado_headers.get_all())
|
76
|
+
|
77
|
+
def get_all(self, key: str) -> list[str]:
|
78
|
+
return list(self._headers.get(_normalize_header(key), []))
|
79
|
+
|
80
|
+
def __getitem__(self, key: str) -> str:
|
81
|
+
try:
|
82
|
+
return self._headers[_normalize_header(key)][0]
|
83
|
+
except LookupError:
|
84
|
+
raise KeyError(key) from None
|
85
|
+
|
86
|
+
def __len__(self) -> int:
|
87
|
+
"""Number of unique headers present in request."""
|
88
|
+
return len(self._headers)
|
89
|
+
|
90
|
+
def __iter__(self) -> Iterator[str]:
|
91
|
+
return iter(self._headers)
|
92
|
+
|
93
|
+
def to_dict(self) -> dict[str, str]:
|
94
|
+
return {key: self[key] for key in self}
|
95
|
+
|
96
|
+
|
97
|
+
class StreamlitCookies(Mapping[str, str]):
|
98
|
+
def __init__(self, cookies: Mapping[str, str]):
|
99
|
+
self._cookies = MappingProxyType(cookies)
|
100
|
+
|
101
|
+
@classmethod
|
102
|
+
def from_tornado_cookies(
|
103
|
+
cls, tornado_cookies: dict[str, Morsel[Any]]
|
104
|
+
) -> StreamlitCookies:
|
105
|
+
dict_like_cookies = {}
|
106
|
+
for key, morsel in tornado_cookies.items():
|
107
|
+
dict_like_cookies[key] = morsel.value
|
108
|
+
return cls(dict_like_cookies)
|
109
|
+
|
110
|
+
def __getitem__(self, key: str) -> str:
|
111
|
+
return self._cookies[key]
|
112
|
+
|
113
|
+
def __len__(self) -> int:
|
114
|
+
"""Number of unique headers present in request."""
|
115
|
+
return len(self._cookies)
|
116
|
+
|
117
|
+
def __iter__(self) -> Iterator[str]:
|
118
|
+
return iter(self._cookies)
|
119
|
+
|
120
|
+
def to_dict(self) -> dict[str, str]:
|
121
|
+
return dict(self._cookies)
|
122
|
+
|
123
|
+
|
124
|
+
class ContextProxy:
|
125
|
+
"""An interface to context about the current user session. Context is exposed as
|
126
|
+
properties on `st.context`, such as `st.context.headers` or `st.context.cookies`.
|
127
|
+
Each property description explains more about the contents."""
|
128
|
+
|
129
|
+
@property
|
130
|
+
@gather_metrics("context.headers")
|
131
|
+
def headers(self) -> StreamlitHeaders:
|
132
|
+
"""A read-only, dict-like access to headers sent in the initial request.
|
133
|
+
Keys are case-insensitive. Use get_all() to see all values if the same header
|
134
|
+
is set multiple times.
|
135
|
+
"""
|
136
|
+
# We have a docstring in line above as one-liner, to have a correct docstring
|
137
|
+
# in the st.write(st,context) call.
|
138
|
+
session_client_request = _get_request()
|
139
|
+
|
140
|
+
if session_client_request is None:
|
141
|
+
return StreamlitHeaders({})
|
142
|
+
|
143
|
+
return StreamlitHeaders.from_tornado_headers(session_client_request.headers)
|
144
|
+
|
145
|
+
@property
|
146
|
+
@gather_metrics("context.cookies")
|
147
|
+
def cookies(self) -> StreamlitCookies:
|
148
|
+
"""A read-only, dict-like access to cookies sent in the initial request."""
|
149
|
+
# We have a docstring in line above as one-liner, to have a correct docstring
|
150
|
+
# in the st.write(st,context) call.
|
151
|
+
session_client_request = _get_request()
|
152
|
+
|
153
|
+
if session_client_request is None:
|
154
|
+
return StreamlitCookies({})
|
155
|
+
|
156
|
+
cookies = session_client_request.cookies
|
157
|
+
return StreamlitCookies.from_tornado_cookies(cookies)
|
streamlit/runtime/fragment.py
CHANGED
@@ -22,22 +22,24 @@ from copy import deepcopy
|
|
22
22
|
from functools import wraps
|
23
23
|
from typing import TYPE_CHECKING, Any, Callable, Protocol, TypeVar, overload
|
24
24
|
|
25
|
+
from streamlit.error_util import handle_uncaught_app_exception
|
26
|
+
from streamlit.errors import FragmentHandledException, FragmentStorageKeyError
|
25
27
|
from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
|
26
28
|
from streamlit.runtime.metrics_util import gather_metrics
|
27
29
|
from streamlit.runtime.scriptrunner import get_script_run_ctx
|
28
|
-
from streamlit.runtime.scriptrunner.
|
30
|
+
from streamlit.runtime.scriptrunner.exceptions import RerunException, StopException
|
29
31
|
from streamlit.time_util import time_to_seconds
|
30
32
|
|
31
33
|
if TYPE_CHECKING:
|
32
34
|
from datetime import timedelta
|
33
35
|
|
36
|
+
|
34
37
|
F = TypeVar("F", bound=Callable[..., Any])
|
35
38
|
Fragment = Callable[[], Any]
|
36
39
|
|
37
40
|
|
38
41
|
class FragmentStorage(Protocol):
|
39
|
-
"""A key-value store for Fragments. Used to implement the @st.
|
40
|
-
decorator.
|
42
|
+
"""A key-value store for Fragments. Used to implement the @st.fragment decorator.
|
41
43
|
|
42
44
|
We intentionally define this as its own protocol despite how generic it appears to
|
43
45
|
be at first glance. The reason why is that, in any case where fragments aren't just
|
@@ -47,6 +49,14 @@ class FragmentStorage(Protocol):
|
|
47
49
|
protocols.
|
48
50
|
"""
|
49
51
|
|
52
|
+
# Weirdly, we have to define this above the `set` method, or mypy gets it confused
|
53
|
+
# with the `set` type of `new_fragments_ids`.
|
54
|
+
@abstractmethod
|
55
|
+
def clear(self, new_fragment_ids: set[str] | None = None) -> None:
|
56
|
+
"""Remove all fragments saved in this FragmentStorage unless listed in
|
57
|
+
new_fragment_ids."""
|
58
|
+
raise NotImplementedError
|
59
|
+
|
50
60
|
@abstractmethod
|
51
61
|
def get(self, key: str) -> Fragment:
|
52
62
|
"""Returns the stored fragment for the given key."""
|
@@ -63,8 +73,8 @@ class FragmentStorage(Protocol):
|
|
63
73
|
raise NotImplementedError
|
64
74
|
|
65
75
|
@abstractmethod
|
66
|
-
def
|
67
|
-
"""
|
76
|
+
def contains(self, key: str) -> bool:
|
77
|
+
"""Return whether the given key is present in this FragmentStorage."""
|
68
78
|
raise NotImplementedError
|
69
79
|
|
70
80
|
|
@@ -83,21 +93,42 @@ class MemoryFragmentStorage(FragmentStorage):
|
|
83
93
|
def __init__(self):
|
84
94
|
self._fragments: dict[str, Fragment] = {}
|
85
95
|
|
96
|
+
# Weirdly, we have to define this above the `set` method, or mypy gets it confused
|
97
|
+
# with the `set` type of `new_fragments_ids`.
|
98
|
+
def clear(self, new_fragment_ids: set[str] | None = None) -> None:
|
99
|
+
if new_fragment_ids is None:
|
100
|
+
new_fragment_ids = set()
|
101
|
+
|
102
|
+
fragment_ids = list(self._fragments.keys())
|
103
|
+
|
104
|
+
for fid in fragment_ids:
|
105
|
+
if fid not in new_fragment_ids:
|
106
|
+
del self._fragments[fid]
|
107
|
+
|
86
108
|
def get(self, key: str) -> Fragment:
|
87
|
-
|
109
|
+
try:
|
110
|
+
return self._fragments[key]
|
111
|
+
except KeyError as e:
|
112
|
+
raise FragmentStorageKeyError(str(e))
|
88
113
|
|
89
114
|
def set(self, key: str, value: Fragment) -> None:
|
90
115
|
self._fragments[key] = value
|
91
116
|
|
92
117
|
def delete(self, key: str) -> None:
|
93
|
-
|
118
|
+
try:
|
119
|
+
del self._fragments[key]
|
120
|
+
except KeyError as e:
|
121
|
+
raise FragmentStorageKeyError(str(e))
|
94
122
|
|
95
|
-
def
|
96
|
-
self._fragments
|
123
|
+
def contains(self, key: str) -> bool:
|
124
|
+
return key in self._fragments
|
97
125
|
|
98
126
|
|
99
127
|
def _fragment(
|
100
|
-
func: F | None = None,
|
128
|
+
func: F | None = None,
|
129
|
+
*,
|
130
|
+
run_every: int | float | timedelta | str | None = None,
|
131
|
+
additional_hash_info: str = "",
|
101
132
|
) -> Callable[[F], F] | F:
|
102
133
|
"""Contains the actual fragment logic.
|
103
134
|
|
@@ -128,10 +159,9 @@ def _fragment(
|
|
128
159
|
|
129
160
|
cursors_snapshot = deepcopy(ctx.cursors)
|
130
161
|
dg_stack_snapshot = deepcopy(dg_stack.get())
|
131
|
-
active_dg = dg_stack_snapshot[-1]
|
132
162
|
h = hashlib.new("md5")
|
133
163
|
h.update(
|
134
|
-
f"{non_optional_func.__module__}.{non_optional_func.__qualname__}{
|
164
|
+
f"{non_optional_func.__module__}.{non_optional_func.__qualname__}{dg_stack_snapshot[-1]._get_delta_path_str()}{additional_hash_info}".encode()
|
135
165
|
)
|
136
166
|
fragment_id = h.hexdigest()
|
137
167
|
|
@@ -149,18 +179,22 @@ def _fragment(
|
|
149
179
|
ctx = get_script_run_ctx(suppress_warning=True)
|
150
180
|
assert ctx is not None
|
151
181
|
|
152
|
-
if ctx.
|
182
|
+
if ctx.script_requests and ctx.script_requests.fragment_id_queue:
|
153
183
|
# This script run is a run of one or more fragments. We restore the
|
154
184
|
# state of ctx.cursors and dg_stack to the snapshots we took when this
|
155
185
|
# fragment was declared.
|
156
186
|
ctx.cursors = deepcopy(cursors_snapshot)
|
157
187
|
dg_stack.set(deepcopy(dg_stack_snapshot))
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
188
|
+
|
189
|
+
# Always add the fragment id to new_fragment_ids. For full app runs
|
190
|
+
# we need to add them anyways and for fragment runs we add them
|
191
|
+
# in case the to-be-executed fragment id was cleared from the storage
|
192
|
+
# by the full app run.
|
193
|
+
ctx.new_fragment_ids.add(fragment_id)
|
194
|
+
# Set ctx.current_fragment_id so that elements corresponding to this
|
195
|
+
# fragment get tagged with the appropriate ID. ctx.current_fragment_id gets
|
196
|
+
# reset after the fragment function finishes running.
|
197
|
+
ctx.current_fragment_id = fragment_id
|
164
198
|
|
165
199
|
try:
|
166
200
|
# Make sure we set the active script hash to the same value
|
@@ -174,18 +208,48 @@ def _fragment(
|
|
174
208
|
if initialized_active_script_hash != ctx.active_script_hash
|
175
209
|
else contextlib.nullcontext()
|
176
210
|
)
|
211
|
+
result = None
|
177
212
|
with active_hash_context:
|
178
213
|
with st.container():
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
214
|
+
try:
|
215
|
+
# use dg_stack instead of active_dg to have correct copy
|
216
|
+
# during execution (otherwise we can run into concurrency
|
217
|
+
# issues with multiple fragments). Use dg_stack because we
|
218
|
+
# just entered a container and [:-1] of the delta path
|
219
|
+
# because thats the prefix of the fragment,
|
220
|
+
# e.g. [0, 3, 0] -> [0, 3].
|
221
|
+
# All fragment elements start with [0, 3].
|
222
|
+
active_dg = dg_stack.get()[-1]
|
223
|
+
ctx.current_fragment_delta_path = (
|
224
|
+
active_dg._cursor.delta_path
|
225
|
+
if active_dg._cursor
|
226
|
+
else []
|
227
|
+
)[:-1]
|
228
|
+
result = non_optional_func(*args, **kwargs)
|
229
|
+
except (
|
230
|
+
RerunException,
|
231
|
+
StopException,
|
232
|
+
) as e:
|
233
|
+
# The wrapped_fragment function is executed
|
234
|
+
# inside of a exec_func_with_error_handling call, so
|
235
|
+
# there is a correct handler for these exceptions.
|
236
|
+
raise e
|
237
|
+
except Exception as e:
|
238
|
+
# render error here so that the delta path is correct
|
239
|
+
# for full app runs, the error will be displayed by the
|
240
|
+
# main code handler
|
241
|
+
# if not is_full_app_run:
|
242
|
+
handle_uncaught_app_exception(e)
|
243
|
+
# raise here again in case we are in full app execution
|
244
|
+
# and some flags have to be set
|
245
|
+
raise FragmentHandledException(e)
|
246
|
+
return result
|
183
247
|
finally:
|
184
248
|
ctx.current_fragment_id = None
|
249
|
+
ctx.current_fragment_delta_path = []
|
185
250
|
|
186
|
-
|
187
|
-
|
188
|
-
ctx.fragment_storage.set(fragment_id, wrapped_fragment)
|
251
|
+
if not ctx.fragment_storage.contains(fragment_id):
|
252
|
+
ctx.fragment_storage.set(fragment_id, wrapped_fragment)
|
189
253
|
|
190
254
|
if run_every:
|
191
255
|
msg = ForwardMsg()
|
@@ -193,15 +257,8 @@ def _fragment(
|
|
193
257
|
msg.auto_rerun.fragment_id = fragment_id
|
194
258
|
ctx.enqueue(msg)
|
195
259
|
|
196
|
-
#
|
197
|
-
|
198
|
-
# the same execution and error-handling logic is used. This makes errors in the
|
199
|
-
# fragment appear in the fragment path also for the first execution here in
|
200
|
-
# context of a full app run.
|
201
|
-
result, _, _, _ = exec_func_with_error_handling(
|
202
|
-
wrapped_fragment, ctx, reraise_rerun_exception=True
|
203
|
-
)
|
204
|
-
return result
|
260
|
+
# Immediate execute the wrapped fragment since we are in a full app run
|
261
|
+
return wrapped_fragment()
|
205
262
|
|
206
263
|
with contextlib.suppress(AttributeError):
|
207
264
|
# Make this a well-behaved decorator by preserving important function
|
@@ -230,7 +287,7 @@ def fragment(
|
|
230
287
|
) -> Callable[[F], F]: ...
|
231
288
|
|
232
289
|
|
233
|
-
@gather_metrics("
|
290
|
+
@gather_metrics("fragment")
|
234
291
|
def fragment(
|
235
292
|
func: F | None = None,
|
236
293
|
*,
|
@@ -297,14 +354,14 @@ def fragment(
|
|
297
354
|
Examples
|
298
355
|
--------
|
299
356
|
The following example demonstrates basic usage of
|
300
|
-
``@st.
|
301
|
-
|
302
|
-
|
357
|
+
``@st.fragment``. As an analogy, "inflating balloons" is a slow process that happens
|
358
|
+
outside of the fragment. "Releasing balloons" is a quick process that happens inside
|
359
|
+
of the fragment.
|
303
360
|
|
304
361
|
>>> import streamlit as st
|
305
362
|
>>> import time
|
306
363
|
>>>
|
307
|
-
>>> @st.
|
364
|
+
>>> @st.fragment
|
308
365
|
>>> def release_the_balloons():
|
309
366
|
>>> st.button("Release the balloons", help="Fragment rerun")
|
310
367
|
>>> st.balloons()
|
@@ -332,14 +389,14 @@ def fragment(
|
|
332
389
|
>>> st.session_state.app_runs = 0
|
333
390
|
>>> st.session_state.fragment_runs = 0
|
334
391
|
>>>
|
335
|
-
>>> @st.
|
336
|
-
>>> def
|
392
|
+
>>> @st.fragment
|
393
|
+
>>> def my_fragment():
|
337
394
|
>>> st.session_state.fragment_runs += 1
|
338
395
|
>>> st.button("Rerun fragment")
|
339
396
|
>>> st.write(f"Fragment says it ran {st.session_state.fragment_runs} times.")
|
340
397
|
>>>
|
341
398
|
>>> st.session_state.app_runs += 1
|
342
|
-
>>>
|
399
|
+
>>> my_fragment()
|
343
400
|
>>> st.button("Rerun full app")
|
344
401
|
>>> st.write(f"Full app says it ran {st.session_state.app_runs} times.")
|
345
402
|
>>> st.write(f"Full app sees that fragment ran {st.session_state.fragment_runs} times.")
|
@@ -356,7 +413,7 @@ def fragment(
|
|
356
413
|
>>> if "clicks" not in st.session_state:
|
357
414
|
>>> st.session_state.clicks = 0
|
358
415
|
>>>
|
359
|
-
>>> @st.
|
416
|
+
>>> @st.fragment
|
360
417
|
>>> def count_to_five():
|
361
418
|
>>> if st.button("Plus one!"):
|
362
419
|
>>> st.session_state.clicks += 1
|
@@ -376,3 +433,31 @@ def fragment(
|
|
376
433
|
|
377
434
|
"""
|
378
435
|
return _fragment(func, run_every=run_every)
|
436
|
+
|
437
|
+
|
438
|
+
@overload
|
439
|
+
def experimental_fragment(
|
440
|
+
func: F,
|
441
|
+
*,
|
442
|
+
run_every: int | float | timedelta | str | None = None,
|
443
|
+
) -> F: ...
|
444
|
+
|
445
|
+
|
446
|
+
# Support being able to pass parameters to this decorator (that is, being able to write
|
447
|
+
# `@fragment(run_every=5.0)`).
|
448
|
+
@overload
|
449
|
+
def experimental_fragment(
|
450
|
+
func: None = None,
|
451
|
+
*,
|
452
|
+
run_every: int | float | timedelta | str | None = None,
|
453
|
+
) -> Callable[[F], F]: ...
|
454
|
+
|
455
|
+
|
456
|
+
@gather_metrics("experimental_fragment")
|
457
|
+
def experimental_fragment(
|
458
|
+
func: F | None = None,
|
459
|
+
*,
|
460
|
+
run_every: int | float | timedelta | str | None = None,
|
461
|
+
) -> Callable[[F], F] | F:
|
462
|
+
"""Deprecated alias for @st.fragment. See the docstring for the decorator's new name."""
|
463
|
+
return _fragment(func, run_every=run_every)
|
@@ -482,6 +482,8 @@ def create_page_profile_message(
|
|
482
482
|
page_profile.uncaught_exception = uncaught_exception
|
483
483
|
|
484
484
|
if ctx := get_script_run_ctx():
|
485
|
-
page_profile.is_fragment_run = bool(
|
485
|
+
page_profile.is_fragment_run = bool(
|
486
|
+
ctx.script_requests and ctx.script_requests.fragment_id_queue
|
487
|
+
)
|
486
488
|
|
487
489
|
return msg
|
@@ -17,6 +17,7 @@ from __future__ import annotations
|
|
17
17
|
from typing import TYPE_CHECKING, Any, Callable
|
18
18
|
|
19
19
|
from streamlit.error_util import handle_uncaught_app_exception
|
20
|
+
from streamlit.errors import FragmentHandledException
|
20
21
|
from streamlit.runtime.scriptrunner.exceptions import RerunException, StopException
|
21
22
|
|
22
23
|
if TYPE_CHECKING:
|
@@ -25,10 +26,7 @@ if TYPE_CHECKING:
|
|
25
26
|
|
26
27
|
|
27
28
|
def exec_func_with_error_handling(
|
28
|
-
func: Callable[[], None],
|
29
|
-
ctx: ScriptRunContext,
|
30
|
-
*,
|
31
|
-
reraise_rerun_exception: bool = False,
|
29
|
+
func: Callable[[], None], ctx: ScriptRunContext
|
32
30
|
) -> tuple[Any | None, bool, RerunData | None, bool]:
|
33
31
|
"""Execute the passed function wrapped in a try/except block.
|
34
32
|
|
@@ -43,10 +41,6 @@ def exec_func_with_error_handling(
|
|
43
41
|
The function to execute wrapped in the try/except block.
|
44
42
|
ctx : ScriptRunContext
|
45
43
|
The context in which the script is being run.
|
46
|
-
reraise_rerun_exception : bool, default False
|
47
|
-
If True, an occuring RerunException will be raised instead of handled. This can
|
48
|
-
be used if this function is called outside of the script_run context and we want
|
49
|
-
the script_runner to react on the rerun exception.
|
50
44
|
|
51
45
|
Returns
|
52
46
|
-------
|
@@ -70,16 +64,6 @@ def exec_func_with_error_handling(
|
|
70
64
|
# is interrupted by a RerunException.
|
71
65
|
rerun_exception_data: RerunData | None = None
|
72
66
|
|
73
|
-
# Saving and restoring our original cursors/dg_stack is needed
|
74
|
-
# specifically to handle the case where a RerunException is raised while
|
75
|
-
# running a fragment. In this case, we need to restore both to their states
|
76
|
-
# at the start of the script run to ensure that we write to the correct
|
77
|
-
# places in the app during the rerun (without this, ctx.cursors and dg_stack
|
78
|
-
# will still be set to the snapshots they were restored from when running
|
79
|
-
# the fragment).
|
80
|
-
original_cursors = ctx.cursors
|
81
|
-
original_dg_stack = dg_stack.get()
|
82
|
-
|
83
67
|
# If the script stops early, we don't want to remove unseen widgets,
|
84
68
|
# so we track this to potentially skip session state cleanup later.
|
85
69
|
premature_stop: bool = False
|
@@ -90,22 +74,17 @@ def exec_func_with_error_handling(
|
|
90
74
|
try:
|
91
75
|
result = func()
|
92
76
|
except RerunException as e:
|
93
|
-
if reraise_rerun_exception:
|
94
|
-
raise e
|
95
|
-
|
96
77
|
rerun_exception_data = e.rerun_data
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
ctx.cursors.clear()
|
108
|
-
dg_stack.set(get_default_dg_stack())
|
78
|
+
|
79
|
+
# Since the script is about to rerun, we may need to reset our cursors/dg_stack
|
80
|
+
# so that we write to the right place in the app. For full script runs, this
|
81
|
+
# needs to happen in case the same thread reruns our script (a different thread
|
82
|
+
# would automatically come with fresh cursors/dg_stack values). For fragments,
|
83
|
+
# it doesn't matter either way since the fragment resets these values from its
|
84
|
+
# snapshot before execution.
|
85
|
+
ctx.cursors.clear()
|
86
|
+
dg_stack.set(get_default_dg_stack())
|
87
|
+
|
109
88
|
# Interruption due to a rerun is usually from `st.rerun()`, which
|
110
89
|
# we want to count as a script completion so triggers reset.
|
111
90
|
# It is also possible for this to happen if fast reruns is off,
|
@@ -116,11 +95,12 @@ def exec_func_with_error_handling(
|
|
116
95
|
# This is thrown when the script executes `st.stop()`.
|
117
96
|
# We don't have to do anything here.
|
118
97
|
premature_stop = True
|
119
|
-
|
98
|
+
except FragmentHandledException:
|
99
|
+
run_without_errors = False
|
100
|
+
premature_stop = True
|
120
101
|
except Exception as ex:
|
121
102
|
run_without_errors = False
|
122
|
-
uncaught_exception = ex
|
123
|
-
handle_uncaught_app_exception(uncaught_exception)
|
124
103
|
premature_stop = True
|
104
|
+
handle_uncaught_app_exception(ex)
|
125
105
|
|
126
106
|
return result, run_without_errors, rerun_exception_data, premature_stop
|