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.
Files changed (27) hide show
  1. streamlit/__init__.py +27 -6
  2. streamlit/commands/execution_control.py +71 -10
  3. streamlit/components/v1/components.py +15 -2
  4. streamlit/delta_generator.py +3 -3
  5. streamlit/elements/dialog_decorator.py +59 -25
  6. streamlit/elements/json.py +11 -1
  7. streamlit/elements/widgets/chat.py +3 -2
  8. streamlit/elements/write.py +11 -2
  9. streamlit/errors.py +15 -0
  10. streamlit/runtime/app_session.py +15 -13
  11. streamlit/runtime/context.py +157 -0
  12. streamlit/runtime/forward_msg_queue.py +1 -0
  13. streamlit/runtime/fragment.py +129 -44
  14. streamlit/runtime/metrics_util.py +3 -1
  15. streamlit/runtime/scriptrunner/exec_code.py +16 -36
  16. streamlit/runtime/scriptrunner/script_requests.py +63 -22
  17. streamlit/runtime/scriptrunner/script_run_context.py +3 -3
  18. streamlit/runtime/scriptrunner/script_runner.py +27 -6
  19. streamlit/runtime/state/session_state.py +6 -2
  20. streamlit/runtime/state/widgets.py +20 -8
  21. streamlit/web/server/websocket_headers.py +9 -0
  22. {streamlit_nightly-1.36.1.dev20240714.dist-info → streamlit_nightly-1.36.1.dev20240716.dist-info}/METADATA +1 -1
  23. {streamlit_nightly-1.36.1.dev20240714.dist-info → streamlit_nightly-1.36.1.dev20240716.dist-info}/RECORD +27 -26
  24. {streamlit_nightly-1.36.1.dev20240714.data → streamlit_nightly-1.36.1.dev20240716.data}/scripts/streamlit.cmd +0 -0
  25. {streamlit_nightly-1.36.1.dev20240714.dist-info → streamlit_nightly-1.36.1.dev20240716.dist-info}/WHEEL +0 -0
  26. {streamlit_nightly-1.36.1.dev20240714.dist-info → streamlit_nightly-1.36.1.dev20240716.dist-info}/entry_points.txt +0 -0
  27. {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)
@@ -98,6 +98,7 @@ class ForwardMsgQueue:
98
98
  for msg in self._queue
99
99
  if msg.WhichOneof("type")
100
100
  in {
101
+ "new_session",
101
102
  "script_finished",
102
103
  "session_status_changed",
103
104
  "parent_message",
@@ -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.exec_code import exec_func_with_error_handling
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.experimental_fragment
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 clear(self) -> None:
67
- """Remove all fragments saved in this FragmentStorage."""
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
- return self._fragments[key]
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
- del self._fragments[key]
118
+ try:
119
+ del self._fragments[key]
120
+ except KeyError as e:
121
+ raise FragmentStorageKeyError(str(e))
94
122
 
95
- def clear(self) -> None:
96
- self._fragments.clear()
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, *, run_every: int | float | timedelta | str | 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__}{active_dg._get_delta_path_str()}".encode()
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.fragment_ids_this_run:
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
- else:
159
- # Otherwise, we must be in a full script run. We need to temporarily set
160
- # ctx.current_fragment_id so that elements corresponding to this
161
- # fragment get tagged with the appropriate ID. ctx.current_fragment_id
162
- # gets reset after the fragment function finishes running.
163
- ctx.current_fragment_id = fragment_id
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
- ctx.current_fragment_delta_path = (
180
- active_dg._cursor.delta_path if active_dg._cursor else []
181
- )
182
- result = non_optional_func(*args, **kwargs)
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
- return result
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
- # Wrap the fragment function in the same try-except block as in a normal
197
- # script_run so that for a main-app run (this execution) and a fragment-rerun
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("experimental_fragment")
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.experimental_fragment``. As an anology, "inflating balloons" is a
301
- slow process that happens outside of the fragment. "Releasing balloons" is
302
- a quick process that happens inside of the fragment.
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.experimental_fragment
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.experimental_fragment
336
- >>> def fragment():
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
- >>> fragment()
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.experimental_fragment
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(ctx.fragment_ids_this_run)
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
- if rerun_exception_data.fragment_id_queue:
98
- # This is a fragment-specific rerun, so we need to restore the stack
99
- ctx.cursors = original_cursors
100
- dg_stack.set(original_dg_stack)
101
- else:
102
- # If it is a full-app rerun, the stack needs to be refreshed.
103
- # We should land here when `st.rerun` is called from within a
104
- # fragment. Since we re-use the same thread, we have to clear the
105
- # stack or otherwise we might render the main app in the old
106
- # fragment's dg_stack.
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