streamlit-nightly 1.35.1.dev20240530__py2.py3-none-any.whl → 1.35.1.dev20240601__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 (58) hide show
  1. streamlit/__init__.py +2 -0
  2. streamlit/commands/execution_control.py +23 -13
  3. streamlit/commands/navigation.py +191 -0
  4. streamlit/components/v1/custom_component.py +2 -2
  5. streamlit/elements/media.py +2 -2
  6. streamlit/elements/plotly_chart.py +1 -1
  7. streamlit/elements/widgets/button.py +49 -40
  8. streamlit/elements/widgets/camera_input.py +1 -1
  9. streamlit/elements/widgets/chat.py +1 -1
  10. streamlit/elements/widgets/checkbox.py +1 -1
  11. streamlit/elements/widgets/color_picker.py +1 -1
  12. streamlit/elements/widgets/data_editor.py +1 -1
  13. streamlit/elements/widgets/file_uploader.py +1 -1
  14. streamlit/elements/widgets/multiselect.py +1 -1
  15. streamlit/elements/widgets/number_input.py +1 -1
  16. streamlit/elements/widgets/radio.py +1 -1
  17. streamlit/elements/widgets/select_slider.py +1 -1
  18. streamlit/elements/widgets/selectbox.py +1 -1
  19. streamlit/elements/widgets/slider.py +1 -1
  20. streamlit/elements/widgets/text_widgets.py +2 -2
  21. streamlit/elements/widgets/time_widgets.py +2 -2
  22. streamlit/navigation/__init__.py +13 -0
  23. streamlit/navigation/page.py +197 -0
  24. streamlit/proto/AppPage_pb2.py +3 -3
  25. streamlit/proto/AppPage_pb2.pyi +11 -1
  26. streamlit/proto/ForwardMsg_pb2.py +10 -9
  27. streamlit/proto/ForwardMsg_pb2.pyi +17 -5
  28. streamlit/proto/Navigation_pb2.py +29 -0
  29. streamlit/proto/Navigation_pb2.pyi +79 -0
  30. streamlit/proto/NewSession_pb2.py +24 -24
  31. streamlit/proto/NewSession_pb2.pyi +5 -1
  32. streamlit/runtime/app_session.py +35 -21
  33. streamlit/runtime/fragment.py +18 -2
  34. streamlit/runtime/pages_manager.py +354 -0
  35. streamlit/runtime/scriptrunner/script_run_context.py +13 -2
  36. streamlit/runtime/scriptrunner/script_runner.py +23 -37
  37. streamlit/source_util.py +25 -11
  38. streamlit/static/asset-manifest.json +4 -4
  39. streamlit/static/index.html +1 -1
  40. streamlit/static/static/js/8571.cfc22b99.chunk.js +1 -0
  41. streamlit/static/static/js/9945.47d54f35.chunk.js +2 -0
  42. streamlit/static/static/js/{main.9978e612.js → main.707da454.js} +2 -2
  43. streamlit/testing/v1/app_test.py +7 -1
  44. streamlit/testing/v1/local_script_runner.py +6 -1
  45. streamlit/watcher/local_sources_watcher.py +20 -10
  46. streamlit/web/bootstrap.py +1 -19
  47. streamlit/web/server/routes.py +23 -25
  48. streamlit/web/server/server.py +9 -8
  49. {streamlit_nightly-1.35.1.dev20240530.dist-info → streamlit_nightly-1.35.1.dev20240601.dist-info}/METADATA +1 -1
  50. {streamlit_nightly-1.35.1.dev20240530.dist-info → streamlit_nightly-1.35.1.dev20240601.dist-info}/RECORD +56 -50
  51. streamlit/static/static/js/5117.04bfe5d3.chunk.js +0 -1
  52. streamlit/static/static/js/6950.70fe55c2.chunk.js +0 -2
  53. /streamlit/static/static/js/{6950.70fe55c2.chunk.js.LICENSE.txt → 9945.47d54f35.chunk.js.LICENSE.txt} +0 -0
  54. /streamlit/static/static/js/{main.9978e612.js.LICENSE.txt → main.707da454.js.LICENSE.txt} +0 -0
  55. {streamlit_nightly-1.35.1.dev20240530.data → streamlit_nightly-1.35.1.dev20240601.data}/scripts/streamlit.cmd +0 -0
  56. {streamlit_nightly-1.35.1.dev20240530.dist-info → streamlit_nightly-1.35.1.dev20240601.dist-info}/WHEEL +0 -0
  57. {streamlit_nightly-1.35.1.dev20240530.dist-info → streamlit_nightly-1.35.1.dev20240601.dist-info}/entry_points.txt +0 -0
  58. {streamlit_nightly-1.35.1.dev20240530.dist-info → streamlit_nightly-1.35.1.dev20240601.dist-info}/top_level.txt +0 -0
@@ -21,7 +21,7 @@ from enum import Enum
21
21
  from typing import TYPE_CHECKING, Callable, Final
22
22
 
23
23
  import streamlit.elements.exception as exception_utils
24
- from streamlit import config, runtime, source_util
24
+ from streamlit import config, runtime
25
25
  from streamlit.case_converters import to_snake_case
26
26
  from streamlit.logger import get_logger
27
27
  from streamlit.proto.BackMsg_pb2 import BackMsg
@@ -40,11 +40,13 @@ from streamlit.runtime import caching
40
40
  from streamlit.runtime.forward_msg_queue import ForwardMsgQueue
41
41
  from streamlit.runtime.fragment import FragmentStorage, MemoryFragmentStorage
42
42
  from streamlit.runtime.metrics_util import Installation
43
+ from streamlit.runtime.pages_manager import PagesManager
43
44
  from streamlit.runtime.script_data import ScriptData
44
45
  from streamlit.runtime.scriptrunner import RerunData, ScriptRunner, ScriptRunnerEvent
45
46
  from streamlit.runtime.scriptrunner.script_cache import ScriptCache
46
47
  from streamlit.runtime.secrets import secrets_singleton
47
48
  from streamlit.runtime.uploaded_file_manager import UploadedFileManager
49
+ from streamlit.source_util import PageHash, PageInfo
48
50
  from streamlit.version import STREAMLIT_VERSION_STRING
49
51
  from streamlit.watcher import LocalSourcesWatcher
50
52
 
@@ -127,6 +129,9 @@ class AppSession:
127
129
  self._script_data = script_data
128
130
  self._uploaded_file_mgr = uploaded_file_manager
129
131
  self._script_cache = script_cache
132
+ self._pages_manager = PagesManager(
133
+ script_data.main_script_path, self._script_cache
134
+ )
130
135
 
131
136
  # The browser queue contains messages that haven't yet been
132
137
  # delivered to the browser. Periodically, the server flushes
@@ -182,9 +187,7 @@ class AppSession:
182
187
  to.
183
188
  """
184
189
  if self._local_sources_watcher is None:
185
- self._local_sources_watcher = LocalSourcesWatcher(
186
- self._script_data.main_script_path
187
- )
190
+ self._local_sources_watcher = LocalSourcesWatcher(self._pages_manager)
188
191
 
189
192
  self._local_sources_watcher.register_file_change_callback(
190
193
  self._on_source_file_changed
@@ -192,7 +195,7 @@ class AppSession:
192
195
  self._stop_config_listener = config.on_config_parsed(
193
196
  self._on_source_file_changed, force_connect=True
194
197
  )
195
- self._stop_pages_listener = source_util.register_pages_changed_callback(
198
+ self._stop_pages_listener = self._pages_manager.register_pages_changed_callback(
196
199
  self._on_pages_changed
197
200
  )
198
201
  secrets_singleton.file_change_listener.connect(self._on_secrets_file_changed)
@@ -408,6 +411,7 @@ class AppSession:
408
411
  initial_rerun_data=initial_rerun_data,
409
412
  user_info=self._user_info,
410
413
  fragment_storage=self._fragment_storage,
414
+ pages_manager=self._pages_manager,
411
415
  )
412
416
  self._scriptrunner.on_event.connect(self._on_scriptrunner_event)
413
417
  self._scriptrunner.start()
@@ -417,8 +421,7 @@ class AppSession:
417
421
  return self._session_state
418
422
 
419
423
  def _should_rerun_on_file_change(self, filepath: str) -> bool:
420
- main_script_path = self._script_data.main_script_path
421
- pages = source_util.get_pages(main_script_path)
424
+ pages = self._pages_manager.get_pages()
422
425
 
423
426
  changed_page_script_hash = next(
424
427
  filter(lambda k: pages[k]["script_path"] == filepath, pages),
@@ -455,7 +458,7 @@ class AppSession:
455
458
 
456
459
  def _on_pages_changed(self, _) -> None:
457
460
  msg = ForwardMsg()
458
- _populate_app_pages(msg.pages_changed, self._script_data.main_script_path)
461
+ self._populate_app_pages(msg.pages_changed, self._pages_manager.get_pages())
459
462
  self._enqueue_forward_msg(msg)
460
463
 
461
464
  if self._local_sources_watcher is not None:
@@ -473,6 +476,7 @@ class AppSession:
473
476
  client_state: ClientState | None = None,
474
477
  page_script_hash: str | None = None,
475
478
  fragment_ids_this_run: set[str] | None = None,
479
+ pages: dict[PageHash, PageInfo] | None = None,
476
480
  ) -> None:
477
481
  """Called when our ScriptRunner emits an event.
478
482
 
@@ -489,6 +493,7 @@ class AppSession:
489
493
  client_state,
490
494
  page_script_hash,
491
495
  fragment_ids_this_run,
496
+ pages,
492
497
  )
493
498
  )
494
499
 
@@ -501,6 +506,7 @@ class AppSession:
501
506
  client_state: ClientState | None = None,
502
507
  page_script_hash: str | None = None,
503
508
  fragment_ids_this_run: set[str] | None = None,
509
+ pages: dict[PageHash, PageInfo] | None = None,
504
510
  ) -> None:
505
511
  """Handle a ScriptRunner event.
506
512
 
@@ -572,7 +578,7 @@ class AppSession:
572
578
 
573
579
  self._enqueue_forward_msg(
574
580
  self._create_new_session_message(
575
- page_script_hash, fragment_ids_this_run
581
+ page_script_hash, fragment_ids_this_run, pages
576
582
  )
577
583
  )
578
584
 
@@ -603,6 +609,7 @@ class AppSession:
603
609
  # that change which modules should be watched.
604
610
  if self._local_sources_watcher:
605
611
  self._local_sources_watcher.update_watched_modules()
612
+ self._local_sources_watcher.update_watched_pages()
606
613
  else:
607
614
  # The script didn't complete successfully: send the exception
608
615
  # to the frontend.
@@ -666,20 +673,26 @@ class AppSession:
666
673
  return msg
667
674
 
668
675
  def _create_new_session_message(
669
- self, page_script_hash: str, fragment_ids_this_run: set[str] | None = None
676
+ self,
677
+ page_script_hash: str,
678
+ fragment_ids_this_run: set[str] | None = None,
679
+ pages: dict[PageHash, PageInfo] | None = None,
670
680
  ) -> ForwardMsg:
671
681
  """Create and return a new_session ForwardMsg."""
672
682
  msg = ForwardMsg()
673
683
 
674
684
  msg.new_session.script_run_id = _generate_scriptrun_id()
675
685
  msg.new_session.name = self._script_data.name
676
- msg.new_session.main_script_path = self._script_data.main_script_path
686
+ msg.new_session.main_script_path = self._pages_manager.main_script_path
687
+ msg.new_session.main_script_hash = self._pages_manager.main_script_hash
677
688
  msg.new_session.page_script_hash = page_script_hash
678
689
 
679
690
  if fragment_ids_this_run:
680
691
  msg.new_session.fragment_ids_this_run.extend(fragment_ids_this_run)
681
692
 
682
- _populate_app_pages(msg.new_session, self._script_data.main_script_path)
693
+ self._populate_app_pages(
694
+ msg.new_session, pages or self._pages_manager.get_pages()
695
+ )
683
696
  _populate_config_msg(msg.new_session.config)
684
697
  _populate_theme_msg(msg.new_session.custom_theme)
685
698
 
@@ -829,6 +842,16 @@ class AppSession:
829
842
 
830
843
  self._enqueue_forward_msg(msg)
831
844
 
845
+ def _populate_app_pages(
846
+ self, msg: NewSession | PagesChanged, pages: dict[PageHash, PageInfo]
847
+ ) -> None:
848
+ for page_script_hash, page_info in pages.items():
849
+ page_proto = msg.app_pages.add()
850
+
851
+ page_proto.page_script_hash = page_script_hash
852
+ page_proto.page_name = page_info["page_name"]
853
+ page_proto.icon = page_info["icon"]
854
+
832
855
 
833
856
  # Config.ToolbarMode.ValueType does not exist at runtime (only in the pyi stubs), so
834
857
  # we need to use quotes.
@@ -913,12 +936,3 @@ def _populate_theme_msg(msg: CustomThemeConfig) -> None:
913
936
  def _populate_user_info_msg(msg: UserInfo) -> None:
914
937
  msg.installation_id = Installation.instance().installation_id
915
938
  msg.installation_id_v3 = Installation.instance().installation_id_v3
916
-
917
-
918
- def _populate_app_pages(msg: NewSession | PagesChanged, main_script_path: str) -> None:
919
- for page_script_hash, page_info in source_util.get_pages(main_script_path).items():
920
- page_proto = msg.app_pages.add()
921
-
922
- page_proto.page_script_hash = page_script_hash
923
- page_proto.page_name = page_info["page_name"]
924
- page_proto.icon = page_info["icon"]
@@ -133,6 +133,10 @@ def _fragment(
133
133
  )
134
134
  fragment_id = h.hexdigest()
135
135
 
136
+ # We intentionally want to capture the active script hash here to ensure
137
+ # that the fragment is associated with the correct script running.
138
+ initialized_active_script_hash = ctx.active_script_hash
139
+
136
140
  def wrapped_fragment():
137
141
  import streamlit as st
138
142
 
@@ -157,8 +161,20 @@ def _fragment(
157
161
  ctx.current_fragment_id = fragment_id
158
162
 
159
163
  try:
160
- with st.container():
161
- result = non_optional_func(*args, **kwargs)
164
+ # Make sure we set the active script hash to the same value
165
+ # for the fragment run as when defined upon initialization
166
+ # This ensures that elements (especially widgets) are tied
167
+ # to a consistent active script hash
168
+ active_hash_context = (
169
+ ctx.pages_manager.run_with_active_hash(
170
+ initialized_active_script_hash
171
+ )
172
+ if initialized_active_script_hash != ctx.active_script_hash
173
+ else contextlib.nullcontext()
174
+ )
175
+ with active_hash_context:
176
+ with st.container():
177
+ result = non_optional_func(*args, **kwargs)
162
178
  finally:
163
179
  ctx.current_fragment_id = None
164
180
 
@@ -0,0 +1,354 @@
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 os
19
+ import threading
20
+ from pathlib import Path
21
+ from typing import TYPE_CHECKING, Any, Callable, Final, Type
22
+
23
+ from streamlit import source_util
24
+ from streamlit.logger import get_logger
25
+ from streamlit.runtime.scriptrunner.script_cache import ScriptCache
26
+ from streamlit.util import calc_md5
27
+ from streamlit.watcher import watch_dir
28
+
29
+ if TYPE_CHECKING:
30
+ from streamlit.source_util import PageHash, PageInfo, PageName, ScriptPath
31
+
32
+ _LOGGER: Final = get_logger(__name__)
33
+
34
+
35
+ class PagesStrategyV1:
36
+ """
37
+ Strategy for MPA v1. This strategy handles pages being set directly
38
+ by a call to `st.navigation`. The key differences here are:
39
+ - The pages are defined by the existence of a `pages` directory
40
+ - We will ensure one watcher is watching the scripts in the directory.
41
+ - Only one script runs for a full rerun.
42
+ - We know at the beginning the intended page script to run.
43
+
44
+ NOTE: Thread safety of the pages is handled by the source_util module
45
+ """
46
+
47
+ is_watching_pages_dir: bool = False
48
+ pages_watcher_lock = threading.Lock()
49
+
50
+ # This is a static method because we only want to watch the pages directory
51
+ # once on initial load.
52
+ @staticmethod
53
+ def watch_pages_dir(pages_manager: PagesManager):
54
+ with PagesStrategyV1.pages_watcher_lock:
55
+ if PagesStrategyV1.is_watching_pages_dir:
56
+ return
57
+
58
+ def _handle_page_changed(_path: str) -> None:
59
+ source_util.invalidate_pages_cache()
60
+
61
+ main_script_path = Path(pages_manager.main_script_path)
62
+ pages_dir = main_script_path.parent / "pages"
63
+ watch_dir(
64
+ str(pages_dir),
65
+ _handle_page_changed,
66
+ glob_pattern="*.py",
67
+ allow_nonexistent=True,
68
+ )
69
+ PagesStrategyV1.is_watching_pages_dir = True
70
+
71
+ def __init__(self, pages_manager: PagesManager, setup_watcher: bool = True):
72
+ self.pages_manager = pages_manager
73
+
74
+ if setup_watcher:
75
+ PagesStrategyV1.watch_pages_dir(pages_manager)
76
+
77
+ # In MPA v1, there's no difference between the active hash
78
+ # and the page script hash.
79
+ def get_active_script_hash(self) -> "PageHash":
80
+ return self.pages_manager.current_page_hash
81
+
82
+ def set_active_script_hash(self, _page_hash: "PageHash"):
83
+ # Intentionally do nothing as MPA v1 active_script_hash does not
84
+ # differentiate the active_script_hash and the page_script_hash
85
+ pass
86
+
87
+ def get_initial_active_script(
88
+ self, page_script_hash: "PageHash", page_name: "PageName"
89
+ ) -> "PageInfo" | None:
90
+ pages = self.get_pages()
91
+
92
+ if page_script_hash:
93
+ return pages.get(page_script_hash, None)
94
+ elif not page_script_hash and page_name:
95
+ # If a user navigates directly to a non-main page of an app, we get
96
+ # the first script run request before the list of pages has been
97
+ # sent to the frontend. In this case, we choose the first script
98
+ # with a name matching the requested page name.
99
+ return next(
100
+ filter(
101
+ # There seems to be this weird bug with mypy where it
102
+ # thinks that p can be None (which is impossible given the
103
+ # types of pages), so we add `p and` at the beginning of
104
+ # the predicate to circumvent this.
105
+ lambda p: p and (p["page_name"] == page_name),
106
+ pages.values(),
107
+ ),
108
+ None,
109
+ )
110
+
111
+ # If no information about what page to run is given, default to
112
+ # running the main page.
113
+ # Safe because pages will at least contain the app's main page.
114
+ main_page_info = list(pages.values())[0]
115
+ return main_page_info
116
+
117
+ def get_pages(self) -> dict["PageHash", "PageInfo"]:
118
+ return source_util.get_pages(self.pages_manager.main_script_path)
119
+
120
+ def register_pages_changed_callback(
121
+ self,
122
+ callback: Callable[[str], None],
123
+ ) -> Callable[[], None]:
124
+ return source_util.register_pages_changed_callback(callback)
125
+
126
+ def set_pages(self, _pages: dict["PageHash", "PageInfo"]) -> None:
127
+ raise NotImplementedError("Unable to set pages in this V1 strategy")
128
+
129
+ def get_page_script(self, _fallback_page_hash: "PageHash") -> "PageInfo" | None:
130
+ raise NotImplementedError("Unable to get page script in this V1 strategy")
131
+
132
+
133
+ class PagesStrategyV2:
134
+ """
135
+ Strategy for MPA v2. This strategy handles pages being set directly
136
+ by a call to `st.navigation`. The key differences here are:
137
+ - The pages are set directly by the user
138
+ - The initial active script will always be the main script
139
+ - More than one script can run in a single app run (sequentially),
140
+ so we must keep track of the active script hash
141
+ - We rely on pages manager to retrieve the intended page script per run
142
+
143
+ NOTE: We don't provide any locks on the pages since the pages are not
144
+ shared across sessions. Only the user script thread can write to
145
+ pages and the event loop thread only reads
146
+ """
147
+
148
+ def __init__(self, pages_manager: PagesManager, **kwargs):
149
+ self.pages_manager = pages_manager
150
+ self._active_script_hash: "PageHash" = self.pages_manager.main_script_hash
151
+ self._pages: dict["PageHash", "PageInfo"] | None = None
152
+
153
+ def get_active_script_hash(self) -> "PageHash":
154
+ return self._active_script_hash
155
+
156
+ def set_active_script_hash(self, page_hash: "PageHash"):
157
+ self._active_script_hash = page_hash
158
+
159
+ def get_initial_active_script(
160
+ self, page_script_hash: "PageHash", page_name: "PageName"
161
+ ) -> "PageInfo":
162
+ return {
163
+ # We always run the main script in V2 as it's the common code
164
+ "script_path": self.pages_manager.main_script_path,
165
+ "page_script_hash": page_script_hash
166
+ or self.pages_manager.main_script_hash, # Default Hash
167
+ }
168
+
169
+ def get_page_script(self, fallback_page_hash: "PageHash") -> "PageInfo" | None:
170
+ if self._pages is None:
171
+ return None
172
+
173
+ if self.pages_manager.intended_page_script_hash:
174
+ # We assume that if initial page hash is specified, that a page should
175
+ # exist, so we check out the page script hash or the default page hash
176
+ # as a backup
177
+ return self._pages.get(
178
+ self.pages_manager.intended_page_script_hash,
179
+ self._pages.get(fallback_page_hash, None),
180
+ )
181
+ elif self.pages_manager.intended_page_name:
182
+ # If a user navigates directly to a non-main page of an app, the
183
+ # the page name can identify the page script to run
184
+ return next(
185
+ filter(
186
+ # There seems to be this weird bug with mypy where it
187
+ # thinks that p can be None (which is impossible given the
188
+ # types of pages), so we add `p and` at the beginning of
189
+ # the predicate to circumvent this.
190
+ lambda p: p
191
+ and (p["url_pathname"] == self.pages_manager.intended_page_name),
192
+ self._pages.values(),
193
+ ),
194
+ None,
195
+ )
196
+
197
+ return self._pages.get(fallback_page_hash, None)
198
+
199
+ def get_pages(self) -> dict["PageHash", "PageInfo"]:
200
+ # If pages are not set, provide the common page info where
201
+ # - the main script path is the executing script to start
202
+ # - the page script hash and name reflects the intended page requested
203
+ return self._pages or {
204
+ self.pages_manager.main_script_hash: {
205
+ "page_script_hash": self.pages_manager.intended_page_script_hash or "",
206
+ "page_name": self.pages_manager.intended_page_name or "",
207
+ "icon": "",
208
+ "script_path": self.pages_manager.main_script_path,
209
+ }
210
+ }
211
+
212
+ def set_pages(self, pages: dict["PageHash", "PageInfo"]) -> None:
213
+ self._pages = pages
214
+
215
+ def register_pages_changed_callback(
216
+ self,
217
+ callback: Callable[[str], None],
218
+ ) -> Callable[[], None]:
219
+ # V2 strategy does not handle any pages changed event
220
+ return lambda: None
221
+
222
+
223
+ class PagesManager:
224
+ """
225
+ PagesManager is responsible for managing the set of pages based on the
226
+ strategy. By default, PagesManager uses V1 which relies on the original
227
+ assumption that there exists a `pages` directory with all the scripts.
228
+
229
+ If the `pages` are being set directly, the strategy is switched to V2.
230
+ This indicates someone has written an `st.navigation` call in their app
231
+ which informs us of the pages.
232
+
233
+ NOTE: Each strategy handles its own thread safety when accessing the pages
234
+ """
235
+
236
+ DefaultStrategy: Type[PagesStrategyV1 | PagesStrategyV2] = PagesStrategyV1
237
+
238
+ def __init__(
239
+ self,
240
+ main_script_path: "ScriptPath",
241
+ script_cache: ScriptCache | None = None,
242
+ **kwargs,
243
+ ):
244
+ self._main_script_path = main_script_path
245
+ self._main_script_hash: "PageHash" = calc_md5(main_script_path)
246
+ self._current_page_hash: "PageHash" = self._main_script_hash
247
+ self.pages_strategy = PagesManager.DefaultStrategy(self, **kwargs)
248
+ self._script_cache = script_cache
249
+ self._intended_page_script_hash: "PageHash" | None = None
250
+ self._intended_page_name: "PageName" | None = None
251
+
252
+ @property
253
+ def current_page_hash(self) -> "PageHash":
254
+ return self._current_page_hash
255
+
256
+ @property
257
+ def main_script_path(self) -> "ScriptPath":
258
+ return self._main_script_path
259
+
260
+ @property
261
+ def main_script_hash(self) -> "PageHash":
262
+ return self._main_script_hash
263
+
264
+ @property
265
+ def intended_page_name(self) -> "PageName" | None:
266
+ return self._intended_page_name
267
+
268
+ @property
269
+ def intended_page_script_hash(self) -> "PageHash" | None:
270
+ return self._intended_page_script_hash
271
+
272
+ def get_main_page(self) -> "PageInfo":
273
+ return {
274
+ "script_path": self._main_script_path,
275
+ "page_script_hash": self._main_script_hash,
276
+ }
277
+
278
+ def get_current_page_script_hash(self) -> "PageHash":
279
+ """Gets the script hash of the associated page of a script."""
280
+ return self._current_page_hash
281
+
282
+ def set_current_page_script_hash(self, page_hash: "PageHash") -> None:
283
+ self._current_page_hash = page_hash
284
+
285
+ def get_active_script_hash(self) -> "PageHash":
286
+ """Gets the script hash of the currently executing script."""
287
+ return self.pages_strategy.get_active_script_hash()
288
+
289
+ def set_active_script_hash(self, page_hash: "PageHash"):
290
+ return self.pages_strategy.set_active_script_hash(page_hash)
291
+
292
+ def set_script_intent(
293
+ self, page_script_hash: "PageHash", page_name: "PageName"
294
+ ) -> None:
295
+ self._intended_page_script_hash = page_script_hash
296
+ self._intended_page_name = page_name
297
+
298
+ def get_initial_active_script(
299
+ self, page_script_hash: "PageHash", page_name: "PageName"
300
+ ) -> "PageInfo" | None:
301
+ return self.pages_strategy.get_initial_active_script(
302
+ page_script_hash, page_name
303
+ )
304
+
305
+ @contextlib.contextmanager
306
+ def run_with_active_hash(self, page_hash: "PageHash"):
307
+ original_page_hash = self.get_active_script_hash()
308
+ self.set_active_script_hash(page_hash)
309
+ try:
310
+ yield
311
+ finally:
312
+ # in the event of any exception, ensure we set the active hash back
313
+ self.set_active_script_hash(original_page_hash)
314
+
315
+ def get_pages(self) -> dict["PageHash", "PageInfo"]:
316
+ return self.pages_strategy.get_pages()
317
+
318
+ def set_pages(self, pages: dict["PageHash", "PageInfo"]) -> None:
319
+ # Manually setting the pages indicates we are using MPA v2.
320
+ if isinstance(self.pages_strategy, PagesStrategyV1):
321
+ if os.path.exists(Path(self.main_script_path).parent / "pages"):
322
+ _LOGGER.warning(
323
+ "st.navigation was called in an app with a pages/ directory. This may cause unusual app behavior. You may want to rename the pages/ directory."
324
+ )
325
+ PagesManager.DefaultStrategy = PagesStrategyV2
326
+ self.pages_strategy = PagesStrategyV2(self)
327
+
328
+ self.pages_strategy.set_pages(pages)
329
+
330
+ def get_page_script(self, fallback_page_hash: "PageHash" = "") -> "PageInfo" | None:
331
+ # We assume the pages strategy is V2 cause this is used
332
+ # in the st.navigation call, but we just swallow the error
333
+ try:
334
+ return self.pages_strategy.get_page_script(fallback_page_hash)
335
+ except NotImplementedError:
336
+ return None
337
+
338
+ def register_pages_changed_callback(
339
+ self,
340
+ callback: Callable[[str], None],
341
+ ) -> Callable[[], None]:
342
+ """Register a callback to be called when the set of pages changes.
343
+
344
+ The callback will be called with the path changed.
345
+ """
346
+
347
+ return self.pages_strategy.register_pages_changed_callback(callback)
348
+
349
+ def get_page_script_byte_code(self, script_path: str) -> Any:
350
+ if self._script_cache is None:
351
+ # Returning an empty string for an empty script
352
+ return ""
353
+
354
+ return self._script_cache.get_bytecode(script_path)
@@ -33,6 +33,7 @@ from streamlit.runtime.uploaded_file_manager import UploadedFileManager
33
33
 
34
34
  if TYPE_CHECKING:
35
35
  from streamlit.runtime.fragment import FragmentStorage
36
+ from streamlit.runtime.pages_manager import PagesManager
36
37
 
37
38
  _LOGGER: Final = get_logger(__name__)
38
39
 
@@ -60,9 +61,9 @@ class ScriptRunContext:
60
61
  session_state: SafeSessionState
61
62
  uploaded_file_mgr: UploadedFileManager
62
63
  main_script_path: str
63
- page_script_hash: str
64
64
  user_info: UserInfo
65
65
  fragment_storage: "FragmentStorage"
66
+ pages_manager: "PagesManager"
66
67
 
67
68
  gather_usage_stats: bool = False
68
69
  command_tracking_deactivated: bool = False
@@ -87,6 +88,14 @@ class ScriptRunContext:
87
88
  _experimental_query_params_used = False
88
89
  _production_query_params_used = False
89
90
 
91
+ @property
92
+ def page_script_hash(self):
93
+ return self.pages_manager.get_current_page_script_hash()
94
+
95
+ @property
96
+ def active_script_hash(self):
97
+ return self.pages_manager.get_active_script_hash()
98
+
90
99
  def reset(
91
100
  self,
92
101
  query_string: str = "",
@@ -98,7 +107,7 @@ class ScriptRunContext:
98
107
  self.widget_user_keys_this_run = set()
99
108
  self.form_ids_this_run = set()
100
109
  self.query_string = query_string
101
- self.page_script_hash = page_script_hash
110
+ self.pages_manager.set_current_page_script_hash(page_script_hash)
102
111
  # Permit set_page_config when the ScriptRunContext is reused on a rerun
103
112
  self._set_page_config_allowed = True
104
113
  self._has_script_started = False
@@ -142,6 +151,8 @@ class ScriptRunContext:
142
151
  ):
143
152
  self._set_page_config_allowed = False
144
153
 
154
+ msg.metadata.active_script_hash = self.active_script_hash
155
+
145
156
  # Pass the message up to our associated ScriptRunner.
146
157
  self._enqueue(msg)
147
158