From 2b224f5e19c6c3c5c34322b91d53a89e1b12fed1 Mon Sep 17 00:00:00 2001 From: lukasmasuch Date: Fri, 26 Apr 2024 18:58:51 +0200 Subject: [PATCH 1/3] Add `key` parameter to `st.container` --- frontend/lib/src/components/core/Block/Block.tsx | 6 +++++- proto/streamlit/proto/Block.proto | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/lib/src/components/core/Block/Block.tsx b/frontend/lib/src/components/core/Block/Block.tsx index 8f2e7acd85bb..247a0292013d 100644 --- a/frontend/lib/src/components/core/Block/Block.tsx +++ b/frontend/lib/src/components/core/Block/Block.tsx @@ -334,7 +334,11 @@ const VerticalBlock = (props: BlockPropsWithoutWidth): ReactElement => { data-test-scroll-behavior="normal" > - + diff --git a/proto/streamlit/proto/Block.proto b/proto/streamlit/proto/Block.proto index 1809b5fd8871..7776804c6ed4 100644 --- a/proto/streamlit/proto/Block.proto +++ b/proto/streamlit/proto/Block.proto @@ -34,6 +34,7 @@ message Block { } bool allow_empty = 8; + string key = 12; message Vertical { bool border = 1; From 665ab62a80da0ad9d8d892610186acc996d95305 Mon Sep 17 00:00:00 2001 From: lukasmasuch Date: Fri, 26 Apr 2024 19:48:22 +0200 Subject: [PATCH 2/3] Add container key to widget id --- .../lib/src/components/core/Block/Block.tsx | 6 +++- .../components/v1/custom_component.py | 4 ++- lib/streamlit/delta_generator.py | 1 + lib/streamlit/elements/layouts.py | 22 ++++++++++++-- lib/streamlit/elements/utils.py | 29 +++++++++++++++++++ lib/streamlit/elements/widgets/button.py | 4 +++ .../elements/widgets/camera_input.py | 2 ++ lib/streamlit/elements/widgets/chat.py | 2 ++ lib/streamlit/elements/widgets/checkbox.py | 3 +- .../elements/widgets/color_picker.py | 3 +- lib/streamlit/elements/widgets/data_editor.py | 3 +- .../elements/widgets/file_uploader.py | 2 ++ lib/streamlit/elements/widgets/multiselect.py | 2 ++ .../elements/widgets/number_input.py | 2 ++ lib/streamlit/elements/widgets/radio.py | 2 ++ .../elements/widgets/select_slider.py | 2 ++ lib/streamlit/elements/widgets/selectbox.py | 2 ++ lib/streamlit/elements/widgets/slider.py | 2 ++ .../elements/widgets/text_widgets.py | 3 ++ .../elements/widgets/time_widgets.py | 3 ++ 20 files changed, 92 insertions(+), 7 deletions(-) diff --git a/frontend/lib/src/components/core/Block/Block.tsx b/frontend/lib/src/components/core/Block/Block.tsx index 247a0292013d..f6a66a610795 100644 --- a/frontend/lib/src/components/core/Block/Block.tsx +++ b/frontend/lib/src/components/core/Block/Block.tsx @@ -337,7 +337,11 @@ const VerticalBlock = (props: BlockPropsWithoutWidth): ReactElement => { diff --git a/lib/streamlit/components/v1/custom_component.py b/lib/streamlit/components/v1/custom_component.py index 32bd4feade43..c15d5c69a4bd 100644 --- a/lib/streamlit/components/v1/custom_component.py +++ b/lib/streamlit/components/v1/custom_component.py @@ -20,7 +20,7 @@ from streamlit import _main, type_util from streamlit.components.types.base_custom_component import BaseCustomComponent from streamlit.elements.form import current_form_id -from streamlit.elements.utils import check_cache_replay_rules +from streamlit.elements.utils import check_cache_replay_rules, current_container_key from streamlit.errors import StreamlitAPIException from streamlit.proto.Components_pb2 import ArrowTable as ArrowTableProto from streamlit.proto.Components_pb2 import SpecialArg @@ -169,6 +169,7 @@ def marshall_element_args(): key=key, json_args=serialized_json_args, special_args=special_args, + container_key=current_container_key(dg), page=ctx.page_script_hash if ctx else None, ) else: @@ -179,6 +180,7 @@ def marshall_element_args(): form_id=current_form_id(dg), url=self.url, key=key, + container_key=current_container_key(dg), page=ctx.page_script_hash if ctx else None, ) element.component_instance.id = computed_id diff --git a/lib/streamlit/delta_generator.py b/lib/streamlit/delta_generator.py index 5d30e52fed2c..349638e4d50e 100644 --- a/lib/streamlit/delta_generator.py +++ b/lib/streamlit/delta_generator.py @@ -277,6 +277,7 @@ def __init__( # If this an `st.form` block, this will get filled in. self._form_data: FormData | None = None + self._container_key: str | None = None # Change the module of all mixin'ed functions to be st.delta_generator, # instead of the original module (e.g. st.elements.markdown) diff --git a/lib/streamlit/elements/layouts.py b/lib/streamlit/elements/layouts.py index 44826782e5f0..d7ea5a7f7b3c 100644 --- a/lib/streamlit/elements/layouts.py +++ b/lib/streamlit/elements/layouts.py @@ -14,13 +14,15 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Literal, Sequence, Union, cast +import re +from typing import TYPE_CHECKING, Final, Literal, Sequence, Union, cast from typing_extensions import TypeAlias from streamlit.errors import StreamlitAPIException from streamlit.proto.Block_pb2 import Block as BlockProto from streamlit.runtime.metrics_util import gather_metrics +from streamlit.type_util import Key, to_key if TYPE_CHECKING: from streamlit.delta_generator import DeltaGenerator @@ -29,11 +31,18 @@ SpecType: TypeAlias = Union[int, Sequence[Union[int, float]]] +# Pattern to validate a container key (to be compatible with css class names): +_KEY_PATTERN: Final = re.compile(r"^[a-zA-Z_][a-zA-Z0-9\-_]*$") + class LayoutsMixin: @gather_metrics("container") def container( - self, *, height: int | None = None, border: bool | None = None + self, + *, + height: int | None = None, + border: bool | None = None, + key: Key | None = None, ) -> DeltaGenerator: """Insert a multi-element container. @@ -130,6 +139,15 @@ def container( block_proto = BlockProto() block_proto.allow_empty = False block_proto.vertical.border = border or False + if key := to_key(key): + if not _KEY_PATTERN.match(key): + raise StreamlitAPIException( + f"Invalid key '{key}'. Key must match the pattern '{_KEY_PATTERN.pattern}'." + ) + block_proto.key = key + # Set the container key in the DeltaGenerator + self.dg._container_key = key + if height: # Activate scrolling container behavior: block_proto.allow_empty = True diff --git a/lib/streamlit/elements/utils.py b/lib/streamlit/elements/utils.py index ea34af6ed287..d268ea5c0313 100644 --- a/lib/streamlit/elements/utils.py +++ b/lib/streamlit/elements/utils.py @@ -86,6 +86,35 @@ def check_session_state_rules( _shown_default_value_warning = True +def current_container_key(this_dg: DeltaGenerator) -> str | None: + """Find the container key for the given DeltaGenerator.""" + # Avoid circular imports. + from streamlit.delta_generator import dg_stack + + if not runtime.exists(): + return None + + if this_dg._container_key is not None: + return this_dg._container_key + + if this_dg == this_dg._main_dg: + # We were created via an `st.foo` call. + # Walk up the dg_stack to see if there is a container key set. + for dg in reversed(dg_stack.get()): + if dg._container_key is not None: + return dg._container_key + else: + # We were created via an `dg.foo` call. + # Take a look at our parent's container key. + parent = this_dg._parent + if parent is not None and parent._container_key is not None: + return parent._container_key + else: + return current_container_key(parent) + + return None + + class CachedWidgetWarning(StreamlitAPIWarning): def __init__(self): super().__init__( diff --git a/lib/streamlit/elements/widgets/button.py b/lib/streamlit/elements/widgets/button.py index e97ece7904c2..3a2a345262a6 100644 --- a/lib/streamlit/elements/widgets/button.py +++ b/lib/streamlit/elements/widgets/button.py @@ -560,6 +560,7 @@ def _download_button( check_cache_replay_rules, check_callback_rules, check_session_state_rules, + current_container_key, ) check_cache_replay_rules() @@ -576,6 +577,7 @@ def _download_button( help=help, type=type, use_container_width=use_container_width, + container_key=current_container_key(self.dg), page=ctx.page_script_hash if ctx else None, ) @@ -724,6 +726,7 @@ def _button( check_cache_replay_rules, check_callback_rules, check_session_state_rules, + current_container_key, ) if not is_form_submitter: @@ -740,6 +743,7 @@ def _button( is_form_submitter=is_form_submitter, type=type, use_container_width=use_container_width, + container_key=current_container_key(self.dg), page=ctx.page_script_hash if ctx else None, ) diff --git a/lib/streamlit/elements/widgets/camera_input.py b/lib/streamlit/elements/widgets/camera_input.py index e9c9a6dfae48..5b60e9607a41 100644 --- a/lib/streamlit/elements/widgets/camera_input.py +++ b/lib/streamlit/elements/widgets/camera_input.py @@ -25,6 +25,7 @@ check_cache_replay_rules, check_callback_rules, check_session_state_rules, + current_container_key, get_label_visibility_proto_value, ) from streamlit.elements.widgets.file_uploader import _get_upload_files @@ -210,6 +211,7 @@ def _camera_input( key=key, help=help, form_id=current_form_id(self.dg), + container_key=current_container_key(self.dg), page=ctx.page_script_hash if ctx else None, ) diff --git a/lib/streamlit/elements/widgets/chat.py b/lib/streamlit/elements/widgets/chat.py index 031822fd1820..de58c102a302 100644 --- a/lib/streamlit/elements/widgets/chat.py +++ b/lib/streamlit/elements/widgets/chat.py @@ -298,6 +298,7 @@ def chat_input( check_cache_replay_rules, check_callback_rules, check_session_state_rules, + current_container_key, ) check_cache_replay_rules() @@ -311,6 +312,7 @@ def chat_input( key=key, placeholder=placeholder, max_chars=max_chars, + container_key=current_container_key(self.dg), page=ctx.page_script_hash if ctx else None, ) diff --git a/lib/streamlit/elements/widgets/checkbox.py b/lib/streamlit/elements/widgets/checkbox.py index 058138dcd689..9e3cb04431dd 100644 --- a/lib/streamlit/elements/widgets/checkbox.py +++ b/lib/streamlit/elements/widgets/checkbox.py @@ -34,7 +34,7 @@ WidgetKwargs, register_widget, ) -from streamlit.runtime.state.common import compute_widget_id +from streamlit.runtime.state.common import compute_widget_id, current_container_key from streamlit.type_util import Key, LabelVisibility, maybe_raise_label_warnings, to_key if TYPE_CHECKING: @@ -293,6 +293,7 @@ def _checkbox( key=key, help=help, form_id=current_form_id(self.dg), + container_key=current_container_key(self.dg), page=ctx.page_script_hash if ctx else None, ) diff --git a/lib/streamlit/elements/widgets/color_picker.py b/lib/streamlit/elements/widgets/color_picker.py index 07a70b439a5a..b294630d0bf1 100644 --- a/lib/streamlit/elements/widgets/color_picker.py +++ b/lib/streamlit/elements/widgets/color_picker.py @@ -20,7 +20,7 @@ from typing import cast import streamlit -from streamlit.elements.form import current_form_id +from streamlit.elements.form import current_container_key, current_form_id from streamlit.elements.utils import ( check_cache_replay_rules, check_callback_rules, @@ -185,6 +185,7 @@ def _color_picker( key=key, help=help, form_id=current_form_id(self.dg), + container_key=current_container_key(self.dg), page=ctx.page_script_hash if ctx else None, ) diff --git a/lib/streamlit/elements/widgets/data_editor.py b/lib/streamlit/elements/widgets/data_editor.py index 2bc31cdb5071..ab3779425a66 100644 --- a/lib/streamlit/elements/widgets/data_editor.py +++ b/lib/streamlit/elements/widgets/data_editor.py @@ -40,7 +40,7 @@ from streamlit import logger as _logger from streamlit import type_util from streamlit.deprecation_util import deprecate_func_name -from streamlit.elements.form import current_form_id +from streamlit.elements.form import current_container_key, current_form_id from streamlit.elements.lib.column_config_utils import ( INDEX_IDENTIFIER, ColumnConfigMapping, @@ -874,6 +874,7 @@ def data_editor( num_rows=num_rows, key=key, form_id=current_form_id(self.dg), + container_key=current_container_key(self.dg), page=ctx.page_script_hash if ctx else None, ) diff --git a/lib/streamlit/elements/widgets/file_uploader.py b/lib/streamlit/elements/widgets/file_uploader.py index a2a3ba60e996..e892b9f10e31 100644 --- a/lib/streamlit/elements/widgets/file_uploader.py +++ b/lib/streamlit/elements/widgets/file_uploader.py @@ -26,6 +26,7 @@ check_cache_replay_rules, check_callback_rules, check_session_state_rules, + current_container_key, get_label_visibility_proto_value, ) from streamlit.proto.Common_pb2 import FileUploaderState as FileUploaderStateProto @@ -415,6 +416,7 @@ def _file_uploader( key=key, help=help, form_id=current_form_id(self.dg), + container_key=current_container_key(self.dg), page=ctx.page_script_hash if ctx else None, ) diff --git a/lib/streamlit/elements/widgets/multiselect.py b/lib/streamlit/elements/widgets/multiselect.py index 3b558fa67edb..be3605d33901 100644 --- a/lib/streamlit/elements/widgets/multiselect.py +++ b/lib/streamlit/elements/widgets/multiselect.py @@ -23,6 +23,7 @@ check_cache_replay_rules, check_callback_rules, check_session_state_rules, + current_container_key, get_label_visibility_proto_value, maybe_coerce_enum_sequence, ) @@ -310,6 +311,7 @@ def _multiselect( max_selections=max_selections, placeholder=placeholder, form_id=current_form_id(self.dg), + container_key=current_container_key(self.dg), page=ctx.page_script_hash if ctx else None, ) diff --git a/lib/streamlit/elements/widgets/number_input.py b/lib/streamlit/elements/widgets/number_input.py index 7f6d972ac85a..e79b052224e3 100644 --- a/lib/streamlit/elements/widgets/number_input.py +++ b/lib/streamlit/elements/widgets/number_input.py @@ -26,6 +26,7 @@ check_cache_replay_rules, check_callback_rules, check_session_state_rules, + current_container_key, get_label_visibility_proto_value, ) from streamlit.errors import StreamlitAPIException @@ -301,6 +302,7 @@ def _number_input( help=help, placeholder=None if placeholder is None else str(placeholder), form_id=current_form_id(self.dg), + container_key=current_container_key(self.dg), page=ctx.page_script_hash if ctx else None, ) diff --git a/lib/streamlit/elements/widgets/radio.py b/lib/streamlit/elements/widgets/radio.py index d89b0f71ecae..aa0e70c27c37 100644 --- a/lib/streamlit/elements/widgets/radio.py +++ b/lib/streamlit/elements/widgets/radio.py @@ -23,6 +23,7 @@ check_cache_replay_rules, check_callback_rules, check_session_state_rules, + current_container_key, get_label_visibility_proto_value, maybe_coerce_enum, ) @@ -272,6 +273,7 @@ def _radio( horizontal=horizontal, captions=captions, form_id=current_form_id(self.dg), + container_key=current_container_key(self.dg), page=ctx.page_script_hash if ctx else None, ) diff --git a/lib/streamlit/elements/widgets/select_slider.py b/lib/streamlit/elements/widgets/select_slider.py index 201c379ad8c1..411e26dae98a 100644 --- a/lib/streamlit/elements/widgets/select_slider.py +++ b/lib/streamlit/elements/widgets/select_slider.py @@ -25,6 +25,7 @@ check_cache_replay_rules, check_callback_rules, check_session_state_rules, + current_container_key, get_label_visibility_proto_value, maybe_coerce_enum, maybe_coerce_enum_sequence, @@ -299,6 +300,7 @@ def as_index_list(v: object) -> list[int]: key=key, help=help, form_id=current_form_id(self.dg), + container_key=current_container_key(self.dg), page=ctx.page_script_hash if ctx else None, ) diff --git a/lib/streamlit/elements/widgets/selectbox.py b/lib/streamlit/elements/widgets/selectbox.py index fb1f1684afe2..86f8de1a5332 100644 --- a/lib/streamlit/elements/widgets/selectbox.py +++ b/lib/streamlit/elements/widgets/selectbox.py @@ -22,6 +22,7 @@ check_cache_replay_rules, check_callback_rules, check_session_state_rules, + current_container_key, get_label_visibility_proto_value, maybe_coerce_enum, ) @@ -251,6 +252,7 @@ def _selectbox( help=help, placeholder=placeholder, form_id=current_form_id(self.dg), + container_key=current_container_key(self.dg), page=ctx.page_script_hash if ctx else None, ) diff --git a/lib/streamlit/elements/widgets/slider.py b/lib/streamlit/elements/widgets/slider.py index f8640078c6b1..9827b365772c 100644 --- a/lib/streamlit/elements/widgets/slider.py +++ b/lib/streamlit/elements/widgets/slider.py @@ -27,6 +27,7 @@ check_cache_replay_rules, check_callback_rules, check_session_state_rules, + current_container_key, get_label_visibility_proto_value, ) from streamlit.errors import StreamlitAPIException @@ -384,6 +385,7 @@ def _slider( key=key, help=help, form_id=current_form_id(self.dg), + container_key=current_container_key(self.dg), page=ctx.page_script_hash if ctx else None, ) diff --git a/lib/streamlit/elements/widgets/text_widgets.py b/lib/streamlit/elements/widgets/text_widgets.py index 0efdf934cdf7..28451fd5acbf 100644 --- a/lib/streamlit/elements/widgets/text_widgets.py +++ b/lib/streamlit/elements/widgets/text_widgets.py @@ -23,6 +23,7 @@ check_cache_replay_rules, check_callback_rules, check_session_state_rules, + current_container_key, get_label_visibility_proto_value, ) from streamlit.errors import StreamlitAPIException @@ -277,6 +278,7 @@ def _text_input( autocomplete=autocomplete, placeholder=str(placeholder), form_id=current_form_id(self.dg), + container_key=current_container_key(self.dg), page=ctx.page_script_hash if ctx else None, ) @@ -542,6 +544,7 @@ def _text_area( help=help, placeholder=str(placeholder), form_id=current_form_id(self.dg), + container_key=current_container_key(self.dg), page=ctx.page_script_hash if ctx else None, ) diff --git a/lib/streamlit/elements/widgets/time_widgets.py b/lib/streamlit/elements/widgets/time_widgets.py index e4056ff0e284..b22385b131eb 100644 --- a/lib/streamlit/elements/widgets/time_widgets.py +++ b/lib/streamlit/elements/widgets/time_widgets.py @@ -37,6 +37,7 @@ check_cache_replay_rules, check_callback_rules, check_session_state_rules, + current_container_key, get_label_visibility_proto_value, ) from streamlit.errors import StreamlitAPIException @@ -456,6 +457,7 @@ def _time_input( help=help, step=step, form_id=current_form_id(self.dg), + container_key=current_container_key(self.dg), page=ctx.page_script_hash if ctx else None, ) del value @@ -727,6 +729,7 @@ def parse_date_deterministic( help=help, format=format, form_id=current_form_id(self.dg), + container_key=current_container_key(self.dg), page=ctx.page_script_hash if ctx else None, ) if not bool(ALLOWED_DATE_FORMATS.match(format)): From 654221e88f78635aba28cf62a116cfa508467ae1 Mon Sep 17 00:00:00 2001 From: lukasmasuch Date: Fri, 26 Apr 2024 19:56:04 +0200 Subject: [PATCH 3/3] Add a couple of fixes --- lib/streamlit/elements/layouts.py | 9 ++++++--- lib/streamlit/elements/widgets/checkbox.py | 3 ++- lib/streamlit/elements/widgets/color_picker.py | 3 ++- lib/streamlit/elements/widgets/data_editor.py | 3 ++- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/streamlit/elements/layouts.py b/lib/streamlit/elements/layouts.py index d7ea5a7f7b3c..577952d0b557 100644 --- a/lib/streamlit/elements/layouts.py +++ b/lib/streamlit/elements/layouts.py @@ -145,8 +145,6 @@ def container( f"Invalid key '{key}'. Key must match the pattern '{_KEY_PATTERN.pattern}'." ) block_proto.key = key - # Set the container key in the DeltaGenerator - self.dg._container_key = key if height: # Activate scrolling container behavior: @@ -158,7 +156,12 @@ def container( # containers. block_proto.vertical.border = True - return self.dg._block(block_proto) + block_dg = self.dg._block(block_proto) + + # Attach the container key info to the newly-created block's + # DeltaGenerator. + block_dg._container_key = key + return block_dg @gather_metrics("columns") def columns( diff --git a/lib/streamlit/elements/widgets/checkbox.py b/lib/streamlit/elements/widgets/checkbox.py index 9e3cb04431dd..9cf51719dcfe 100644 --- a/lib/streamlit/elements/widgets/checkbox.py +++ b/lib/streamlit/elements/widgets/checkbox.py @@ -23,6 +23,7 @@ check_cache_replay_rules, check_callback_rules, check_session_state_rules, + current_container_key, get_label_visibility_proto_value, ) from streamlit.proto.Checkbox_pb2 import Checkbox as CheckboxProto @@ -34,7 +35,7 @@ WidgetKwargs, register_widget, ) -from streamlit.runtime.state.common import compute_widget_id, current_container_key +from streamlit.runtime.state.common import compute_widget_id from streamlit.type_util import Key, LabelVisibility, maybe_raise_label_warnings, to_key if TYPE_CHECKING: diff --git a/lib/streamlit/elements/widgets/color_picker.py b/lib/streamlit/elements/widgets/color_picker.py index b294630d0bf1..bb13ff7b5dcd 100644 --- a/lib/streamlit/elements/widgets/color_picker.py +++ b/lib/streamlit/elements/widgets/color_picker.py @@ -20,11 +20,12 @@ from typing import cast import streamlit -from streamlit.elements.form import current_container_key, current_form_id +from streamlit.elements.form import current_form_id from streamlit.elements.utils import ( check_cache_replay_rules, check_callback_rules, check_session_state_rules, + current_container_key, get_label_visibility_proto_value, ) from streamlit.errors import StreamlitAPIException diff --git a/lib/streamlit/elements/widgets/data_editor.py b/lib/streamlit/elements/widgets/data_editor.py index ab3779425a66..b59bd23e408a 100644 --- a/lib/streamlit/elements/widgets/data_editor.py +++ b/lib/streamlit/elements/widgets/data_editor.py @@ -40,7 +40,7 @@ from streamlit import logger as _logger from streamlit import type_util from streamlit.deprecation_util import deprecate_func_name -from streamlit.elements.form import current_container_key, current_form_id +from streamlit.elements.form import current_form_id from streamlit.elements.lib.column_config_utils import ( INDEX_IDENTIFIER, ColumnConfigMapping, @@ -782,6 +782,7 @@ def data_editor( check_cache_replay_rules, check_callback_rules, check_session_state_rules, + current_container_key, ) key = to_key(key)