streamlit-nightly 1.32.3.dev20240326__py2.py3-none-any.whl → 1.32.3.dev20240329__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 (102) hide show
  1. streamlit/__init__.py +6 -3
  2. streamlit/components/v1/__init__.py +3 -17
  3. streamlit/components/v1/{custom_component.py → components.py} +159 -11
  4. streamlit/delta_generator.py +5 -0
  5. streamlit/elements/html.py +83 -0
  6. streamlit/elements/widgets/time_widgets.py +5 -21
  7. streamlit/errors.py +0 -6
  8. streamlit/proto/AutoRerun_pb2.py +25 -0
  9. streamlit/proto/AutoRerun_pb2.pyi +48 -0
  10. streamlit/proto/ClientState_pb2.py +3 -3
  11. streamlit/proto/ClientState_pb2.pyi +4 -1
  12. streamlit/proto/Delta_pb2.py +2 -2
  13. streamlit/proto/Delta_pb2.pyi +4 -1
  14. streamlit/proto/Element_pb2.py +4 -3
  15. streamlit/proto/Element_pb2.pyi +9 -4
  16. streamlit/proto/ForwardMsg_pb2.py +10 -9
  17. streamlit/proto/ForwardMsg_pb2.pyi +12 -3
  18. streamlit/proto/Html_pb2.py +26 -0
  19. streamlit/proto/Html_pb2.pyi +45 -0
  20. streamlit/proto/NewSession_pb2.py +24 -24
  21. streamlit/proto/NewSession_pb2.pyi +8 -1
  22. streamlit/proto/PageProfile_pb2.py +6 -6
  23. streamlit/proto/PageProfile_pb2.pyi +4 -1
  24. streamlit/runtime/app_session.py +67 -24
  25. streamlit/runtime/caching/cache_data_api.py +3 -3
  26. streamlit/runtime/caching/cache_resource_api.py +2 -2
  27. streamlit/runtime/fragment.py +239 -0
  28. streamlit/runtime/metrics_util.py +17 -9
  29. streamlit/runtime/runtime.py +6 -12
  30. streamlit/runtime/scriptrunner/script_requests.py +53 -37
  31. streamlit/runtime/scriptrunner/script_run_context.py +15 -2
  32. streamlit/runtime/scriptrunner/script_runner.py +63 -14
  33. streamlit/runtime/state/common.py +2 -0
  34. streamlit/runtime/state/session_state.py +51 -7
  35. streamlit/runtime/state/widgets.py +10 -2
  36. streamlit/static/asset-manifest.json +26 -24
  37. streamlit/static/index.html +1 -1
  38. streamlit/static/static/css/2411.8b8f33d6.chunk.css +1 -0
  39. streamlit/static/static/css/3092.95a45cfe.chunk.css +1 -0
  40. streamlit/static/static/css/43.e3b876c5.chunk.css +1 -0
  41. streamlit/static/static/js/1074.73973756.chunk.js +1 -0
  42. streamlit/static/static/js/1451.3b0a3e31.chunk.js +1 -0
  43. streamlit/static/static/js/1792.b8efa879.chunk.js +1 -0
  44. streamlit/static/static/js/2411.a8823789.chunk.js +2 -0
  45. streamlit/static/static/js/2634.1249dc7a.chunk.js +1 -0
  46. streamlit/static/static/js/2736.779ccbc1.chunk.js +2 -0
  47. streamlit/static/static/js/2736.779ccbc1.chunk.js.LICENSE.txt +60 -0
  48. streamlit/static/static/js/{3092.3d4df25e.chunk.js → 3092.ad569cc8.chunk.js} +1 -1
  49. streamlit/static/static/js/3513.e3e7300a.chunk.js +1 -0
  50. streamlit/static/static/js/4132.49bf3f2c.chunk.js.LICENSE.txt +4 -4
  51. streamlit/static/static/js/4177.69f9f18d.chunk.js +1 -0
  52. streamlit/static/static/js/4319.a6745434.chunk.js +1 -0
  53. streamlit/static/static/js/{4477.2555c11a.chunk.js → 4477.e10e4373.chunk.js} +1 -1
  54. streamlit/static/static/js/{4666.99f3abc3.chunk.js → 4666.b694c5a9.chunk.js} +1 -1
  55. streamlit/static/static/js/5106.44f0ff51.chunk.js +1 -0
  56. streamlit/static/static/js/5379.6571574f.chunk.js +1 -0
  57. streamlit/static/static/js/6013.8e80e091.chunk.js +1 -0
  58. streamlit/static/static/js/6718.802da17e.chunk.js +1 -0
  59. streamlit/static/static/js/7175.be4076bc.chunk.js +1 -0
  60. streamlit/static/static/js/{7602.f0420392.chunk.js → 7602.6175e969.chunk.js} +1 -1
  61. streamlit/static/static/js/{8492.e6dab83f.chunk.js → 8492.f56c9d4c.chunk.js} +1 -1
  62. streamlit/static/static/js/8691.9ccf7f89.chunk.js +1 -0
  63. streamlit/static/static/js/937.a1248039.chunk.js +2 -0
  64. streamlit/static/static/js/937.a1248039.chunk.js.LICENSE.txt +1 -0
  65. streamlit/static/static/js/main.356407e8.js +2 -0
  66. streamlit/testing/v1/local_script_runner.py +2 -0
  67. streamlit/time_util.py +88 -0
  68. streamlit/web/server/component_request_handler.py +2 -2
  69. streamlit/web/server/server.py +2 -1
  70. {streamlit_nightly-1.32.3.dev20240326.dist-info → streamlit_nightly-1.32.3.dev20240329.dist-info}/METADATA +1 -1
  71. {streamlit_nightly-1.32.3.dev20240326.dist-info → streamlit_nightly-1.32.3.dev20240329.dist-info}/RECORD +77 -73
  72. streamlit/components/lib/__init__.py +0 -13
  73. streamlit/components/lib/local_component_registry.py +0 -82
  74. streamlit/components/types/__init__.py +0 -13
  75. streamlit/components/types/base_component_registry.py +0 -98
  76. streamlit/components/types/base_custom_component.py +0 -137
  77. streamlit/components/v1/component_registry.py +0 -103
  78. streamlit/static/static/css/2411.81b3d18f.chunk.css +0 -1
  79. streamlit/static/static/css/3092.f719e2e6.chunk.css +0 -1
  80. streamlit/static/static/css/43.c24b25fa.chunk.css +0 -1
  81. streamlit/static/static/js/1074.71719df6.chunk.js +0 -1
  82. streamlit/static/static/js/1451.e3be1711.chunk.js +0 -1
  83. streamlit/static/static/js/1792.16c16498.chunk.js +0 -1
  84. streamlit/static/static/js/2411.b389bf4e.chunk.js +0 -2
  85. streamlit/static/static/js/2736.17fbad1a.chunk.js +0 -2
  86. streamlit/static/static/js/2736.17fbad1a.chunk.js.LICENSE.txt +0 -60
  87. streamlit/static/static/js/3513.57cff89c.chunk.js +0 -1
  88. streamlit/static/static/js/4177.ab9a7aa1.chunk.js +0 -1
  89. streamlit/static/static/js/4319.213fc321.chunk.js +0 -1
  90. streamlit/static/static/js/5106.22187bfc.chunk.js +0 -1
  91. streamlit/static/static/js/5379.e466522d.chunk.js +0 -1
  92. streamlit/static/static/js/6013.75c92264.chunk.js +0 -1
  93. streamlit/static/static/js/6718.97945fc6.chunk.js +0 -1
  94. streamlit/static/static/js/7175.8c1b4d38.chunk.js +0 -1
  95. streamlit/static/static/js/8691.24a5792f.chunk.js +0 -1
  96. streamlit/static/static/js/main.7fde7092.js +0 -2
  97. /streamlit/static/static/js/{2411.b389bf4e.chunk.js.LICENSE.txt → 2411.a8823789.chunk.js.LICENSE.txt} +0 -0
  98. /streamlit/static/static/js/{main.7fde7092.js.LICENSE.txt → main.356407e8.js.LICENSE.txt} +0 -0
  99. {streamlit_nightly-1.32.3.dev20240326.data → streamlit_nightly-1.32.3.dev20240329.data}/scripts/streamlit.cmd +0 -0
  100. {streamlit_nightly-1.32.3.dev20240326.dist-info → streamlit_nightly-1.32.3.dev20240329.dist-info}/WHEEL +0 -0
  101. {streamlit_nightly-1.32.3.dev20240326.dist-info → streamlit_nightly-1.32.3.dev20240329.dist-info}/entry_points.txt +0 -0
  102. {streamlit_nightly-1.32.3.dev20240326.dist-info → streamlit_nightly-1.32.3.dev20240329.dist-info}/top_level.txt +0 -0
@@ -15,7 +15,7 @@
15
15
  from __future__ import annotations
16
16
 
17
17
  import threading
18
- from dataclasses import dataclass
18
+ from dataclasses import dataclass, field
19
19
  from enum import Enum
20
20
  from typing import cast
21
21
 
@@ -46,6 +46,7 @@ class RerunData:
46
46
  widget_states: WidgetStates | None = None
47
47
  page_script_hash: str = ""
48
48
  page_name: str = ""
49
+ fragment_id_queue: list[str] = field(default_factory=list)
49
50
 
50
51
  def __repr__(self) -> str:
51
52
  return util.repr_(self)
@@ -103,41 +104,46 @@ class ScriptRequests:
103
104
  return False
104
105
 
105
106
  if self._state == ScriptRequestType.CONTINUE:
106
- # If we're running, we can handle a rerun request
107
- # unconditionally.
107
+ # The script is currently running, and we haven't received a request to
108
+ # rerun it as of yet. We can handle a rerun request unconditionally so
109
+ # just change self._state and set self._rerun_data.
108
110
  self._state = ScriptRequestType.RERUN
109
111
  self._rerun_data = new_data
110
112
  return True
111
113
 
112
114
  if self._state == ScriptRequestType.RERUN:
113
- # If we have an existing Rerun request, we coalesce this
114
- # new request into it.
115
- if self._rerun_data.widget_states is None:
116
- # The existing request's widget_states is None, which
117
- # means it wants to rerun with whatever the most
118
- # recent script execution's widget state was.
119
- # We have no meaningful state to merge with, and
120
- # so we simply overwrite the existing request.
121
- self._rerun_data = new_data
122
- return True
123
-
124
- if new_data.widget_states is not None:
125
- # Both the existing and the new request have
126
- # non-null widget_states. Merge them together.
127
- coalesced_states = coalesce_widget_states(
128
- self._rerun_data.widget_states, new_data.widget_states
129
- )
130
- self._rerun_data = RerunData(
131
- query_string=new_data.query_string,
132
- widget_states=coalesced_states,
133
- page_script_hash=new_data.page_script_hash,
134
- page_name=new_data.page_name,
135
- )
136
- return True
137
-
138
- # If old widget_states is NOT None, and new widget_states IS
139
- # None, then this new request is entirely redundant. Leave
140
- # our existing rerun_data as is.
115
+ # We already have an existing Rerun request, so we can coalesce the new
116
+ # rerun request into the existing one.
117
+
118
+ coalesced_states = coalesce_widget_states(
119
+ self._rerun_data.widget_states, new_data.widget_states
120
+ )
121
+
122
+ if new_data.fragment_id_queue:
123
+ # This RERUN request corresponds to a fragment run. We append the
124
+ # new fragment ID to the end of the current fragment_id_queue if it
125
+ # isn't already contained in it.
126
+ fragment_id_queue = [*self._rerun_data.fragment_id_queue]
127
+ if (
128
+ # new_data.fragment_id_queue is always a singleton
129
+ (new_fragment_id := new_data.fragment_id_queue[0])
130
+ not in fragment_id_queue
131
+ ):
132
+ fragment_id_queue.append(new_fragment_id)
133
+ else:
134
+ # Otherwise, this is a request to rerun the full script, so we want
135
+ # to clear out any fragments we have queued to run since they'll all
136
+ # be run with the full script anyway.
137
+ fragment_id_queue = []
138
+
139
+ self._rerun_data = RerunData(
140
+ query_string=new_data.query_string,
141
+ widget_states=coalesced_states,
142
+ page_script_hash=new_data.page_script_hash,
143
+ page_name=new_data.page_name,
144
+ fragment_id_queue=fragment_id_queue,
145
+ )
146
+
141
147
  return True
142
148
 
143
149
  # We'll never get here
@@ -146,22 +152,32 @@ class ScriptRequests:
146
152
  def on_scriptrunner_yield(self) -> ScriptRequest | None:
147
153
  """Called by the ScriptRunner when it's at a yield point.
148
154
 
149
- If we have no request, return None.
155
+ If we have no request or a RERUN request corresponding to one or more fragments,
156
+ return None.
150
157
 
151
- If we have a RERUN request, return the request and set our internal
158
+ If we have a (full script) RERUN request, return the request and set our internal
152
159
  state to CONTINUE.
153
160
 
154
161
  If we have a STOP request, return the request and remain stopped.
155
162
  """
156
- if self._state == ScriptRequestType.CONTINUE:
157
- # We avoid taking a lock in the common case. If a STOP or RERUN
158
- # request is received between the `if` and `return`, it will be
159
- # handled at the next `on_scriptrunner_yield`, or when
163
+ if self._state == ScriptRequestType.CONTINUE or (
164
+ # Reruns corresponding to fragments should *not* cancel the current script
165
+ # run as doing so will affect elements outside of the fragment.
166
+ self._state == ScriptRequestType.RERUN
167
+ and self._rerun_data.fragment_id_queue
168
+ ):
169
+ # We avoid taking the lock in the common cases of having no request and
170
+ # having a RERUN request corresponding to >=1 fragments. If a STOP or
171
+ # (full script) RERUN request is received between the `if` and `return`, it
172
+ # will be handled at the next `on_scriptrunner_yield`, or when
160
173
  # `on_scriptrunner_ready` is called.
161
174
  return None
162
175
 
163
176
  with self._lock:
164
177
  if self._state == ScriptRequestType.RERUN:
178
+ if self._rerun_data.fragment_id_queue:
179
+ return None
180
+
165
181
  self._state = ScriptRequestType.CONTINUE
166
182
  return ScriptRequest(ScriptRequestType.RERUN, self._rerun_data)
167
183
 
@@ -17,7 +17,7 @@ from __future__ import annotations
17
17
  import collections
18
18
  import threading
19
19
  from dataclasses import dataclass, field
20
- from typing import Callable, Counter, Dict, Final, Union
20
+ from typing import TYPE_CHECKING, Callable, Counter, Dict, Final, Union
21
21
  from urllib import parse
22
22
 
23
23
  from typing_extensions import TypeAlias
@@ -31,6 +31,9 @@ from streamlit.runtime.scriptrunner.script_requests import ScriptRequests
31
31
  from streamlit.runtime.state import SafeSessionState
32
32
  from streamlit.runtime.uploaded_file_manager import UploadedFileManager
33
33
 
34
+ if TYPE_CHECKING:
35
+ from streamlit.runtime.fragment import FragmentStorage
36
+
34
37
  _LOGGER: Final = get_logger(__name__)
35
38
 
36
39
  UserInfo: TypeAlias = Dict[str, Union[str, None]]
@@ -59,6 +62,7 @@ class ScriptRunContext:
59
62
  main_script_path: str
60
63
  page_script_hash: str
61
64
  user_info: UserInfo
65
+ fragment_storage: "FragmentStorage"
62
66
 
63
67
  gather_usage_stats: bool = False
64
68
  command_tracking_deactivated: bool = False
@@ -71,12 +75,19 @@ class ScriptRunContext:
71
75
  form_ids_this_run: set[str] = field(default_factory=set)
72
76
  cursors: dict[int, "streamlit.cursor.RunningCursor"] = field(default_factory=dict)
73
77
  script_requests: ScriptRequests | None = None
78
+ current_fragment_id: str | None = None
79
+ fragment_ids_this_run: set[str] | None = None
74
80
 
75
81
  # TODO(willhuang1997): Remove this variable when experimental query params are removed
76
82
  _experimental_query_params_used = False
77
83
  _production_query_params_used = False
78
84
 
79
- def reset(self, query_string: str = "", page_script_hash: str = "") -> None:
85
+ def reset(
86
+ self,
87
+ query_string: str = "",
88
+ page_script_hash: str = "",
89
+ fragment_ids_this_run: set[str] | None = None,
90
+ ) -> None:
80
91
  self.cursors = {}
81
92
  self.widget_ids_this_run = set()
82
93
  self.widget_user_keys_this_run = set()
@@ -89,6 +100,8 @@ class ScriptRunContext:
89
100
  self.command_tracking_deactivated: bool = False
90
101
  self.tracked_commands = []
91
102
  self.tracked_commands_counter = collections.Counter()
103
+ self.current_fragment_id = None
104
+ self.fragment_ids_this_run = fragment_ids_this_run
92
105
 
93
106
  parsed_query_params = parse.parse_qs(query_string, keep_blank_values=True)
94
107
  with self.session_state.query_params() as qp:
@@ -21,7 +21,7 @@ import types
21
21
  from contextlib import contextmanager
22
22
  from enum import Enum
23
23
  from timeit import default_timer as timer
24
- from typing import Callable, Final
24
+ from typing import TYPE_CHECKING, Callable, Final
25
25
 
26
26
  from blinker import Signal
27
27
 
@@ -49,6 +49,9 @@ from streamlit.runtime.state import (
49
49
  from streamlit.runtime.uploaded_file_manager import UploadedFileManager
50
50
  from streamlit.vendor.ipython.modified_sys_path import modified_sys_path
51
51
 
52
+ if TYPE_CHECKING:
53
+ from streamlit.runtime.fragment import FragmentStorage
54
+
52
55
  _LOGGER: Final = get_logger(__name__)
53
56
 
54
57
 
@@ -68,6 +71,10 @@ class ScriptRunnerEvent(Enum):
68
71
  # The script run stopped in order to start a script run with newer widget state.
69
72
  SCRIPT_STOPPED_FOR_RERUN = "SCRIPT_STOPPED_FOR_RERUN"
70
73
 
74
+ # The script run corresponding to a fragment ran to completion, or was interrupted
75
+ # by the user.
76
+ FRAGMENT_STOPPED_WITH_SUCCESS = "FRAGMENT_STOPPED_WITH_SUCCESS"
77
+
71
78
  # The ScriptRunner is done processing the ScriptEventQueue and
72
79
  # is shut down.
73
80
  SHUTDOWN = "SHUTDOWN"
@@ -106,6 +113,7 @@ class ScriptRunner:
106
113
  script_cache: ScriptCache,
107
114
  initial_rerun_data: RerunData,
108
115
  user_info: dict[str, str | None],
116
+ fragment_storage: "FragmentStorage",
109
117
  ):
110
118
  """Initialize the ScriptRunner.
111
119
 
@@ -119,12 +127,18 @@ class ScriptRunner:
119
127
  main_script_path
120
128
  Path to our main app script.
121
129
 
130
+ session_state
131
+ The AppSession's SessionState instance.
132
+
122
133
  uploaded_file_mgr
123
134
  The File manager to store the data uploaded by the file_uploader widget.
124
135
 
125
136
  script_cache
126
137
  A ScriptCache instance.
127
138
 
139
+ initial_rerun_data
140
+ RerunData to initialize this ScriptRunner with.
141
+
128
142
  user_info
129
143
  A dict that contains information about the current user. For now,
130
144
  it only contains the user's email address.
@@ -136,16 +150,18 @@ class ScriptRunner:
136
150
  Information about the current user is optionally provided when a
137
151
  websocket connection is initialized via the "X-Streamlit-User" header.
138
152
 
153
+ fragment_storage
154
+ The AppSession's FragmentStorage instance.
139
155
  """
140
156
  self._session_id = session_id
141
157
  self._main_script_path = main_script_path
142
- self._uploaded_file_mgr = uploaded_file_mgr
143
- self._script_cache = script_cache
144
- self._user_info = user_info
145
-
146
158
  self._session_state = SafeSessionState(
147
159
  session_state, yield_callback=self._maybe_handle_execution_control_request
148
160
  )
161
+ self._uploaded_file_mgr = uploaded_file_mgr
162
+ self._script_cache = script_cache
163
+ self._user_info = user_info
164
+ self._fragment_storage = fragment_storage
149
165
 
150
166
  self._requests = ScriptRequests()
151
167
  self._requests.request_rerun(initial_rerun_data)
@@ -277,6 +293,7 @@ class ScriptRunner:
277
293
  page_script_hash="",
278
294
  user_info=self._user_info,
279
295
  gather_usage_stats=bool(config.get_option("browser.gatherUsageStats")),
296
+ fragment_storage=self._fragment_storage,
280
297
  )
281
298
  add_script_run_ctx(threading.current_thread(), ctx)
282
299
 
@@ -394,6 +411,9 @@ class ScriptRunner:
394
411
  The RerunData to use.
395
412
 
396
413
  """
414
+ # Avoid circular imports
415
+ from streamlit.delta_generator import dg_stack
416
+
397
417
  assert self._is_in_script_thread()
398
418
 
399
419
  # An explicit loop instead of recursion to avoid stack overflows
@@ -441,16 +461,20 @@ class ScriptRunner:
441
461
  else main_page_info["page_script_hash"]
442
462
  )
443
463
 
464
+ fragment_ids_this_run = set(rerun_data.fragment_id_queue)
465
+
444
466
  ctx = self._get_script_run_ctx()
445
467
  ctx.reset(
446
468
  query_string=rerun_data.query_string,
447
469
  page_script_hash=page_script_hash,
470
+ fragment_ids_this_run=fragment_ids_this_run,
448
471
  )
449
472
 
450
473
  self.on_event.send(
451
474
  self,
452
475
  event=ScriptRunnerEvent.SCRIPT_STARTED,
453
476
  page_script_hash=page_script_hash,
477
+ fragment_ids_this_run=fragment_ids_this_run,
454
478
  )
455
479
 
456
480
  # Compile the script. Any errors thrown here will be surfaced
@@ -497,6 +521,16 @@ class ScriptRunner:
497
521
  # is interrupted by a RerunException.
498
522
  rerun_exception_data: RerunData | None = None
499
523
 
524
+ # Saving and restoring our original cursors/dg_stack is needed
525
+ # specifically to handle the case where a RerunException is raised while
526
+ # running a fragment. In this case, we need to restore both to their states
527
+ # at the start of the script run to ensure that we write to the correct
528
+ # places in the app during the rerun (without this, ctx.cursors and dg_stack
529
+ # will still be set to the snapshots they were restored from when running
530
+ # the fragment).
531
+ original_cursors = ctx.cursors
532
+ original_dg_stack = dg_stack.get()
533
+
500
534
  # If the script stops early, we don't want to remove unseen widgets,
501
535
  # so we track this to potentially skip session state cleanup later.
502
536
  premature_stop: bool = False
@@ -504,14 +538,6 @@ class ScriptRunner:
504
538
  try:
505
539
  # Create fake module. This gives us a name global namespace to
506
540
  # execute the code in.
507
- # TODO(vdonato): Double-check that we're okay with naming the
508
- # module for every page `__main__`. I'm pretty sure this is
509
- # necessary given that people will likely often write
510
- # ```
511
- # if __name__ == "__main__":
512
- # ...
513
- # ```
514
- # in their scripts.
515
541
  module = self._new_module("__main__")
516
542
 
517
543
  # Install the fake module as the __main__ module. This allows
@@ -539,11 +565,30 @@ class ScriptRunner:
539
565
 
540
566
  ctx.on_script_start()
541
567
  prep_time = timer() - start_time
542
- exec(code, module.__dict__)
568
+
569
+ if rerun_data.fragment_id_queue:
570
+ for fragment_id in rerun_data.fragment_id_queue:
571
+ try:
572
+ wrapped_fragment = self._fragment_storage.get(
573
+ fragment_id
574
+ )
575
+ ctx.current_fragment_id = fragment_id
576
+ wrapped_fragment()
577
+
578
+ except KeyError:
579
+ raise RuntimeError(
580
+ f"Could not find fragment with id {fragment_id}"
581
+ )
582
+ else:
583
+ self._fragment_storage.clear()
584
+ exec(code, module.__dict__)
585
+
543
586
  self._session_state.maybe_check_serializable()
544
587
  self._session_state[SCRIPT_RUN_WITHOUT_ERRORS_KEY] = True
545
588
  except RerunException as e:
546
589
  rerun_exception_data = e.rerun_data
590
+ ctx.cursors = original_cursors
591
+ dg_stack.set(original_dg_stack)
547
592
  # Interruption due to a rerun is usually from `st.rerun()`, which
548
593
  # we want to count as a script completion so triggers reset.
549
594
  # It is also possible for this to happen if fast reruns is off,
@@ -563,7 +608,11 @@ class ScriptRunner:
563
608
 
564
609
  finally:
565
610
  if rerun_exception_data:
611
+ # The handling for when a full script run or a fragment is stopped early
612
+ # is the same, so we only have one ScriptRunnerEvent for this scenario.
566
613
  finished_event = ScriptRunnerEvent.SCRIPT_STOPPED_FOR_RERUN
614
+ elif rerun_data.fragment_id_queue:
615
+ finished_event = ScriptRunnerEvent.FRAGMENT_STOPPED_WITH_SUCCESS
567
616
  else:
568
617
  finished_event = ScriptRunnerEvent.SCRIPT_STOPPED_WITH_SUCCESS
569
618
 
@@ -123,6 +123,8 @@ class WidgetMetadata(Generic[T]):
123
123
  callback_args: WidgetArgs | None = None
124
124
  callback_kwargs: WidgetKwargs | None = None
125
125
 
126
+ fragment_id: str | None = None
127
+
126
128
  def __repr__(self) -> str:
127
129
  return util.repr_(self)
128
130
 
@@ -182,11 +182,21 @@ class WStates(MutableMapping[str, Any]):
182
182
  """Set a widget's metadata, overwriting any existing metadata it has."""
183
183
  self.widget_metadata[widget_meta.id] = widget_meta
184
184
 
185
- def remove_stale_widgets(self, active_widget_ids: set[str]) -> None:
186
- """Remove widget state for widgets whose ids aren't in `active_widget_ids`."""
187
- # TODO(vdonato / kajarenc): Remove files corresponding to an inactive file
188
- # uploader.
189
- self.states = {k: v for k, v in self.states.items() if k in active_widget_ids}
185
+ def remove_stale_widgets(
186
+ self,
187
+ active_widget_ids: set[str],
188
+ fragment_ids_this_run: set[str] | None,
189
+ ) -> None:
190
+ """Remove widget state for stale widgets."""
191
+ self.states = {
192
+ k: v
193
+ for k, v in self.states.items()
194
+ if not _is_stale_widget(
195
+ self.widget_metadata.get(k),
196
+ active_widget_ids,
197
+ fragment_ids_this_run,
198
+ )
199
+ }
190
200
 
191
201
  def get_serialized(self, k: str) -> WidgetStateProto | None:
192
202
  """Get the serialized value of the widget with the given id.
@@ -561,14 +571,31 @@ class SessionState:
561
571
 
562
572
  def _remove_stale_widgets(self, active_widget_ids: set[str]) -> None:
563
573
  """Remove widget state for widgets whose ids aren't in `active_widget_ids`."""
564
- self._new_widget_state.remove_stale_widgets(active_widget_ids)
574
+ # Avoid circular imports.
575
+ from streamlit.runtime.scriptrunner import get_script_run_ctx
576
+
577
+ ctx = get_script_run_ctx()
578
+ if ctx is None:
579
+ return
580
+
581
+ self._new_widget_state.remove_stale_widgets(
582
+ active_widget_ids,
583
+ ctx.fragment_ids_this_run,
584
+ )
565
585
 
566
586
  # Remove entries from _old_state corresponding to
567
587
  # widgets not in widget_ids.
568
588
  self._old_state = {
569
589
  k: v
570
590
  for k, v in self._old_state.items()
571
- if (k in active_widget_ids or not is_widget_id(k))
591
+ if (
592
+ not is_widget_id(k)
593
+ or not _is_stale_widget(
594
+ self._new_widget_state.widget_metadata.get(k),
595
+ active_widget_ids,
596
+ ctx.fragment_ids_this_run,
597
+ )
598
+ )
572
599
  }
573
600
 
574
601
  def _set_widget_metadata(self, widget_metadata: WidgetMetadata[Any]) -> None:
@@ -671,6 +698,23 @@ def _is_internal_key(key: str) -> bool:
671
698
  return key.startswith(STREAMLIT_INTERNAL_KEY_PREFIX)
672
699
 
673
700
 
701
+ def _is_stale_widget(
702
+ metadata: WidgetMetadata[Any] | None,
703
+ active_widget_ids: set[str],
704
+ fragment_ids_this_run: set[str] | None,
705
+ ) -> bool:
706
+ if not metadata:
707
+ return True
708
+ elif metadata.id in active_widget_ids:
709
+ return False
710
+ # If we're running 1 or more fragments, but this widget is unrelated to any of the
711
+ # fragments that we're running, then it should not be marked as stale as its value
712
+ # may still be needed for a future fragment run or full script run.
713
+ elif fragment_ids_this_run and metadata.fragment_id not in fragment_ids_this_run:
714
+ return False
715
+ return True
716
+
717
+
674
718
  @dataclass
675
719
  class SessionStateStatProvider(CacheStatsProvider):
676
720
  _session_mgr: SessionManager
@@ -159,6 +159,7 @@ def register_widget(
159
159
  callback=on_change_handler,
160
160
  callback_args=args,
161
161
  callback_kwargs=kwargs,
162
+ fragment_id=ctx.current_fragment_id if ctx else None,
162
163
  )
163
164
  return register_widget_from_metadata(metadata, ctx, widget_func_name, element_type)
164
165
 
@@ -217,8 +218,8 @@ def register_widget_from_metadata(
217
218
 
218
219
 
219
220
  def coalesce_widget_states(
220
- old_states: WidgetStates, new_states: WidgetStates
221
- ) -> WidgetStates:
221
+ old_states: WidgetStates | None, new_states: WidgetStates | None
222
+ ) -> WidgetStates | None:
222
223
  """Coalesce an older WidgetStates into a newer one, and return a new
223
224
  WidgetStates containing the result.
224
225
 
@@ -228,6 +229,13 @@ def coalesce_widget_states(
228
229
  `old_states` will be set to True in the coalesced result, so that button
229
230
  presses don't go missing.
230
231
  """
232
+ if not old_states and not new_states:
233
+ return None
234
+ elif not old_states:
235
+ return new_states
236
+ elif not new_states:
237
+ return old_states
238
+
231
239
  states_by_id: dict[str, WidgetState] = {
232
240
  wstate.id: wstate for wstate in new_states.widgets
233
241
  }
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "files": {
3
3
  "main.css": "./static/css/main.bf304093.css",
4
- "main.js": "./static/js/main.7fde7092.js",
4
+ "main.js": "./static/js/main.356407e8.js",
5
5
  "static/js/9336.2d95d840.chunk.js": "./static/js/9336.2d95d840.chunk.js",
6
6
  "static/js/9330.d29313d4.chunk.js": "./static/js/9330.d29313d4.chunk.js",
7
7
  "static/js/7217.d970c074.chunk.js": "./static/js/7217.d970c074.chunk.js",
8
8
  "static/js/3301.1d1b10bb.chunk.js": "./static/js/3301.1d1b10bb.chunk.js",
9
- "static/css/3092.f719e2e6.chunk.css": "./static/css/3092.f719e2e6.chunk.css",
10
- "static/js/3092.3d4df25e.chunk.js": "./static/js/3092.3d4df25e.chunk.js",
11
- "static/css/43.c24b25fa.chunk.css": "./static/css/43.c24b25fa.chunk.css",
9
+ "static/css/3092.95a45cfe.chunk.css": "./static/css/3092.95a45cfe.chunk.css",
10
+ "static/js/3092.ad569cc8.chunk.js": "./static/js/3092.ad569cc8.chunk.js",
11
+ "static/css/43.e3b876c5.chunk.css": "./static/css/43.e3b876c5.chunk.css",
12
12
  "static/js/43.76c54963.chunk.js": "./static/js/43.76c54963.chunk.js",
13
13
  "static/js/8427.44d27448.chunk.js": "./static/js/8427.44d27448.chunk.js",
14
14
  "static/js/7323.2808d029.chunk.js": "./static/js/7323.2808d029.chunk.js",
@@ -20,24 +20,25 @@
20
20
  "static/js/4113.1e7eff4d.chunk.js": "./static/js/4113.1e7eff4d.chunk.js",
21
21
  "static/js/1168.3029456a.chunk.js": "./static/js/1168.3029456a.chunk.js",
22
22
  "static/js/178.b5384fd0.chunk.js": "./static/js/178.b5384fd0.chunk.js",
23
- "static/js/1792.16c16498.chunk.js": "./static/js/1792.16c16498.chunk.js",
24
- "static/js/3513.57cff89c.chunk.js": "./static/js/3513.57cff89c.chunk.js",
25
- "static/js/7602.f0420392.chunk.js": "./static/js/7602.f0420392.chunk.js",
26
- "static/js/6013.75c92264.chunk.js": "./static/js/6013.75c92264.chunk.js",
27
- "static/js/8492.e6dab83f.chunk.js": "./static/js/8492.e6dab83f.chunk.js",
28
- "static/js/4177.ab9a7aa1.chunk.js": "./static/js/4177.ab9a7aa1.chunk.js",
29
- "static/js/1451.e3be1711.chunk.js": "./static/js/1451.e3be1711.chunk.js",
30
- "static/js/1074.71719df6.chunk.js": "./static/js/1074.71719df6.chunk.js",
23
+ "static/js/1792.b8efa879.chunk.js": "./static/js/1792.b8efa879.chunk.js",
24
+ "static/js/3513.e3e7300a.chunk.js": "./static/js/3513.e3e7300a.chunk.js",
25
+ "static/js/7602.6175e969.chunk.js": "./static/js/7602.6175e969.chunk.js",
26
+ "static/js/6013.8e80e091.chunk.js": "./static/js/6013.8e80e091.chunk.js",
27
+ "static/js/8492.f56c9d4c.chunk.js": "./static/js/8492.f56c9d4c.chunk.js",
28
+ "static/js/4177.69f9f18d.chunk.js": "./static/js/4177.69f9f18d.chunk.js",
29
+ "static/js/1451.3b0a3e31.chunk.js": "./static/js/1451.3b0a3e31.chunk.js",
30
+ "static/js/2634.1249dc7a.chunk.js": "./static/js/2634.1249dc7a.chunk.js",
31
+ "static/js/1074.73973756.chunk.js": "./static/js/1074.73973756.chunk.js",
31
32
  "static/js/8477.e948c092.chunk.js": "./static/js/8477.e948c092.chunk.js",
32
33
  "static/js/6853.d999ac75.chunk.js": "./static/js/6853.d999ac75.chunk.js",
33
- "static/js/4477.2555c11a.chunk.js": "./static/js/4477.2555c11a.chunk.js",
34
- "static/js/4319.213fc321.chunk.js": "./static/js/4319.213fc321.chunk.js",
35
- "static/js/5106.22187bfc.chunk.js": "./static/js/5106.22187bfc.chunk.js",
36
- "static/js/4666.99f3abc3.chunk.js": "./static/js/4666.99f3abc3.chunk.js",
37
- "static/js/5379.e466522d.chunk.js": "./static/js/5379.e466522d.chunk.js",
38
- "static/js/8691.24a5792f.chunk.js": "./static/js/8691.24a5792f.chunk.js",
39
- "static/js/6718.97945fc6.chunk.js": "./static/js/6718.97945fc6.chunk.js",
40
- "static/js/7175.8c1b4d38.chunk.js": "./static/js/7175.8c1b4d38.chunk.js",
34
+ "static/js/4477.e10e4373.chunk.js": "./static/js/4477.e10e4373.chunk.js",
35
+ "static/js/4319.a6745434.chunk.js": "./static/js/4319.a6745434.chunk.js",
36
+ "static/js/5106.44f0ff51.chunk.js": "./static/js/5106.44f0ff51.chunk.js",
37
+ "static/js/4666.b694c5a9.chunk.js": "./static/js/4666.b694c5a9.chunk.js",
38
+ "static/js/5379.6571574f.chunk.js": "./static/js/5379.6571574f.chunk.js",
39
+ "static/js/8691.9ccf7f89.chunk.js": "./static/js/8691.9ccf7f89.chunk.js",
40
+ "static/js/6718.802da17e.chunk.js": "./static/js/6718.802da17e.chunk.js",
41
+ "static/js/7175.be4076bc.chunk.js": "./static/js/7175.be4076bc.chunk.js",
41
42
  "static/js/5345.65c91ee7.chunk.js": "./static/js/5345.65c91ee7.chunk.js",
42
43
  "static/js/9865.fd93213d.chunk.js": "./static/js/9865.fd93213d.chunk.js",
43
44
  "static/js/6405.ac5a6f23.chunk.js": "./static/js/6405.ac5a6f23.chunk.js",
@@ -50,8 +51,8 @@
50
51
  "static/js/8570.6de19120.chunk.js": "./static/js/8570.6de19120.chunk.js",
51
52
  "static/js/7142.83028745.chunk.js": "./static/js/7142.83028745.chunk.js",
52
53
  "static/js/3998.01237fcf.chunk.js": "./static/js/3998.01237fcf.chunk.js",
53
- "static/css/2411.81b3d18f.chunk.css": "./static/css/2411.81b3d18f.chunk.css",
54
- "static/js/2411.b389bf4e.chunk.js": "./static/js/2411.b389bf4e.chunk.js",
54
+ "static/css/2411.8b8f33d6.chunk.css": "./static/css/2411.8b8f33d6.chunk.css",
55
+ "static/js/2411.a8823789.chunk.js": "./static/js/2411.a8823789.chunk.js",
55
56
  "static/js/656.ae85f8f1.chunk.js": "./static/js/656.ae85f8f1.chunk.js",
56
57
  "static/js/6150.1294fa0d.chunk.js": "./static/js/6150.1294fa0d.chunk.js",
57
58
  "static/js/4253.749d5244.chunk.js": "./static/js/4253.749d5244.chunk.js",
@@ -60,7 +61,8 @@
60
61
  "static/js/2187.9469f035.chunk.js": "./static/js/2187.9469f035.chunk.js",
61
62
  "static/js/1479.6709db03.chunk.js": "./static/js/1479.6709db03.chunk.js",
62
63
  "static/js/4132.49bf3f2c.chunk.js": "./static/js/4132.49bf3f2c.chunk.js",
63
- "static/js/2736.17fbad1a.chunk.js": "./static/js/2736.17fbad1a.chunk.js",
64
+ "static/js/2736.779ccbc1.chunk.js": "./static/js/2736.779ccbc1.chunk.js",
65
+ "static/js/937.a1248039.chunk.js": "./static/js/937.a1248039.chunk.js",
64
66
  "static/media/fireworks.gif": "./static/media/fireworks.0906f02ea43f1018a6d2.gif",
65
67
  "static/media/flake-2.png": "./static/media/flake-2.e3f07d06933dd0e84c24.png",
66
68
  "static/media/flake-1.png": "./static/media/flake-1.8077dc154e0bf900aa73.png",
@@ -150,6 +152,6 @@
150
152
  },
151
153
  "entrypoints": [
152
154
  "static/css/main.bf304093.css",
153
- "static/js/main.7fde7092.js"
155
+ "static/js/main.356407e8.js"
154
156
  ]
155
157
  }
@@ -1 +1 @@
1
- <!doctype html><html lang="en"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"/><link rel="shortcut icon" href="./favicon.png"/><link rel="preload" href="./static/media/SourceSansPro-Regular.0d69e5ff5e92ac64a0c9.woff2" as="font" type="font/woff2" crossorigin><link rel="preload" href="./static/media/SourceSansPro-SemiBold.abed79cd0df1827e18cf.woff2" as="font" type="font/woff2" crossorigin><link rel="preload" href="./static/media/SourceSansPro-Bold.118dea98980e20a81ced.woff2" as="font" type="font/woff2" crossorigin><title>Streamlit</title><script>window.prerenderReady=!1</script><script defer="defer" src="./static/js/main.7fde7092.js"></script><link href="./static/css/main.bf304093.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
1
+ <!doctype html><html lang="en"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"/><link rel="shortcut icon" href="./favicon.png"/><link rel="preload" href="./static/media/SourceSansPro-Regular.0d69e5ff5e92ac64a0c9.woff2" as="font" type="font/woff2" crossorigin><link rel="preload" href="./static/media/SourceSansPro-SemiBold.abed79cd0df1827e18cf.woff2" as="font" type="font/woff2" crossorigin><link rel="preload" href="./static/media/SourceSansPro-Bold.118dea98980e20a81ced.woff2" as="font" type="font/woff2" crossorigin><title>Streamlit</title><script>window.prerenderReady=!1</script><script defer="defer" src="./static/js/main.356407e8.js"></script><link href="./static/css/main.bf304093.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>