streamlit-nightly 1.36.1.dev20240713__py2.py3-none-any.whl → 1.36.1.dev20240715__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 (26) hide show
  1. streamlit/__init__.py +27 -6
  2. streamlit/commands/execution_control.py +71 -10
  3. streamlit/delta_generator.py +3 -3
  4. streamlit/elements/dialog_decorator.py +59 -25
  5. streamlit/elements/json.py +11 -1
  6. streamlit/elements/widgets/chat.py +3 -2
  7. streamlit/elements/write.py +11 -2
  8. streamlit/errors.py +15 -0
  9. streamlit/runtime/app_session.py +15 -13
  10. streamlit/runtime/context.py +157 -0
  11. streamlit/runtime/forward_msg_queue.py +1 -0
  12. streamlit/runtime/fragment.py +129 -44
  13. streamlit/runtime/metrics_util.py +3 -1
  14. streamlit/runtime/scriptrunner/exec_code.py +16 -36
  15. streamlit/runtime/scriptrunner/script_requests.py +63 -22
  16. streamlit/runtime/scriptrunner/script_run_context.py +3 -3
  17. streamlit/runtime/scriptrunner/script_runner.py +27 -6
  18. streamlit/runtime/state/session_state.py +6 -2
  19. streamlit/runtime/state/widgets.py +20 -8
  20. streamlit/web/server/websocket_headers.py +9 -0
  21. {streamlit_nightly-1.36.1.dev20240713.dist-info → streamlit_nightly-1.36.1.dev20240715.dist-info}/METADATA +1 -1
  22. {streamlit_nightly-1.36.1.dev20240713.dist-info → streamlit_nightly-1.36.1.dev20240715.dist-info}/RECORD +26 -25
  23. {streamlit_nightly-1.36.1.dev20240713.data → streamlit_nightly-1.36.1.dev20240715.data}/scripts/streamlit.cmd +0 -0
  24. {streamlit_nightly-1.36.1.dev20240713.dist-info → streamlit_nightly-1.36.1.dev20240715.dist-info}/WHEEL +0 -0
  25. {streamlit_nightly-1.36.1.dev20240713.dist-info → streamlit_nightly-1.36.1.dev20240715.dist-info}/entry_points.txt +0 -0
  26. {streamlit_nightly-1.36.1.dev20240713.dist-info → streamlit_nightly-1.36.1.dev20240715.dist-info}/top_level.txt +0 -0
streamlit/__init__.py CHANGED
@@ -72,7 +72,10 @@ from streamlit.delta_generator import (
72
72
  bottom_dg as _bottom_dg,
73
73
  )
74
74
 
75
- from streamlit.elements.dialog_decorator import dialog_decorator as _dialog_decorator
75
+ from streamlit.elements.dialog_decorator import (
76
+ dialog_decorator as _dialog_decorator,
77
+ experimental_dialog_decorator as _experimental_dialog_decorator,
78
+ )
76
79
  from streamlit.runtime.caching import (
77
80
  cache_resource as _cache_resource,
78
81
  cache_data as _cache_data,
@@ -81,9 +84,13 @@ from streamlit.runtime.caching import (
81
84
  from streamlit.runtime.connection_factory import (
82
85
  connection_factory as _connection,
83
86
  )
84
- from streamlit.runtime.fragment import fragment as _fragment
87
+ from streamlit.runtime.fragment import (
88
+ experimental_fragment as _experimental_fragment,
89
+ fragment as _fragment,
90
+ )
85
91
  from streamlit.runtime.metrics_util import gather_metrics as _gather_metrics
86
92
  from streamlit.runtime.secrets import secrets_singleton as _secrets_singleton
93
+ from streamlit.runtime.context import ContextProxy as _ContextProxy
87
94
  from streamlit.runtime.state import (
88
95
  SessionStateProxy as _SessionStateProxy,
89
96
  QueryParamsProxy as _QueryParamsProxy,
@@ -96,7 +103,6 @@ from streamlit.commands.experimental_query_params import (
96
103
 
97
104
  import streamlit.column_config as _column_config
98
105
 
99
-
100
106
  # Modules that the user should have access to. These are imported with the "as" syntax and the same name; note that renaming the import with "as" does not make it an explicit export.
101
107
  # In this case, you should import it with an underscore to make clear that it is internal and then assign it to a variable with the new intended name.
102
108
  # You can check the export behavior by running 'mypy --strict example_app.py', which disables implicit_reexport, where you use the respective command in the example_app.py Streamlit app.
@@ -126,7 +132,6 @@ def _update_logger() -> None:
126
132
  # in an alternative config.
127
133
  _config.on_config_parsed(_update_logger, True)
128
134
 
129
-
130
135
  secrets = _secrets_singleton
131
136
 
132
137
  # DeltaGenerator methods:
@@ -222,6 +227,8 @@ session_state = _SessionStateProxy()
222
227
 
223
228
  query_params = _QueryParamsProxy()
224
229
 
230
+ context = _ContextProxy()
231
+
225
232
  # Caching
226
233
  cache_data = _cache_data
227
234
  cache_resource = _cache_resource
@@ -234,9 +241,23 @@ column_config = _column_config
234
241
  # Connection
235
242
  connection = _connection
236
243
 
244
+ # Fragment and dialog
245
+ dialog = _dialog_decorator
246
+ fragment = _fragment
247
+
237
248
  # Experimental APIs
238
- experimental_dialog = _dialog_decorator
239
- experimental_fragment = _fragment
249
+ experimental_dialog = _deprecate_func_name(
250
+ _experimental_dialog_decorator,
251
+ "experimental_dialog",
252
+ "2025-01-01",
253
+ name_override="dialog",
254
+ )
255
+ experimental_fragment = _deprecate_func_name(
256
+ _experimental_fragment,
257
+ "experimental_fragment",
258
+ "2025-01-01",
259
+ name_override="fragment",
260
+ )
240
261
  experimental_user = _UserInfoProxy()
241
262
 
242
263
  _EXPERIMENTAL_QUERY_PARAMS_DEPRECATE_MSG = "Refer to our [docs page](https://docs.streamlit.io/develop/api-reference/caching-and-state/st.query_params) for more information."
@@ -15,7 +15,8 @@
15
15
  from __future__ import annotations
16
16
 
17
17
  import os
18
- from typing import Final, NoReturn
18
+ from itertools import dropwhile
19
+ from typing import Final, Literal, NoReturn
19
20
 
20
21
  import streamlit as st
21
22
  from streamlit.errors import NoSessionContext, StreamlitAPIException
@@ -23,7 +24,11 @@ from streamlit.file_util import get_main_script_directory, normalize_path_join
23
24
  from streamlit.logger import get_logger
24
25
  from streamlit.navigation.page import StreamlitPage
25
26
  from streamlit.runtime.metrics_util import gather_metrics
26
- from streamlit.runtime.scriptrunner import RerunData, get_script_run_ctx
27
+ from streamlit.runtime.scriptrunner import (
28
+ RerunData,
29
+ ScriptRunContext,
30
+ get_script_run_ctx,
31
+ )
27
32
 
28
33
  _LOGGER: Final = get_logger(__name__)
29
34
 
@@ -54,22 +59,76 @@ def stop() -> NoReturn: # type: ignore[misc]
54
59
  st.empty()
55
60
 
56
61
 
62
+ def _new_fragment_id_queue(
63
+ ctx: ScriptRunContext,
64
+ scope: Literal["app", "fragment"],
65
+ ) -> list[str]:
66
+ if scope == "app":
67
+ return []
68
+
69
+ else: # scope == "fragment"
70
+ curr_queue = (
71
+ ctx.script_requests.fragment_id_queue if ctx.script_requests else []
72
+ )
73
+
74
+ # If st.rerun(scope="fragment") is called during a full script run, we raise an
75
+ # exception. This occurs, of course, if st.rerun(scope="fragment") is called
76
+ # outside of a fragment, but it somewhat surprisingly occurs if it gets called
77
+ # from within a fragment during a run of the full script. While this behvior may
78
+ # be surprising, it seems somewhat reasonable given that the correct behavior of
79
+ # calling st.rerun(scope="fragment") in this situation is unclear to me:
80
+ # * Rerunning just the fragment immediately may cause weirdness down the line
81
+ # as any part of the script that occurs after the fragment will not be
82
+ # executed.
83
+ # * Waiting until the full script run completes before rerunning the fragment
84
+ # seems odd (even if we normally do this before running a fragment not
85
+ # triggered by st.rerun()) because it defers the execution of st.rerun().
86
+ # * Rerunning the full app feels incorrect as we're seemingly ignoring the
87
+ # `scope` argument.
88
+ # With these issues and given that it seems pretty unnatural to have a
89
+ # fragment-scoped rerun happen during a full script run to begin with, it seems
90
+ # reasonable to just disallow this completely for now.
91
+ if not curr_queue:
92
+ raise StreamlitAPIException(
93
+ 'scope="fragment" can only be specified from `@st.fragment`-decorated '
94
+ "functions during fragment reruns."
95
+ )
96
+
97
+ assert (
98
+ new_queue := list(
99
+ dropwhile(lambda x: x != ctx.current_fragment_id, curr_queue)
100
+ )
101
+ ), "Could not find current_fragment_id in fragment_id_queue. This should never happen."
102
+
103
+ return new_queue
104
+
105
+
57
106
  @gather_metrics("rerun")
58
- def rerun() -> NoReturn: # type: ignore[misc]
107
+ def rerun( # type: ignore[misc]
108
+ *, # The scope argument can only be passed via keyword.
109
+ scope: Literal["app", "fragment"] = "app",
110
+ ) -> NoReturn:
59
111
  """Rerun the script immediately.
60
112
 
61
113
  When ``st.rerun()`` is called, the script is halted - no more statements will
62
- be run, and the script will be queued to re-run from the top.
114
+ be run, and the script will be queued to re-run.
115
+
116
+ Parameters
117
+ ----------
118
+ scope : Literal["app", "fragment"]
119
+ Specifies what part of the app should rerun. Setting scope="app" reruns the
120
+ full script; scope="fragment" limits the rerun to only the fragment from which
121
+ this function is called. If unspecified, defaults to "app".
122
+
123
+ Note that setting scope="fragment" is only valid
124
+ * inside of a fragment
125
+ * *not* during a full script run.
126
+ Passing this argument to ``st.rerun()`` from outside of a fragment or within a
127
+ fragment but when the full script is running raises a ``StreamlitAPIException``.
63
128
  """
64
129
 
65
130
  ctx = get_script_run_ctx()
66
131
 
67
- # TODO: (rerun[scope] project): in order to make it a fragment-scoped rerun, pass
68
- # the fragment_id_queue to the RerunData object and add the following line:
69
- # fragment_id_queue=[ctx.current_fragment_id] if scope == "fragment" else []
70
- # The script_runner RerunException is checking for the fragment_id_queue to decide
71
- # whether or not to reset the dg_stack.
72
-
73
132
  if ctx and ctx.script_requests:
74
133
  query_string = ctx.query_string
75
134
  page_script_hash = ctx.page_script_hash
@@ -78,6 +137,8 @@ def rerun() -> NoReturn: # type: ignore[misc]
78
137
  RerunData(
79
138
  query_string=query_string,
80
139
  page_script_hash=page_script_hash,
140
+ fragment_id_queue=_new_fragment_id_queue(ctx, scope),
141
+ is_fragment_scoped_rerun=True,
81
142
  )
82
143
  )
83
144
  # Force a yield point so the runner can do the rerun
@@ -441,9 +441,9 @@ class DeltaGenerator(
441
441
  ctx = get_script_run_ctx()
442
442
  if ctx and ctx.current_fragment_id and _writes_directly_to_sidebar(dg):
443
443
  raise StreamlitAPIException(
444
- "Calling `st.sidebar` in a function wrapped with `st.experimental_fragment` "
445
- "is not supported. To write elements to the sidebar with a fragment, "
446
- "call your fragment function inside a `with st.sidebar` context manager."
444
+ "Calling `st.sidebar` in a function wrapped with `st.fragment` is not "
445
+ "supported. To write elements to the sidebar with a fragment, call your "
446
+ "fragment function inside a `with st.sidebar` context manager."
447
447
  )
448
448
 
449
449
  # Warn if an element is being changed but the user isn't running the streamlit server.
@@ -28,11 +28,15 @@ if TYPE_CHECKING:
28
28
 
29
29
  def _assert_no_nested_dialogs() -> None:
30
30
  """Check the current stack for existing DeltaGenerator's of type 'dialog'.
31
- Note that the check like this only works when Dialog is called as a context manager, as this populates the dg_stack in delta_generator correctly.
31
+ Note that the check like this only works when Dialog is called as a context manager,
32
+ as this populates the dg_stack in delta_generator correctly.
32
33
 
33
- This does not detect the edge case in which someone calls, for example, `with st.sidebar` inside of a dialog function and opens a dialog in there,
34
- as `with st.sidebar` pushes the new DeltaGenerator to the stack. In order to check for that edge case, we could try to check all DeltaGenerators in the stack,
35
- and not only the last one. Since we deem this to be an edge case, we lean towards simplicity here.
34
+ This does not detect the edge case in which someone calls, for example,
35
+ `with st.sidebar` inside of a dialog function and opens a dialog in there, as
36
+ `with st.sidebar` pushes the new DeltaGenerator to the stack. In order to check for
37
+ that edge case, we could try to check all DeltaGenerators in the stack, and not only
38
+ the last one. Since we deem this to be an edge case, we lean towards simplicity
39
+ here.
36
40
 
37
41
  Raises
38
42
  ------
@@ -54,25 +58,34 @@ def _dialog_decorator(
54
58
  ) -> F:
55
59
  if title is None or title == "":
56
60
  raise StreamlitAPIException(
57
- 'A non-empty `title` argument has to be provided for dialogs, for example `@st.experimental_dialog("Example Title")`.'
61
+ "A non-empty `title` argument has to be provided for dialogs, for example "
62
+ '`@st.dialog("Example Title")`.'
58
63
  )
59
64
 
60
65
  @wraps(non_optional_func)
61
66
  def wrap(*args, **kwargs) -> None:
62
67
  _assert_no_nested_dialogs()
63
68
  # Call the Dialog on the event_dg because it lives outside of the normal
64
- # Streamlit UI flow. For example, if it is called from the sidebar, it should not
65
- # inherit the sidebar theming.
69
+ # Streamlit UI flow. For example, if it is called from the sidebar, it should
70
+ # not inherit the sidebar theming.
66
71
  dialog = event_dg._dialog(title=title, dismissible=True, width=width)
67
72
  dialog.open()
68
73
 
69
74
  def dialog_content() -> None:
70
- # if the dialog should be closed, st.rerun() has to be called (same behavior as with st.fragment)
75
+ # if the dialog should be closed, st.rerun() has to be called
76
+ # (same behavior as with st.fragment)
71
77
  _ = non_optional_func(*args, **kwargs)
72
78
  return None
73
79
 
74
- # the fragment decorator has multiple return types so that you can pass arguments to it. Here we know the return type, so we cast
75
- fragmented_dialog_content = cast(Callable[[], None], _fragment(dialog_content))
80
+ # the fragment decorator has multiple return types so that you can pass
81
+ # arguments to it. Here we know the return type, so we cast
82
+ fragmented_dialog_content = cast(
83
+ Callable[[], None],
84
+ _fragment(
85
+ dialog_content, additional_hash_info=non_optional_func.__qualname__
86
+ ),
87
+ )
88
+
76
89
  with dialog:
77
90
  fragmented_dialog_content()
78
91
  return None
@@ -86,21 +99,23 @@ def dialog_decorator(
86
99
  ) -> Callable[[F], F]: ...
87
100
 
88
101
 
89
- # 'title' can be a function since `dialog_decorator` is a decorator. We just call it 'title' here though
90
- # to make the user-doc more friendly as we want the user to pass a title, not a function.
91
- # The user is supposed to call it like @st.dialog("my_title") , which makes 'title' a positional arg, hence
92
- # this 'trick'. The overload is required to have a good type hint for the decorated function args.
102
+ # 'title' can be a function since `dialog_decorator` is a decorator.
103
+ # We just call it 'title' here though to make the user-doc more friendly as
104
+ # we want the user to pass a title, not a function. The user is supposed to
105
+ # call it like @st.dialog("my_title") , which makes 'title' a positional arg, hence
106
+ # this 'trick'. The overload is required to have a good type hint for the decorated
107
+ # function args.
93
108
  @overload
94
109
  def dialog_decorator(title: F, *, width: DialogWidth = "small") -> F: ...
95
110
 
96
111
 
97
- @gather_metrics("experimental_dialog")
112
+ @gather_metrics("dialog")
98
113
  def dialog_decorator(
99
114
  title: F | str, *, width: DialogWidth = "small"
100
115
  ) -> F | Callable[[F], F]:
101
116
  """Function decorator to create a modal dialog.
102
117
 
103
- A function decorated with ``@st.experimental_dialog`` becomes a dialog
118
+ A function decorated with ``@st.dialog`` becomes a dialog
104
119
  function. When you call a dialog function, Streamlit inserts a modal dialog
105
120
  into your app. Streamlit element commands called within the dialog function
106
121
  render inside the modal dialog.
@@ -115,7 +130,7 @@ def dialog_decorator(
115
130
  dialog programmatically, call ``st.rerun()`` explicitly inside of the
116
131
  dialog function.
117
132
 
118
- ``st.experimental_dialog`` inherits behavior from |st.experimental_fragment|_.
133
+ ``st.dialog`` inherits behavior from |st.fragment|_.
119
134
  When a user interacts with an input widget created inside a dialog function,
120
135
  Streamlit only reruns the dialog function instead of the full script.
121
136
 
@@ -128,13 +143,10 @@ def dialog_decorator(
128
143
 
129
144
  .. warning::
130
145
  Only one dialog function may be called in a script run, which means
131
- that only one dialog can be open at any given time. Since a dialog is
132
- also a fragment, all fragment limitations apply. Dialogs can't contain
133
- fragments, and fragments can't contain dialogs. Using dialogs in widget
134
- callback functions is not supported.
146
+ that only one dialog can be open at any given time.
135
147
 
136
- .. |st.experimental_fragment| replace:: ``st.experimental_fragment``
137
- .. _st.experimental_fragment: https://docs.streamlit.io/develop/api-reference/execution-flow/st.fragment
148
+ .. |st.fragment| replace:: ``st.fragment``
149
+ .. _st.fragment: https://docs.streamlit.io/develop/api-reference/execution-flow/st.fragment
138
150
 
139
151
  Parameters
140
152
  ----------
@@ -147,7 +159,7 @@ def dialog_decorator(
147
159
 
148
160
  Examples
149
161
  --------
150
- The following example demonstrates the basic usage of ``@st.experimental_dialog``.
162
+ The following example demonstrates the basic usage of ``@st.dialog``.
151
163
  In this app, clicking "**A**" or "**B**" will open a modal dialog and prompt you
152
164
  to enter a reason for your vote. In the modal dialog, click "**Submit**" to record
153
165
  your vote into Session State and rerun the app. This will close the modal dialog
@@ -155,7 +167,7 @@ def dialog_decorator(
155
167
 
156
168
  >>> import streamlit as st
157
169
  >>>
158
- >>> @st.experimental_dialog("Cast your vote")
170
+ >>> @st.dialog("Cast your vote")
159
171
  >>> def vote(item):
160
172
  >>> st.write(f"Why is {item} your favorite?")
161
173
  >>> reason = st.text_input("Because...")
@@ -189,3 +201,25 @@ def dialog_decorator(
189
201
 
190
202
  func: F = func_or_title
191
203
  return _dialog_decorator(func, "", width=width)
204
+
205
+
206
+ @overload
207
+ def experimental_dialog_decorator(
208
+ title: str, *, width: DialogWidth = "small"
209
+ ) -> Callable[[F], F]: ...
210
+
211
+
212
+ # 'title' can be a function since `dialog_decorator` is a decorator. We just call it
213
+ # 'title' here though to make the user-doc more friendly as we want the user to pass a
214
+ # title, not a function. The user is supposed to call it like @st.dialog("my_title"),
215
+ # which makes 'title' a positional arg, hence this 'trick'. The overload is required to
216
+ # have a good type hint for the decorated function args.
217
+ @overload
218
+ def experimental_dialog_decorator(title: F, *, width: DialogWidth = "small") -> F: ...
219
+
220
+
221
+ @gather_metrics("experimental_dialog")
222
+ def experimental_dialog_decorator(
223
+ title: F | str, *, width: DialogWidth = "small"
224
+ ) -> F | Callable[[F], F]:
225
+ return dialog_decorator(title, width=width)
@@ -18,6 +18,7 @@ import json
18
18
  from typing import TYPE_CHECKING, Any, cast
19
19
 
20
20
  from streamlit.proto.Json_pb2 import Json as JsonProto
21
+ from streamlit.runtime.context import StreamlitCookies, StreamlitHeaders
21
22
  from streamlit.runtime.metrics_util import gather_metrics
22
23
  from streamlit.runtime.state import QueryParamsProxy, SessionStateProxy
23
24
  from streamlit.user_info import UserInfoProxy
@@ -78,7 +79,16 @@ class JsonMixin:
78
79
  """
79
80
  import streamlit as st
80
81
 
81
- if isinstance(body, (SessionStateProxy, UserInfoProxy, QueryParamsProxy)):
82
+ if isinstance(
83
+ body,
84
+ (
85
+ SessionStateProxy,
86
+ UserInfoProxy,
87
+ QueryParamsProxy,
88
+ StreamlitHeaders,
89
+ StreamlitCookies,
90
+ ),
91
+ ):
82
92
  body = body.to_dict()
83
93
 
84
94
  if not isinstance(body, str):
@@ -293,8 +293,9 @@ class ChatMixin:
293
293
  height: 350px
294
294
 
295
295
  The chat input can also be used inline by nesting it inside any layout
296
- container (container, columns, tabs, sidebar, etc). Create chat
297
- interfaces embedded next to other content or have multiple chat bots!
296
+ container (container, columns, tabs, sidebar, etc) or fragment. Create
297
+ chat interfaces embedded next to other content or have multiple
298
+ chatbots!
298
299
 
299
300
  >>> import streamlit as st
300
301
  >>>
@@ -24,6 +24,7 @@ from typing import TYPE_CHECKING, Any, Callable, Final, Generator, Iterable, Lis
24
24
  from streamlit import dataframe_util, type_util
25
25
  from streamlit.errors import StreamlitAPIException
26
26
  from streamlit.logger import get_logger
27
+ from streamlit.runtime.context import StreamlitCookies, StreamlitHeaders
27
28
  from streamlit.runtime.metrics_util import gather_metrics
28
29
  from streamlit.runtime.state import QueryParamsProxy, SessionStateProxy
29
30
  from streamlit.string_util import (
@@ -36,7 +37,6 @@ from streamlit.user_info import UserInfoProxy
36
37
  if TYPE_CHECKING:
37
38
  from streamlit.delta_generator import DeltaGenerator
38
39
 
39
-
40
40
  # Special methods:
41
41
  HELP_TYPES: Final[tuple[type[Any], ...]] = (
42
42
  types.BuiltinFunctionType,
@@ -441,7 +441,16 @@ class WriteMixin:
441
441
  dot = vis_utils.model_to_dot(arg)
442
442
  self.dg.graphviz_chart(dot.to_string())
443
443
  elif isinstance(
444
- arg, (dict, list, SessionStateProxy, UserInfoProxy, QueryParamsProxy)
444
+ arg,
445
+ (
446
+ dict,
447
+ list,
448
+ SessionStateProxy,
449
+ UserInfoProxy,
450
+ QueryParamsProxy,
451
+ StreamlitHeaders,
452
+ StreamlitCookies,
453
+ ),
445
454
  ):
446
455
  flush_buffer()
447
456
  self.dg.json(arg)
streamlit/errors.py CHANGED
@@ -38,6 +38,21 @@ class DeprecationError(Error):
38
38
  pass
39
39
 
40
40
 
41
+ class FragmentStorageKeyError(Error, KeyError):
42
+ """A KeyError raised when a KeyError is encountered during a FragmentStorage
43
+ operation."""
44
+
45
+ pass
46
+
47
+
48
+ class FragmentHandledException(Exception):
49
+ """An exception that is raised by the fragment
50
+ when it has handled the exception itself.
51
+ """
52
+
53
+ pass
54
+
55
+
41
56
  class NoStaticFiles(Error):
42
57
  pass
43
58
 
@@ -122,6 +122,7 @@ class AppSession:
122
122
  service that a Streamlit Runtime is running in wants to tie the lifecycle of
123
123
  a Streamlit session to some other session-like object that it manages.
124
124
  """
125
+
125
126
  # Each AppSession has a unique string ID.
126
127
  self.id = session_id_override or str(uuid.uuid4())
127
128
 
@@ -361,7 +362,7 @@ class AppSession:
361
362
  client_state.widget_states,
362
363
  client_state.page_script_hash,
363
364
  client_state.page_name,
364
- fragment_id_queue=[fragment_id] if fragment_id else [],
365
+ fragment_id=fragment_id if fragment_id else None,
365
366
  )
366
367
  else:
367
368
  rerun_data = RerunData()
@@ -369,7 +370,7 @@ class AppSession:
369
370
  if self._scriptrunner is not None:
370
371
  if (
371
372
  bool(config.get_option("runner.fastReruns"))
372
- and not rerun_data.fragment_id_queue
373
+ and not rerun_data.fragment_id
373
374
  ):
374
375
  # If fastReruns is enabled and this is *not* a rerun of a fragment,
375
376
  # we don't send rerun requests to our existing ScriptRunner. Instead, we
@@ -477,8 +478,9 @@ class AppSession:
477
478
  exception: BaseException | None = None,
478
479
  client_state: ClientState | None = None,
479
480
  page_script_hash: str | None = None,
480
- fragment_ids_this_run: set[str] | None = None,
481
+ fragment_ids_this_run: list[str] | None = None,
481
482
  pages: dict[PageHash, PageInfo] | None = None,
483
+ clear_forward_msg_queue: bool = True,
482
484
  ) -> None:
483
485
  """Called when our ScriptRunner emits an event.
484
486
 
@@ -496,6 +498,7 @@ class AppSession:
496
498
  page_script_hash,
497
499
  fragment_ids_this_run,
498
500
  pages,
501
+ clear_forward_msg_queue,
499
502
  )
500
503
  )
501
504
 
@@ -507,8 +510,9 @@ class AppSession:
507
510
  exception: BaseException | None = None,
508
511
  client_state: ClientState | None = None,
509
512
  page_script_hash: str | None = None,
510
- fragment_ids_this_run: set[str] | None = None,
513
+ fragment_ids_this_run: list[str] | None = None,
511
514
  pages: dict[PageHash, PageInfo] | None = None,
515
+ clear_forward_msg_queue: bool = True,
512
516
  ) -> None:
513
517
  """Handle a ScriptRunner event.
514
518
 
@@ -540,10 +544,14 @@ class AppSession:
540
544
  A hash of the script path corresponding to the page currently being
541
545
  run. Set only for the SCRIPT_STARTED event.
542
546
 
543
- fragment_ids_this_run : set[str] | None
547
+ fragment_ids_this_run : list[str] | None
544
548
  The fragment IDs of the fragments being executed in this script run. Only
545
549
  set for the SCRIPT_STARTED event. If this value is falsy, this script run
546
550
  must be for the full script.
551
+
552
+ clear_forward_msg_queue : bool
553
+ If set (the default), clears the queue of forward messages to be sent to the
554
+ browser. Set only for the SCRIPT_STARTED event.
547
555
  """
548
556
 
549
557
  assert (
@@ -569,13 +577,7 @@ class AppSession:
569
577
  page_script_hash is not None
570
578
  ), "page_script_hash must be set for the SCRIPT_STARTED event"
571
579
 
572
- # When running the full script, we clear the browser ForwardMsg queue since
573
- # anything from a previous script run that has yet to be sent to the browser
574
- # will be overwritten. For fragment runs, however, we don't want to do this
575
- # as the ForwardMsgs in the queue may not correspond to the running
576
- # fragment, so dropping the messages may result in the app missing
577
- # information.
578
- if not fragment_ids_this_run:
580
+ if clear_forward_msg_queue:
579
581
  self._clear_queue()
580
582
 
581
583
  self._enqueue_forward_msg(
@@ -677,7 +679,7 @@ class AppSession:
677
679
  def _create_new_session_message(
678
680
  self,
679
681
  page_script_hash: str,
680
- fragment_ids_this_run: set[str] | None = None,
682
+ fragment_ids_this_run: list[str] | None = None,
681
683
  pages: dict[PageHash, PageInfo] | None = None,
682
684
  ) -> ForwardMsg:
683
685
  """Create and return a new_session ForwardMsg."""