Mise à jour de Monitor.py et autres scripts
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""The modules in this package are separated from
|
||||
the scriptrunner-package, because they are more or less
|
||||
standalone and other modules import them quite frequently.
|
||||
This separation helps us to remove dependency cycles.
|
||||
"""
|
||||
@@ -0,0 +1,48 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from streamlit.runtime.scriptrunner_utils.script_requests import RerunData
|
||||
from streamlit.util import repr_
|
||||
|
||||
|
||||
# We inherit from BaseException to avoid being caught by user code.
|
||||
# For example, having it inherit from Exception might make st.rerun not
|
||||
# work in a try/except block.
|
||||
class ScriptControlException(BaseException): # NOSONAR
|
||||
"""Base exception for ScriptRunner."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class StopException(ScriptControlException):
|
||||
"""Silently stop the execution of the user's script."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RerunException(ScriptControlException):
|
||||
"""Silently stop and rerun the user's script."""
|
||||
|
||||
def __init__(self, rerun_data: RerunData):
|
||||
"""Construct a RerunException.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
rerun_data : RerunData
|
||||
The RerunData that should be used to rerun the script
|
||||
"""
|
||||
self.rerun_data = rerun_data
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return repr_(self)
|
||||
@@ -0,0 +1,305 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from dataclasses import dataclass, field, replace
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from streamlit import util
|
||||
from streamlit.proto.Common_pb2 import ChatInputValue as ChatInputValueProto
|
||||
from streamlit.proto.WidgetStates_pb2 import WidgetState, WidgetStates
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.proto.ClientState_pb2 import ContextInfo
|
||||
|
||||
|
||||
class ScriptRequestType(Enum):
|
||||
# The ScriptRunner should continue running its script.
|
||||
CONTINUE = "CONTINUE"
|
||||
|
||||
# If the script is running, it should be stopped as soon
|
||||
# as the ScriptRunner reaches an interrupt point.
|
||||
# This is a terminal state.
|
||||
STOP = "STOP"
|
||||
|
||||
# A script rerun has been requested. The ScriptRunner should
|
||||
# handle this request as soon as it reaches an interrupt point.
|
||||
RERUN = "RERUN"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RerunData:
|
||||
"""Data attached to RERUN requests. Immutable."""
|
||||
|
||||
query_string: str = ""
|
||||
widget_states: WidgetStates | None = None
|
||||
page_script_hash: str = ""
|
||||
page_name: str = ""
|
||||
|
||||
# A single fragment_id to append to fragment_id_queue.
|
||||
fragment_id: str | None = None
|
||||
# The queue of fragment_ids waiting to be run.
|
||||
fragment_id_queue: list[str] = field(default_factory=list)
|
||||
is_fragment_scoped_rerun: bool = False
|
||||
# set to true when a script is rerun by the fragment auto-rerun mechanism
|
||||
is_auto_rerun: bool = False
|
||||
# context_info is used to store information from the user browser (e.g. timezone)
|
||||
context_info: ContextInfo | None = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return util.repr_(self)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ScriptRequest:
|
||||
"""A STOP or RERUN request and associated data."""
|
||||
|
||||
type: ScriptRequestType
|
||||
_rerun_data: RerunData | None = None
|
||||
|
||||
@property
|
||||
def rerun_data(self) -> RerunData:
|
||||
if self.type is not ScriptRequestType.RERUN:
|
||||
raise RuntimeError("RerunData is only set for RERUN requests.")
|
||||
return cast("RerunData", self._rerun_data)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return util.repr_(self)
|
||||
|
||||
|
||||
def _fragment_run_should_not_preempt_script(
|
||||
fragment_id_queue: list[str],
|
||||
is_fragment_scoped_rerun: bool,
|
||||
) -> bool:
|
||||
"""Returns whether the currently running script should be preempted due to a
|
||||
fragment rerun.
|
||||
|
||||
Reruns corresponding to fragment runs that weren't caused by calls to
|
||||
`st.rerun(scope="fragment")` should *not* cancel the current script run
|
||||
as doing so will affect elements outside of the fragment.
|
||||
"""
|
||||
return bool(fragment_id_queue) and not is_fragment_scoped_rerun
|
||||
|
||||
|
||||
def _coalesce_widget_states(
|
||||
old_states: WidgetStates | None, new_states: WidgetStates | None
|
||||
) -> WidgetStates | None:
|
||||
"""Coalesce an older WidgetStates into a newer one, and return a new
|
||||
WidgetStates containing the result.
|
||||
|
||||
For most widget values, we just take the latest version.
|
||||
|
||||
However, any trigger_values (which are set by buttons) that are True in
|
||||
`old_states` will be set to True in the coalesced result, so that button
|
||||
presses don't go missing.
|
||||
"""
|
||||
if not old_states and not new_states:
|
||||
return None
|
||||
elif not old_states:
|
||||
return new_states
|
||||
elif not new_states:
|
||||
return old_states
|
||||
|
||||
states_by_id: dict[str, WidgetState] = {
|
||||
wstate.id: wstate for wstate in new_states.widgets
|
||||
}
|
||||
|
||||
trigger_value_types = [
|
||||
("trigger_value", False),
|
||||
("chat_input_value", ChatInputValueProto(data=None)),
|
||||
]
|
||||
for old_state in old_states.widgets:
|
||||
for trigger_value_type, unset_value in trigger_value_types:
|
||||
if (
|
||||
old_state.WhichOneof("value") == trigger_value_type
|
||||
and getattr(old_state, trigger_value_type) != unset_value
|
||||
):
|
||||
new_trigger_val = states_by_id.get(old_state.id)
|
||||
# It should nearly always be the case that new_trigger_val is None
|
||||
# here as trigger values are deleted from the client's WidgetStateManager
|
||||
# as soon as a rerun_script BackMsg is sent to the server. Since it's
|
||||
# impossible to test that the client sends us state in the expected
|
||||
# format in a unit test, we test for this behavior in
|
||||
# e2e_playwright/test_fragment_queue_test.py
|
||||
if not new_trigger_val or (
|
||||
# Ensure the corresponding new_state is also a trigger;
|
||||
# otherwise, a widget that was previously a button/chat_input but no
|
||||
# longer is could get a bad value.
|
||||
new_trigger_val.WhichOneof("value") == trigger_value_type
|
||||
# We only want to take the value of old_state if new_trigger_val is
|
||||
# unset as the old value may be stale if a newer one was entered.
|
||||
and getattr(new_trigger_val, trigger_value_type) == unset_value
|
||||
):
|
||||
states_by_id[old_state.id] = old_state
|
||||
|
||||
coalesced = WidgetStates()
|
||||
coalesced.widgets.extend(states_by_id.values())
|
||||
|
||||
return coalesced
|
||||
|
||||
|
||||
class ScriptRequests:
|
||||
"""An interface for communicating with a ScriptRunner. Thread-safe.
|
||||
|
||||
AppSession makes requests of a ScriptRunner through this class, and
|
||||
ScriptRunner handles those requests.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._lock = threading.Lock()
|
||||
self._state = ScriptRequestType.CONTINUE
|
||||
self._rerun_data = RerunData()
|
||||
|
||||
def request_stop(self) -> None:
|
||||
"""Request that the ScriptRunner stop running. A stopped ScriptRunner
|
||||
can't be used anymore. STOP requests succeed unconditionally.
|
||||
"""
|
||||
with self._lock:
|
||||
self._state = ScriptRequestType.STOP
|
||||
|
||||
def request_rerun(self, new_data: RerunData) -> bool:
|
||||
"""Request that the ScriptRunner rerun its script.
|
||||
|
||||
If the ScriptRunner has been stopped, this request can't be honored:
|
||||
return False.
|
||||
|
||||
Otherwise, record the request and return True. The ScriptRunner will
|
||||
handle the rerun request as soon as it reaches an interrupt point.
|
||||
"""
|
||||
|
||||
with self._lock:
|
||||
if self._state == ScriptRequestType.STOP:
|
||||
# We can't rerun after being stopped.
|
||||
return False
|
||||
|
||||
if self._state == ScriptRequestType.CONTINUE:
|
||||
# The script is currently running, and we haven't received a request to
|
||||
# rerun it as of yet. We can handle a rerun request unconditionally so
|
||||
# just change self._state and set self._rerun_data.
|
||||
self._state = ScriptRequestType.RERUN
|
||||
|
||||
# Convert from a single fragment_id into fragment_id_queue.
|
||||
if new_data.fragment_id:
|
||||
new_data = replace(
|
||||
new_data,
|
||||
fragment_id=None,
|
||||
fragment_id_queue=[new_data.fragment_id],
|
||||
)
|
||||
|
||||
self._rerun_data = new_data
|
||||
return True
|
||||
|
||||
if self._state == ScriptRequestType.RERUN:
|
||||
# We already have an existing Rerun request, so we can coalesce the new
|
||||
# rerun request into the existing one.
|
||||
|
||||
coalesced_states = _coalesce_widget_states(
|
||||
self._rerun_data.widget_states, new_data.widget_states
|
||||
)
|
||||
|
||||
if new_data.fragment_id:
|
||||
# This RERUN request corresponds to a new fragment run. We append
|
||||
# the new fragment ID to the end of the current fragment_id_queue if
|
||||
# it isn't already contained in it.
|
||||
fragment_id_queue = [*self._rerun_data.fragment_id_queue]
|
||||
|
||||
if new_data.fragment_id not in fragment_id_queue:
|
||||
fragment_id_queue.append(new_data.fragment_id)
|
||||
elif new_data.fragment_id_queue:
|
||||
# new_data contains a new fragment_id_queue, so we just use it.
|
||||
fragment_id_queue = new_data.fragment_id_queue
|
||||
else:
|
||||
# Otherwise, this is a request to rerun the full script, so we want
|
||||
# to clear out any fragments we have queued to run since they'll all
|
||||
# be run with the full script anyway.
|
||||
fragment_id_queue = []
|
||||
|
||||
self._rerun_data = RerunData(
|
||||
query_string=new_data.query_string,
|
||||
widget_states=coalesced_states,
|
||||
page_script_hash=new_data.page_script_hash,
|
||||
page_name=new_data.page_name,
|
||||
fragment_id_queue=fragment_id_queue,
|
||||
is_fragment_scoped_rerun=new_data.is_fragment_scoped_rerun,
|
||||
is_auto_rerun=new_data.is_auto_rerun,
|
||||
context_info=new_data.context_info,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
# We'll never get here
|
||||
raise RuntimeError(f"Unrecognized ScriptRunnerState: {self._state}")
|
||||
|
||||
def on_scriptrunner_yield(self) -> ScriptRequest | None:
|
||||
"""Called by the ScriptRunner when it's at a yield point.
|
||||
|
||||
If we have no request or a RERUN request corresponding to one or more fragments
|
||||
(that is not a fragment-scoped rerun), return None.
|
||||
|
||||
If we have a (full script or fragment-scoped) RERUN request, return the request
|
||||
and set our internal state to CONTINUE.
|
||||
|
||||
If we have a STOP request, return the request and remain stopped.
|
||||
"""
|
||||
if self._state == ScriptRequestType.CONTINUE or (
|
||||
self._state == ScriptRequestType.RERUN
|
||||
and _fragment_run_should_not_preempt_script(
|
||||
self._rerun_data.fragment_id_queue,
|
||||
self._rerun_data.is_fragment_scoped_rerun,
|
||||
)
|
||||
):
|
||||
# We avoid taking the lock in the common cases described above. If a STOP or
|
||||
# preempting RERUN request is received after we've taken this code path, it
|
||||
# will be handled at the next `on_scriptrunner_yield`, or when
|
||||
# `on_scriptrunner_ready` is called.
|
||||
return None
|
||||
|
||||
with self._lock:
|
||||
if self._state == ScriptRequestType.RERUN:
|
||||
# We already made this check in the fast-path above but need to do so
|
||||
# again in case our state changed while we were waiting on the lock.
|
||||
if _fragment_run_should_not_preempt_script(
|
||||
self._rerun_data.fragment_id_queue,
|
||||
self._rerun_data.is_fragment_scoped_rerun,
|
||||
):
|
||||
return None
|
||||
|
||||
self._state = ScriptRequestType.CONTINUE
|
||||
return ScriptRequest(ScriptRequestType.RERUN, self._rerun_data)
|
||||
|
||||
assert self._state == ScriptRequestType.STOP
|
||||
return ScriptRequest(ScriptRequestType.STOP)
|
||||
|
||||
def on_scriptrunner_ready(self) -> ScriptRequest:
|
||||
"""Called by the ScriptRunner when it's about to run its script for
|
||||
the first time, and also after its script has successfully completed.
|
||||
|
||||
If we have a RERUN request, return the request and set
|
||||
our internal state to CONTINUE.
|
||||
|
||||
If we have a STOP request or no request, set our internal state
|
||||
to STOP.
|
||||
"""
|
||||
with self._lock:
|
||||
if self._state == ScriptRequestType.RERUN:
|
||||
self._state = ScriptRequestType.CONTINUE
|
||||
return ScriptRequest(ScriptRequestType.RERUN, self._rerun_data)
|
||||
|
||||
# If we don't have a rerun request, unconditionally change our
|
||||
# state to STOP.
|
||||
self._state = ScriptRequestType.STOP
|
||||
return ScriptRequest(ScriptRequestType.STOP)
|
||||
@@ -0,0 +1,288 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import contextlib
|
||||
import contextvars
|
||||
import threading
|
||||
from collections import Counter
|
||||
from dataclasses import dataclass, field
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Callable,
|
||||
Final,
|
||||
Union,
|
||||
)
|
||||
from urllib import parse
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit.errors import (
|
||||
NoSessionContext,
|
||||
StreamlitAPIException,
|
||||
StreamlitSetPageConfigMustBeFirstCommandError,
|
||||
)
|
||||
from streamlit.logger import get_logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
from streamlit.cursor import RunningCursor
|
||||
from streamlit.proto.ClientState_pb2 import ContextInfo
|
||||
from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
|
||||
from streamlit.proto.PageProfile_pb2 import Command
|
||||
from streamlit.runtime.fragment import FragmentStorage
|
||||
from streamlit.runtime.pages_manager import PagesManager
|
||||
from streamlit.runtime.scriptrunner_utils.script_requests import ScriptRequests
|
||||
from streamlit.runtime.state import SafeSessionState
|
||||
from streamlit.runtime.uploaded_file_manager import UploadedFileManager
|
||||
_LOGGER: Final = get_logger(__name__)
|
||||
|
||||
UserInfo: TypeAlias = dict[str, Union[str, bool, None]]
|
||||
|
||||
|
||||
# If true, it indicates that we are in a cached function that disallows the usage of
|
||||
# widgets. Using contextvars to be thread-safe.
|
||||
in_cached_function: contextvars.ContextVar[bool] = contextvars.ContextVar(
|
||||
"in_cached_function", default=False
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScriptRunContext:
|
||||
"""A context object that contains data for a "script run" - that is,
|
||||
data that's scoped to a single ScriptRunner execution (and therefore also
|
||||
scoped to a single connected "session").
|
||||
|
||||
ScriptRunContext is used internally by virtually every `st.foo()` function.
|
||||
It is accessed only from the script thread that's created by ScriptRunner,
|
||||
or from app-created helper threads that have been "attached" to the
|
||||
ScriptRunContext via `add_script_run_ctx`.
|
||||
|
||||
Streamlit code typically retrieves the active ScriptRunContext via the
|
||||
`get_script_run_ctx` function.
|
||||
"""
|
||||
|
||||
session_id: str
|
||||
_enqueue: Callable[[ForwardMsg], None]
|
||||
query_string: str
|
||||
session_state: SafeSessionState
|
||||
uploaded_file_mgr: UploadedFileManager
|
||||
main_script_path: str
|
||||
user_info: UserInfo
|
||||
fragment_storage: FragmentStorage
|
||||
pages_manager: PagesManager
|
||||
|
||||
context_info: ContextInfo | None = None
|
||||
gather_usage_stats: bool = False
|
||||
command_tracking_deactivated: bool = False
|
||||
tracked_commands: list[Command] = field(default_factory=list)
|
||||
tracked_commands_counter: Counter[str] = field(default_factory=collections.Counter)
|
||||
_set_page_config_allowed: bool = True
|
||||
_has_script_started: bool = False
|
||||
widget_ids_this_run: set[str] = field(default_factory=set)
|
||||
widget_user_keys_this_run: set[str] = field(default_factory=set)
|
||||
form_ids_this_run: set[str] = field(default_factory=set)
|
||||
cursors: dict[int, RunningCursor] = field(default_factory=dict)
|
||||
script_requests: ScriptRequests | None = None
|
||||
current_fragment_id: str | None = None
|
||||
fragment_ids_this_run: list[str] | None = None
|
||||
new_fragment_ids: set[str] = field(default_factory=set)
|
||||
_active_script_hash: str = ""
|
||||
# we allow only one dialog to be open at the same time
|
||||
has_dialog_opened: bool = False
|
||||
|
||||
# TODO(willhuang1997): Remove this variable when experimental query params are removed
|
||||
_experimental_query_params_used = False
|
||||
_production_query_params_used = False
|
||||
|
||||
@property
|
||||
def page_script_hash(self):
|
||||
return self.pages_manager.current_page_script_hash
|
||||
|
||||
@property
|
||||
def active_script_hash(self):
|
||||
return self._active_script_hash
|
||||
|
||||
@property
|
||||
def main_script_parent(self) -> Path:
|
||||
return self.pages_manager.main_script_parent
|
||||
|
||||
@contextlib.contextmanager
|
||||
def run_with_active_hash(self, page_hash: str):
|
||||
original_page_hash = self._active_script_hash
|
||||
self._active_script_hash = page_hash
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
# in the event of any exception, ensure we set the active hash back
|
||||
self._active_script_hash = original_page_hash
|
||||
|
||||
def set_mpa_v2_page(self, page_script_hash: str):
|
||||
self._active_script_hash = self.pages_manager.main_script_hash
|
||||
self.pages_manager.set_current_page_script_hash(page_script_hash)
|
||||
|
||||
def reset(
|
||||
self,
|
||||
query_string: str = "",
|
||||
page_script_hash: str = "",
|
||||
fragment_ids_this_run: list[str] | None = None,
|
||||
context_info: ContextInfo | None = None,
|
||||
) -> None:
|
||||
self.cursors = {}
|
||||
self.widget_ids_this_run = set()
|
||||
self.widget_user_keys_this_run = set()
|
||||
self.form_ids_this_run = set()
|
||||
self.query_string = query_string
|
||||
self.context_info = context_info
|
||||
self.pages_manager.set_current_page_script_hash(page_script_hash)
|
||||
self._active_script_hash = self.pages_manager.main_script_hash
|
||||
# Permit set_page_config when the ScriptRunContext is reused on a rerun
|
||||
self._set_page_config_allowed = True
|
||||
self._has_script_started = False
|
||||
self.command_tracking_deactivated: bool = False
|
||||
self.tracked_commands = []
|
||||
self.tracked_commands_counter = collections.Counter()
|
||||
self.current_fragment_id = None
|
||||
self.current_fragment_delta_path: list[int] = []
|
||||
self.fragment_ids_this_run = fragment_ids_this_run
|
||||
self.new_fragment_ids = set()
|
||||
self.has_dialog_opened = False
|
||||
in_cached_function.set(False)
|
||||
|
||||
parsed_query_params = parse.parse_qs(query_string, keep_blank_values=True)
|
||||
with self.session_state.query_params() as qp:
|
||||
qp.clear_with_no_forward_msg()
|
||||
for key, val in parsed_query_params.items():
|
||||
if len(val) == 0:
|
||||
qp.set_with_no_forward_msg(key, val="")
|
||||
elif len(val) == 1:
|
||||
qp.set_with_no_forward_msg(key, val=val[-1])
|
||||
else:
|
||||
qp.set_with_no_forward_msg(key, val)
|
||||
|
||||
def on_script_start(self) -> None:
|
||||
self._has_script_started = True
|
||||
|
||||
def enqueue(self, msg: ForwardMsg) -> None:
|
||||
"""Enqueue a ForwardMsg for this context's session."""
|
||||
if msg.HasField("page_config_changed") and not self._set_page_config_allowed:
|
||||
raise StreamlitSetPageConfigMustBeFirstCommandError()
|
||||
|
||||
# We want to disallow set_page config if one of the following occurs:
|
||||
# - set_page_config was called on this message
|
||||
# - The script has already started and a different st call occurs (a delta)
|
||||
if msg.HasField("page_config_changed") or (
|
||||
msg.HasField("delta") and self._has_script_started
|
||||
):
|
||||
self._set_page_config_allowed = False
|
||||
|
||||
msg.metadata.active_script_hash = self.active_script_hash
|
||||
|
||||
# Pass the message up to our associated ScriptRunner.
|
||||
self._enqueue(msg)
|
||||
|
||||
def ensure_single_query_api_used(self):
|
||||
if self._experimental_query_params_used and self._production_query_params_used:
|
||||
raise StreamlitAPIException(
|
||||
"Using `st.query_params` together with either `st.experimental_get_query_params` "
|
||||
"or `st.experimental_set_query_params` is not supported. Please convert your app "
|
||||
"to only use `st.query_params`"
|
||||
)
|
||||
|
||||
def mark_experimental_query_params_used(self):
|
||||
self._experimental_query_params_used = True
|
||||
self.ensure_single_query_api_used()
|
||||
|
||||
def mark_production_query_params_used(self):
|
||||
self._production_query_params_used = True
|
||||
self.ensure_single_query_api_used()
|
||||
|
||||
|
||||
SCRIPT_RUN_CONTEXT_ATTR_NAME: Final = "streamlit_script_run_ctx"
|
||||
|
||||
|
||||
def add_script_run_ctx(
|
||||
thread: threading.Thread | None = None, ctx: ScriptRunContext | None = None
|
||||
):
|
||||
"""Adds the current ScriptRunContext to a newly-created thread.
|
||||
|
||||
This should be called from this thread's parent thread,
|
||||
before the new thread starts.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
thread : threading.Thread
|
||||
The thread to attach the current ScriptRunContext to.
|
||||
ctx : ScriptRunContext or None
|
||||
The ScriptRunContext to add, or None to use the current thread's
|
||||
ScriptRunContext.
|
||||
|
||||
Returns
|
||||
-------
|
||||
threading.Thread
|
||||
The same thread that was passed in, for chaining.
|
||||
|
||||
"""
|
||||
if thread is None:
|
||||
thread = threading.current_thread()
|
||||
if ctx is None:
|
||||
ctx = get_script_run_ctx()
|
||||
if ctx is not None:
|
||||
setattr(thread, SCRIPT_RUN_CONTEXT_ATTR_NAME, ctx)
|
||||
return thread
|
||||
|
||||
|
||||
def get_script_run_ctx(suppress_warning: bool = False) -> ScriptRunContext | None:
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
suppress_warning : bool
|
||||
If True, don't log a warning if there's no ScriptRunContext.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ScriptRunContext | None
|
||||
The current thread's ScriptRunContext, or None if it doesn't have one.
|
||||
|
||||
"""
|
||||
thread = threading.current_thread()
|
||||
ctx: ScriptRunContext | None = getattr(thread, SCRIPT_RUN_CONTEXT_ATTR_NAME, None)
|
||||
if ctx is None and not suppress_warning:
|
||||
# Only warn about a missing ScriptRunContext if suppress_warning is False, and
|
||||
# we were started via `streamlit run`. Otherwise, the user is likely running a
|
||||
# script "bare", and doesn't need to be warned about streamlit
|
||||
# bits that are irrelevant when not connected to a session.
|
||||
_LOGGER.warning(
|
||||
"Thread '%s': missing ScriptRunContext! This warning can be ignored when "
|
||||
"running in bare mode.",
|
||||
thread.name,
|
||||
)
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
def enqueue_message(msg: ForwardMsg) -> None:
|
||||
"""Enqueues a ForwardMsg proto to send to the app."""
|
||||
ctx = get_script_run_ctx()
|
||||
|
||||
if ctx is None:
|
||||
raise NoSessionContext()
|
||||
|
||||
if ctx.current_fragment_id and msg.WhichOneof("type") == "delta":
|
||||
msg.delta.fragment_id = ctx.current_fragment_id
|
||||
|
||||
ctx.enqueue(msg)
|
||||
Reference in New Issue
Block a user