# 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 from typing import TYPE_CHECKING, Any, Final from streamlit import config, errors, logger, runtime from streamlit.elements.lib.form_utils import is_in_form from streamlit.errors import ( StreamlitAPIWarning, StreamlitFragmentWidgetsNotAllowedOutsideError, StreamlitInvalidFormCallbackError, StreamlitValueAssignmentNotAllowedError, ) from streamlit.runtime.scriptrunner_utils.script_run_context import ( get_script_run_ctx, in_cached_function, ) from streamlit.runtime.state import WidgetCallback, get_session_state if TYPE_CHECKING: from collections.abc import Sequence from streamlit.delta_generator import DeltaGenerator _LOGGER: Final = logger.get_logger(__name__) def check_callback_rules(dg: DeltaGenerator, on_change: WidgetCallback | None) -> None: """Ensures that widgets other than `st.form_submit_button` within a form don't have an on_change callback set. Raises ------ StreamlitInvalidFormCallbackError: Raised when the described rule is violated. """ if runtime.exists() and is_in_form(dg) and on_change is not None: raise StreamlitInvalidFormCallbackError() _shown_default_value_warning: bool = False def check_session_state_rules( default_value: Any, key: str | None, writes_allowed: bool = True ) -> None: """Ensures that no values are set for widgets with the given key when writing is not allowed. Additionally, if `global.disableWidgetStateDuplicationWarning` is False a warning is shown when a widget has a default value but its value is also set via session state. Raises ------ StreamlitAPIException: Raised when the described rule is violated. """ global _shown_default_value_warning # noqa: PLW0603 if key is None or not runtime.exists(): return session_state = get_session_state() if not session_state.is_new_state_value(key): return if not writes_allowed: raise StreamlitValueAssignmentNotAllowedError(key=key) if ( default_value is not None and not _shown_default_value_warning and not config.get_option("global.disableWidgetStateDuplicationWarning") ): from streamlit import warning warning( f'The widget with key "{key}" was created with a default value but' " also had its value set via the Session State API." ) _shown_default_value_warning = True class CachedWidgetWarning(StreamlitAPIWarning): def __init__(self) -> None: super().__init__( """ Your script uses a widget command in a cached function (function decorated with `@st.cache_data` or `@st.cache_resource`). This code will only be called when we detect a cache "miss", which can lead to unexpected results. To fix this, move all widget commands outside the cached function. """ ) def check_cache_replay_rules() -> None: """Check if a widget is allowed to be used in the current context. More specifically, this checks if the current context is inside a cached function that disallows widget usage. If so, it raises a warning. If there are other similar checks in the future, we could extend this function to check for those as well. And rename it to check_widget_usage_rules. """ if in_cached_function.get(): from streamlit import exception # We use an exception here to show a proper stack trace # that indicates to the user where the issue is. exception(CachedWidgetWarning()) def check_fragment_path_policy(dg: DeltaGenerator) -> None: """Ensures that the current widget is not written outside of the fragment's delta path. Should be called by ever element that acts as a widget. We don't allow writing widgets from within a widget to the outside path because it can lead to unexpected behavior. For elements, this is okay because they do not trigger a re-run. """ ctx = get_script_run_ctx() # Check is only relevant for fragments if ctx is None or ctx.current_fragment_id is None: return current_fragment_delta_path = ctx.current_fragment_delta_path current_cursor = dg._active_dg._cursor if current_cursor is None: return current_cursor_delta_path = current_cursor.delta_path # the elements delta path cannot be smaller than the fragment's delta path if it is # inside of the fragment if len(current_cursor_delta_path) < len(current_fragment_delta_path): raise StreamlitFragmentWidgetsNotAllowedOutsideError() # all path indices of the fragment-path must occur in the inner-elements delta path, # otherwise it is outside of the fragment container for index, path_index in enumerate(current_fragment_delta_path): if current_cursor_delta_path[index] != path_index: raise StreamlitFragmentWidgetsNotAllowedOutsideError() def check_widget_policies( dg: DeltaGenerator, key: str | None, on_change: WidgetCallback | None = None, *, default_value: Sequence[Any] | Any | None = None, writes_allowed: bool = True, enable_check_callback_rules: bool = True, ) -> None: """Check all widget policies for the given DeltaGenerator.""" check_fragment_path_policy(dg) check_cache_replay_rules() if enable_check_callback_rules: check_callback_rules(dg, on_change) check_session_state_rules( default_value=default_value, key=key, writes_allowed=writes_allowed ) def maybe_raise_label_warnings(label: str | None, label_visibility: str | None) -> None: if not label: _LOGGER.warning( "`label` got an empty value. This is discouraged for accessibility " "reasons and may be disallowed in the future by raising an exception. " "Please provide a non-empty label and hide it with label_visibility " "if needed.", stack_info=True, ) if label_visibility not in ("visible", "hidden", "collapsed"): raise errors.StreamlitAPIException( f"Unsupported label_visibility option '{label_visibility}'. " f"Valid values are 'visible', 'hidden' or 'collapsed'." )