streamlit-nightly 1.35.1.dev20240529__py2.py3-none-any.whl → 1.35.1.dev20240531__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.
- streamlit/__init__.py +2 -0
- streamlit/commands/execution_control.py +23 -13
- streamlit/commands/navigation.py +191 -0
- streamlit/components/v1/custom_component.py +2 -2
- streamlit/elements/media.py +2 -2
- streamlit/elements/plotly_chart.py +1 -1
- streamlit/elements/widgets/button.py +49 -40
- streamlit/elements/widgets/camera_input.py +1 -1
- streamlit/elements/widgets/chat.py +1 -1
- streamlit/elements/widgets/checkbox.py +1 -1
- streamlit/elements/widgets/color_picker.py +1 -1
- streamlit/elements/widgets/data_editor.py +1 -1
- streamlit/elements/widgets/file_uploader.py +1 -1
- streamlit/elements/widgets/multiselect.py +1 -1
- streamlit/elements/widgets/number_input.py +1 -1
- streamlit/elements/widgets/radio.py +1 -1
- streamlit/elements/widgets/select_slider.py +1 -1
- streamlit/elements/widgets/selectbox.py +1 -1
- streamlit/elements/widgets/slider.py +1 -1
- streamlit/elements/widgets/text_widgets.py +2 -2
- streamlit/elements/widgets/time_widgets.py +2 -2
- streamlit/navigation/__init__.py +13 -0
- streamlit/navigation/page.py +197 -0
- streamlit/proto/AppPage_pb2.py +3 -3
- streamlit/proto/AppPage_pb2.pyi +11 -1
- streamlit/proto/ForwardMsg_pb2.py +10 -9
- streamlit/proto/ForwardMsg_pb2.pyi +17 -5
- streamlit/proto/Navigation_pb2.py +29 -0
- streamlit/proto/Navigation_pb2.pyi +79 -0
- streamlit/proto/NewSession_pb2.py +24 -24
- streamlit/proto/NewSession_pb2.pyi +5 -1
- streamlit/runtime/app_session.py +35 -21
- streamlit/runtime/fragment.py +18 -2
- streamlit/runtime/pages_manager.py +354 -0
- streamlit/runtime/scriptrunner/script_run_context.py +13 -2
- streamlit/runtime/scriptrunner/script_runner.py +23 -37
- streamlit/source_util.py +25 -11
- streamlit/static/asset-manifest.json +4 -4
- streamlit/static/index.html +1 -1
- streamlit/static/static/js/8571.cfc22b99.chunk.js +1 -0
- streamlit/static/static/js/9945.47d54f35.chunk.js +2 -0
- streamlit/static/static/js/{main.206766ee.js → main.707da454.js} +2 -2
- streamlit/testing/v1/app_test.py +7 -1
- streamlit/testing/v1/local_script_runner.py +6 -1
- streamlit/watcher/local_sources_watcher.py +20 -10
- streamlit/web/bootstrap.py +1 -19
- streamlit/web/server/routes.py +23 -25
- streamlit/web/server/server.py +9 -8
- {streamlit_nightly-1.35.1.dev20240529.dist-info → streamlit_nightly-1.35.1.dev20240531.dist-info}/METADATA +1 -1
- {streamlit_nightly-1.35.1.dev20240529.dist-info → streamlit_nightly-1.35.1.dev20240531.dist-info}/RECORD +56 -50
- streamlit/static/static/js/5117.04bfe5d3.chunk.js +0 -1
- streamlit/static/static/js/6950.70fe55c2.chunk.js +0 -2
- /streamlit/static/static/js/{6950.70fe55c2.chunk.js.LICENSE.txt → 9945.47d54f35.chunk.js.LICENSE.txt} +0 -0
- /streamlit/static/static/js/{main.206766ee.js.LICENSE.txt → main.707da454.js.LICENSE.txt} +0 -0
- {streamlit_nightly-1.35.1.dev20240529.data → streamlit_nightly-1.35.1.dev20240531.data}/scripts/streamlit.cmd +0 -0
- {streamlit_nightly-1.35.1.dev20240529.dist-info → streamlit_nightly-1.35.1.dev20240531.dist-info}/WHEEL +0 -0
- {streamlit_nightly-1.35.1.dev20240529.dist-info → streamlit_nightly-1.35.1.dev20240531.dist-info}/entry_points.txt +0 -0
- {streamlit_nightly-1.35.1.dev20240529.dist-info → streamlit_nightly-1.35.1.dev20240531.dist-info}/top_level.txt +0 -0
streamlit/runtime/app_session.py
CHANGED
@@ -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
|
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 =
|
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
|
-
|
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.
|
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,
|
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.
|
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(
|
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"]
|
streamlit/runtime/fragment.py
CHANGED
@@ -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
|
-
|
161
|
-
|
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
|
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
|
|