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
@@ -38,6 +38,7 @@ from streamlit.proto.NewSession_pb2 import (
38
38
  from streamlit.proto.PagesChanged_pb2 import PagesChanged
39
39
  from streamlit.runtime import caching, legacy_caching
40
40
  from streamlit.runtime.forward_msg_queue import ForwardMsgQueue
41
+ from streamlit.runtime.fragment import FragmentStorage, MemoryFragmentStorage
41
42
  from streamlit.runtime.metrics_util import Installation
42
43
  from streamlit.runtime.script_data import ScriptData
43
44
  from streamlit.runtime.scriptrunner import RerunData, ScriptRunner, ScriptRunnerEvent
@@ -161,6 +162,8 @@ class AppSession:
161
162
 
162
163
  self._debug_last_backmsg_id: str | None = None
163
164
 
165
+ self._fragment_storage: FragmentStorage = MemoryFragmentStorage()
166
+
164
167
  _LOGGER.debug("AppSession initialized (id=%s)", self.id)
165
168
 
166
169
  def __del__(self) -> None:
@@ -353,26 +356,33 @@ class AppSession:
353
356
  return
354
357
 
355
358
  if client_state:
359
+ fragment_id = client_state.fragment_id
360
+
356
361
  rerun_data = RerunData(
357
362
  client_state.query_string,
358
363
  client_state.widget_states,
359
364
  client_state.page_script_hash,
360
365
  client_state.page_name,
366
+ fragment_id_queue=[fragment_id] if fragment_id else [],
361
367
  )
362
368
  else:
363
369
  rerun_data = RerunData()
364
370
 
365
371
  if self._scriptrunner is not None:
366
- if bool(config.get_option("runner.fastReruns")):
367
- # If fastReruns is enabled, we don't send rerun requests to our
368
- # existing ScriptRunner. Instead, we tell it to shut down. We'll
369
- # then spin up a new ScriptRunner, below, to handle the rerun
370
- # immediately.
372
+ if (
373
+ bool(config.get_option("runner.fastReruns"))
374
+ and not rerun_data.fragment_id_queue
375
+ ):
376
+ # If fastReruns is enabled and this is *not* a rerun of a fragment,
377
+ # we don't send rerun requests to our existing ScriptRunner. Instead, we
378
+ # tell it to shut down. We'll then spin up a new ScriptRunner, below, to
379
+ # handle the rerun immediately.
371
380
  self._scriptrunner.request_stop()
372
381
  self._scriptrunner = None
373
382
  else:
374
- # fastReruns is not enabled. Send our ScriptRunner a rerun
375
- # request. If the request is accepted, we're done.
383
+ # Either fastReruns is not enabled or this RERUN request is a request to
384
+ # run a fragment. We send our current ScriptRunner a rerun request, and
385
+ # if it's accepted, we're done.
376
386
  success = self._scriptrunner.request_rerun(rerun_data)
377
387
  if success:
378
388
  return
@@ -400,6 +410,7 @@ class AppSession:
400
410
  script_cache=self._script_cache,
401
411
  initial_rerun_data=initial_rerun_data,
402
412
  user_info=self._user_info,
413
+ fragment_storage=self._fragment_storage,
403
414
  )
404
415
  self._scriptrunner.on_event.connect(self._on_scriptrunner_event)
405
416
  self._scriptrunner.start()
@@ -464,6 +475,7 @@ class AppSession:
464
475
  exception: BaseException | None = None,
465
476
  client_state: ClientState | None = None,
466
477
  page_script_hash: str | None = None,
478
+ fragment_ids_this_run: set[str] | None = None,
467
479
  ) -> None:
468
480
  """Called when our ScriptRunner emits an event.
469
481
 
@@ -473,7 +485,13 @@ class AppSession:
473
485
  """
474
486
  self._event_loop.call_soon_threadsafe(
475
487
  lambda: self._handle_scriptrunner_event_on_event_loop(
476
- sender, event, forward_msg, exception, client_state, page_script_hash
488
+ sender,
489
+ event,
490
+ forward_msg,
491
+ exception,
492
+ client_state,
493
+ page_script_hash,
494
+ fragment_ids_this_run,
477
495
  )
478
496
  )
479
497
 
@@ -485,6 +503,7 @@ class AppSession:
485
503
  exception: BaseException | None = None,
486
504
  client_state: ClientState | None = None,
487
505
  page_script_hash: str | None = None,
506
+ fragment_ids_this_run: set[str] | None = None,
488
507
  ) -> None:
489
508
  """Handle a ScriptRunner event.
490
509
 
@@ -515,6 +534,11 @@ class AppSession:
515
534
  page_script_hash : str | None
516
535
  A hash of the script path corresponding to the page currently being
517
536
  run. Set only for the SCRIPT_STARTED event.
537
+
538
+ fragment_ids_this_run : set[str] | None
539
+ The fragment IDs of the fragments being executed in this script run. Only
540
+ set for the SCRIPT_STARTED event. If this value is falsy, this script run
541
+ must be for the full script.
518
542
  """
519
543
 
520
544
  assert (
@@ -540,30 +564,43 @@ class AppSession:
540
564
  page_script_hash is not None
541
565
  ), "page_script_hash must be set for the SCRIPT_STARTED event"
542
566
 
543
- self._clear_queue()
567
+ # When running the full script, we clear the browser ForwardMsg queue since
568
+ # anything from a previous script run that has yet to be sent to the browser
569
+ # will be overwritten. For fragment runs, however, we don't want to do this
570
+ # as the ForwardMsgs in the queue may not correspond to the running
571
+ # fragment, so dropping the messages may result in the app missing
572
+ # information.
573
+ if not fragment_ids_this_run:
574
+ self._clear_queue()
575
+
544
576
  self._enqueue_forward_msg(
545
- self._create_new_session_message(page_script_hash)
577
+ self._create_new_session_message(
578
+ page_script_hash, fragment_ids_this_run
579
+ )
546
580
  )
547
581
 
548
582
  elif (
549
583
  event == ScriptRunnerEvent.SCRIPT_STOPPED_WITH_SUCCESS
550
584
  or event == ScriptRunnerEvent.SCRIPT_STOPPED_WITH_COMPILE_ERROR
585
+ or event == ScriptRunnerEvent.FRAGMENT_STOPPED_WITH_SUCCESS
551
586
  ):
552
587
  if self._state != AppSessionState.SHUTDOWN_REQUESTED:
553
588
  self._state = AppSessionState.APP_NOT_RUNNING
554
589
 
555
- script_succeeded = event == ScriptRunnerEvent.SCRIPT_STOPPED_WITH_SUCCESS
556
-
557
- script_finished_msg = self._create_script_finished_message(
558
- ForwardMsg.FINISHED_SUCCESSFULLY
559
- if script_succeeded
560
- else ForwardMsg.FINISHED_WITH_COMPILE_ERROR
561
- )
562
- self._enqueue_forward_msg(script_finished_msg)
590
+ if event == ScriptRunnerEvent.SCRIPT_STOPPED_WITH_SUCCESS:
591
+ status = ForwardMsg.FINISHED_SUCCESSFULLY
592
+ elif event == ScriptRunnerEvent.FRAGMENT_STOPPED_WITH_SUCCESS:
593
+ status = ForwardMsg.FINISHED_FRAGMENT_RUN_SUCCESSFULLY
594
+ else:
595
+ status = ForwardMsg.FINISHED_WITH_COMPILE_ERROR
563
596
 
597
+ self._enqueue_forward_msg(self._create_script_finished_message(status))
564
598
  self._debug_last_backmsg_id = None
565
599
 
566
- if script_succeeded:
600
+ if (
601
+ event == ScriptRunnerEvent.SCRIPT_STOPPED_WITH_SUCCESS
602
+ or event == ScriptRunnerEvent.FRAGMENT_STOPPED_WITH_SUCCESS
603
+ ):
567
604
  # The script completed successfully: update our
568
605
  # LocalSourcesWatcher to account for any source code changes
569
606
  # that change which modules should be watched.
@@ -582,11 +619,12 @@ class AppSession:
582
619
  self._enqueue_forward_msg(msg)
583
620
 
584
621
  elif event == ScriptRunnerEvent.SCRIPT_STOPPED_FOR_RERUN:
585
- script_finished_msg = self._create_script_finished_message(
586
- ForwardMsg.FINISHED_EARLY_FOR_RERUN
587
- )
588
622
  self._state = AppSessionState.APP_NOT_RUNNING
589
- self._enqueue_forward_msg(script_finished_msg)
623
+ self._enqueue_forward_msg(
624
+ self._create_script_finished_message(
625
+ ForwardMsg.FINISHED_EARLY_FOR_RERUN
626
+ )
627
+ )
590
628
  if self._local_sources_watcher:
591
629
  self._local_sources_watcher.update_watched_modules()
592
630
 
@@ -630,7 +668,9 @@ class AppSession:
630
668
  msg.session_event.script_changed_on_disk = True
631
669
  return msg
632
670
 
633
- def _create_new_session_message(self, page_script_hash: str) -> ForwardMsg:
671
+ def _create_new_session_message(
672
+ self, page_script_hash: str, fragment_ids_this_run: set[str] | None = None
673
+ ) -> ForwardMsg:
634
674
  """Create and return a new_session ForwardMsg."""
635
675
  msg = ForwardMsg()
636
676
 
@@ -639,6 +679,9 @@ class AppSession:
639
679
  msg.new_session.main_script_path = self._script_data.main_script_path
640
680
  msg.new_session.page_script_hash = page_script_hash
641
681
 
682
+ if fragment_ids_this_run:
683
+ msg.new_session.fragment_ids_this_run.extend(fragment_ids_this_run)
684
+
642
685
  _populate_app_pages(msg.new_session, self._script_data.main_script_path)
643
686
  _populate_config_msg(msg.new_session.config)
644
687
  _populate_theme_msg(msg.new_session.custom_theme)
@@ -58,9 +58,9 @@ from streamlit.runtime.caching.storage.dummy_cache_storage import (
58
58
  MemoryCacheStorageManager,
59
59
  )
60
60
  from streamlit.runtime.metrics_util import gather_metrics
61
- from streamlit.runtime.runtime_util import duration_to_seconds
62
61
  from streamlit.runtime.scriptrunner.script_run_context import get_script_run_ctx
63
62
  from streamlit.runtime.stats import CacheStat, CacheStatsProvider, group_stats
63
+ from streamlit.time_util import time_to_seconds
64
64
 
65
65
  _LOGGER: Final = get_logger(__name__)
66
66
 
@@ -154,7 +154,7 @@ class DataCaches(CacheStatsProvider):
154
154
  If it doesn't exist, create a new one with the given params.
155
155
  """
156
156
 
157
- ttl_seconds = duration_to_seconds(ttl, coerce_none_to_inf=False)
157
+ ttl_seconds = time_to_seconds(ttl, coerce_none_to_inf=False)
158
158
 
159
159
  # Get the existing cache, if it exists, and validate that its params
160
160
  # haven't changed.
@@ -254,7 +254,7 @@ class DataCaches(CacheStatsProvider):
254
254
  CacheStorageContext.
255
255
  """
256
256
 
257
- ttl_seconds = duration_to_seconds(ttl, coerce_none_to_inf=False)
257
+ ttl_seconds = time_to_seconds(ttl, coerce_none_to_inf=False)
258
258
 
259
259
  cache_context = self.create_cache_storage_context(
260
260
  function_key="DUMMY_KEY",
@@ -45,9 +45,9 @@ from streamlit.runtime.caching.cached_message_replay import (
45
45
  )
46
46
  from streamlit.runtime.caching.hashing import HashFuncsDict
47
47
  from streamlit.runtime.metrics_util import gather_metrics
48
- from streamlit.runtime.runtime_util import duration_to_seconds
49
48
  from streamlit.runtime.scriptrunner.script_run_context import get_script_run_ctx
50
49
  from streamlit.runtime.stats import CacheStat, CacheStatsProvider, group_stats
50
+ from streamlit.time_util import time_to_seconds
51
51
 
52
52
  _LOGGER: Final = get_logger(__name__)
53
53
 
@@ -89,7 +89,7 @@ class ResourceCaches(CacheStatsProvider):
89
89
  if max_entries is None:
90
90
  max_entries = math.inf
91
91
 
92
- ttl_seconds = duration_to_seconds(ttl)
92
+ ttl_seconds = time_to_seconds(ttl)
93
93
 
94
94
  # Get the existing cache, if it exists, and validate that its params
95
95
  # haven't changed.
@@ -0,0 +1,239 @@
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
+ import contextlib
18
+ import hashlib
19
+ import inspect
20
+ from abc import abstractmethod
21
+ from copy import deepcopy
22
+ from datetime import timedelta
23
+ from functools import wraps
24
+ from typing import Any, Callable, Protocol, TypeVar, overload
25
+
26
+ from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
27
+ from streamlit.runtime.metrics_util import gather_metrics
28
+ from streamlit.runtime.scriptrunner import get_script_run_ctx
29
+ from streamlit.time_util import time_to_seconds
30
+
31
+ F = TypeVar("F", bound=Callable[..., Any])
32
+ Fragment = Callable[[], Any]
33
+
34
+
35
+ class FragmentStorage(Protocol):
36
+ """A key-value store for Fragments. Used to implement the @st.experimental_fragment
37
+ decorator.
38
+
39
+ We intentionally define this as its own protocol despite how generic it appears to
40
+ be at first glance. The reason why is that, in any case where fragments aren't just
41
+ stored as Python closures in memory, storing and retrieving Fragments will generally
42
+ involve serializing and deserializing function bytecode, which is a tricky aspect
43
+ to implementing FragmentStorages that won't generally appear with our other *Storage
44
+ protocols.
45
+ """
46
+
47
+ @abstractmethod
48
+ def get(self, key: str) -> Fragment:
49
+ """Returns the stored fragment for the given key."""
50
+ raise NotImplementedError
51
+
52
+ @abstractmethod
53
+ def set(self, key: str, value: Fragment) -> None:
54
+ """Saves a fragment under the given key."""
55
+ raise NotImplementedError
56
+
57
+ @abstractmethod
58
+ def delete(self, key: str) -> None:
59
+ """Delete the fragment corresponding to the given key."""
60
+ raise NotImplementedError
61
+
62
+ @abstractmethod
63
+ def clear(self) -> None:
64
+ """Remove all fragments saved in this FragmentStorage."""
65
+ raise NotImplementedError
66
+
67
+
68
+ # NOTE: Ideally, we'd like to add a MemoryFragmentStorageStatProvider implementation to
69
+ # keep track of memory usage due to fragments, but doing something like this ends up
70
+ # being difficult in practice as the memory usage of a closure is hard to measure (the
71
+ # vendored implementation of pympler.asizeof that we use elsewhere is unable to measure
72
+ # the size of a function).
73
+ class MemoryFragmentStorage(FragmentStorage):
74
+ """A simple, memory-backed implementation of FragmentStorage.
75
+
76
+ MemoryFragmentStorage is just a wrapper around a plain Python dict that complies with
77
+ the FragmentStorage protocol.
78
+ """
79
+
80
+ def __init__(self):
81
+ self._fragments: dict[str, Fragment] = {}
82
+
83
+ def get(self, key: str) -> Fragment:
84
+ return self._fragments[key]
85
+
86
+ def set(self, key: str, value: Fragment) -> None:
87
+ self._fragments[key] = value
88
+
89
+ def delete(self, key: str) -> None:
90
+ del self._fragments[key]
91
+
92
+ def clear(self) -> None:
93
+ self._fragments.clear()
94
+
95
+
96
+ @overload
97
+ def fragment(
98
+ func: F,
99
+ *,
100
+ run_every: int | float | timedelta | str | None = None,
101
+ ) -> F:
102
+ ...
103
+
104
+
105
+ # Support being able to pass parameters to this decorator (that is, being able to write
106
+ # `@fragment(run_every=5.0)`).
107
+ @overload
108
+ def fragment(
109
+ func: None = None,
110
+ *,
111
+ run_every: int | float | timedelta | str | None = None,
112
+ ) -> Callable[[F], F]:
113
+ ...
114
+
115
+
116
+ @gather_metrics("experimental_fragment")
117
+ def fragment(
118
+ func: F | None = None,
119
+ *,
120
+ run_every: int | float | timedelta | str | None = None,
121
+ ) -> Callable[[F], F] | F:
122
+ """Allow a function to be run independently of the full script.
123
+
124
+ Functions decorated with ``@st.experimental_fragment`` are handled specially within
125
+ an app: when a widget created within an invocation of the function (a fragment) is
126
+ interacted with, then only that fragment is rerun rather than the full streamlit app.
127
+
128
+ Parameters
129
+ ----------
130
+ run_every: int, float, timedelta, str, or None
131
+ If set, fragments created from this function rerun periodically at the specified
132
+ time interval.
133
+
134
+ Example
135
+ -------
136
+ The following example demonstrates basic usage of ``@st.experimental_fragment``. In
137
+ this app, clicking on the "rerun full script" button will increment both counters,
138
+ but the "rerun fragment" button will only increment the counter within the fragment.
139
+
140
+ ```python3
141
+ import streamlit as st
142
+
143
+ if "script_runs" not in st.session_state:
144
+ st.session_state.script_runs = 0
145
+ st.session_state.fragment_runs = 0
146
+
147
+ @st.experimental_fragment
148
+ def fragment():
149
+ st.button("rerun fragment")
150
+ st.write(f"fragment runs: {st.session_state.fragment_runs}")
151
+ st.session_state.fragment_runs += 1
152
+
153
+ fragment()
154
+
155
+ st.button("rerun full script")
156
+ st.write(f"full script runs: {st.session_state.script_runs}")
157
+ st.session_state.script_runs += 1
158
+ ```
159
+ """
160
+
161
+ if func is None:
162
+ # Support passing the params via function decorator
163
+ def wrapper(f: F) -> F:
164
+ return fragment(
165
+ func=f,
166
+ run_every=run_every,
167
+ )
168
+
169
+ return wrapper
170
+ else:
171
+ non_optional_func = func
172
+
173
+ @wraps(non_optional_func)
174
+ def wrap(*args, **kwargs):
175
+ from streamlit.delta_generator import dg_stack
176
+
177
+ ctx = get_script_run_ctx()
178
+ if ctx is None:
179
+ return
180
+
181
+ cursors_snapshot = deepcopy(ctx.cursors)
182
+ dg_stack_snapshot = deepcopy(dg_stack.get())
183
+ active_dg = dg_stack_snapshot[-1]
184
+ h = hashlib.new("md5")
185
+ h.update(
186
+ f"{non_optional_func.__module__}.{non_optional_func.__qualname__}{active_dg._get_delta_path_str()}".encode(
187
+ "utf-8"
188
+ )
189
+ )
190
+ fragment_id = h.hexdigest()
191
+
192
+ def wrapped_fragment():
193
+ import streamlit as st
194
+
195
+ # NOTE: We need to call get_script_run_ctx here again and can't just use the
196
+ # value of ctx from above captured by the closure because subsequent
197
+ # fragment runs will generally run in a new script run, thus we'll have a
198
+ # new ctx.
199
+ ctx = get_script_run_ctx(suppress_warning=True)
200
+ assert ctx is not None
201
+
202
+ if ctx.fragment_ids_this_run:
203
+ # This script run is a run of one or more fragments. We restore the
204
+ # state of ctx.cursors and dg_stack to the snapshots we took when this
205
+ # fragment was declared.
206
+ ctx.cursors = deepcopy(cursors_snapshot)
207
+ dg_stack.set(deepcopy(dg_stack_snapshot))
208
+ else:
209
+ # Otherwise, we must be in a full script run. We need to temporarily set
210
+ # ctx.current_fragment_id so that elements corresponding to this
211
+ # fragment get tagged with the appropriate ID. ctx.current_fragment_id
212
+ # gets reset after the fragment function finishes running.
213
+ ctx.current_fragment_id = fragment_id
214
+
215
+ try:
216
+ with st.container():
217
+ result = non_optional_func(*args, **kwargs)
218
+ finally:
219
+ ctx.current_fragment_id = None
220
+
221
+ return result
222
+
223
+ ctx.fragment_storage.set(fragment_id, wrapped_fragment)
224
+
225
+ if run_every:
226
+ msg = ForwardMsg()
227
+ msg.auto_rerun.interval = time_to_seconds(run_every)
228
+ msg.auto_rerun.fragment_id = fragment_id
229
+ ctx.enqueue(msg)
230
+
231
+ return wrapped_fragment()
232
+
233
+ with contextlib.suppress(AttributeError):
234
+ # Make this a well-behaved decorator by preserving important function
235
+ # attributes.
236
+ wrap.__dict__.update(non_optional_func.__dict__)
237
+ wrap.__signature__ = inspect.signature(non_optional_func) # type: ignore
238
+
239
+ return wrap
@@ -427,12 +427,17 @@ def create_page_profile_message(
427
427
  uncaught_exception: str | None = None,
428
428
  ) -> ForwardMsg:
429
429
  """Create and return the full PageProfile ForwardMsg."""
430
+ # Local import to prevent circular dependencies
431
+ from streamlit.runtime.scriptrunner import get_script_run_ctx
432
+
430
433
  msg = ForwardMsg()
431
- msg.page_profile.commands.extend(commands)
432
- msg.page_profile.exec_time = exec_time
433
- msg.page_profile.prep_time = prep_time
434
+ page_profile = msg.page_profile
435
+
436
+ page_profile.commands.extend(commands)
437
+ page_profile.exec_time = exec_time
438
+ page_profile.prep_time = prep_time
434
439
 
435
- msg.page_profile.headless = config.get_option("server.headless")
440
+ page_profile.headless = config.get_option("server.headless")
436
441
 
437
442
  # Collect all config options that have been manually set
438
443
  config_options: set[str] = set()
@@ -447,7 +452,7 @@ def create_page_profile_message(
447
452
  option_name = f"{option_name}:default"
448
453
  config_options.add(option_name)
449
454
 
450
- msg.page_profile.config.extend(config_options)
455
+ page_profile.config.extend(config_options)
451
456
 
452
457
  # Check the predefined set of modules for attribution
453
458
  attributions: set[str] = {
@@ -456,11 +461,14 @@ def create_page_profile_message(
456
461
  if attribution in sys.modules
457
462
  }
458
463
 
459
- msg.page_profile.os = str(sys.platform)
460
- msg.page_profile.timezone = str(time.tzname)
461
- msg.page_profile.attributions.extend(attributions)
464
+ page_profile.os = str(sys.platform)
465
+ page_profile.timezone = str(time.tzname)
466
+ page_profile.attributions.extend(attributions)
462
467
 
463
468
  if uncaught_exception:
464
- msg.page_profile.uncaught_exception = uncaught_exception
469
+ page_profile.uncaught_exception = uncaught_exception
470
+
471
+ if ctx := get_script_run_ctx():
472
+ page_profile.is_fragment_run = bool(ctx.current_fragment_id)
465
473
 
466
474
  return msg
@@ -22,8 +22,6 @@ from enum import Enum
22
22
  from typing import TYPE_CHECKING, Awaitable, Final, NamedTuple
23
23
 
24
24
  from streamlit import config
25
- from streamlit.components.lib.local_component_registry import LocalComponentRegistry
26
- from streamlit.components.types.base_component_registry import BaseComponentRegistry
27
25
  from streamlit.logger import get_logger
28
26
  from streamlit.proto.BackMsg_pb2 import BackMsg
29
27
  from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
@@ -98,11 +96,6 @@ class RuntimeConfig:
98
96
  default_factory=LocalDiskCacheStorageManager
99
97
  )
100
98
 
101
- # The ComponentRegistry instance to use.
102
- component_registry: BaseComponentRegistry = field(
103
- default_factory=LocalComponentRegistry
104
- )
105
-
106
99
  # The SessionManager class to be used.
107
100
  session_manager_class: type[SessionManager] = WebsocketSessionManager
108
101
 
@@ -112,6 +105,12 @@ class RuntimeConfig:
112
105
  # True if the command used to start Streamlit was `streamlit hello`.
113
106
  is_hello: bool = False
114
107
 
108
+ # TODO(vdonato): Eventually add a new fragment_storage_class field enabling the code
109
+ # creating a new Streamlit Runtime to configure the FragmentStorage instances
110
+ # created by each new AppSession. We choose not to do this for now to avoid adding
111
+ # additional complexity to RuntimeConfig/SessionManager/etc when it's unlikely
112
+ # we'll have a custom implementation of this class anytime soon.
113
+
115
114
 
116
115
  class RuntimeState(Enum):
117
116
  INITIAL = "INITIAL"
@@ -196,7 +195,6 @@ class Runtime:
196
195
  self._state = RuntimeState.INITIAL
197
196
 
198
197
  # Initialize managers
199
- self._component_registry = config.component_registry
200
198
  self._message_cache = ForwardMsgCache()
201
199
  self._uploaded_file_mgr = config.uploaded_file_manager
202
200
  self._media_file_mgr = MediaFileManager(storage=config.media_file_storage)
@@ -222,10 +220,6 @@ class Runtime:
222
220
  def state(self) -> RuntimeState:
223
221
  return self._state
224
222
 
225
- @property
226
- def component_registry(self) -> BaseComponentRegistry:
227
- return self._component_registry
228
-
229
223
  @property
230
224
  def message_cache(self) -> ForwardMsgCache:
231
225
  return self._message_cache