Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix widget id stability #7003

Merged
merged 47 commits into from Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
e87fb15
Add (failing) slider id stability test
AnOctopus Jul 11, 2023
62b5164
Hacky fix for slider id stability
AnOctopus Jul 11, 2023
335f829
Document safe dependency on state in slider
AnOctopus Jul 13, 2023
2d356e5
Crude early detection of common nonidentity fields
AnOctopus Jul 13, 2023
3908489
Add new widget id function
AnOctopus Jul 17, 2023
0520df1
Convert most widgets to new ID generation
AnOctopus Jul 17, 2023
6ecf282
Add docs for requirements for calling widget id gen
AnOctopus Jul 17, 2023
1a41980
Use current form id as widget id input
AnOctopus Jul 17, 2023
ffa2395
Merge branch 'develop' into fix/widget-id-stability
AnOctopus Jul 17, 2023
4edb863
Add missing label to time widget id
AnOctopus Jul 18, 2023
65f5df6
Early generate ids for custom components and file uploader
AnOctopus Jul 18, 2023
8be3255
Generate id for data editor
AnOctopus Jul 18, 2023
6fb2a1b
Fix and simplify keyed widget id test
AnOctopus Jul 18, 2023
460c3d0
Update other deltagen widget id tests
AnOctopus Jul 18, 2023
2e26e7b
Warn about fallback widget id generation
AnOctopus Jul 18, 2023
af91723
Concision
AnOctopus Jul 18, 2023
3c2dc38
Add widget id kwargs types, start resolving conflicts
AnOctopus Jul 19, 2023
be63d8a
Merge branch 'develop' into fix/widget-id-stability
AnOctopus Jul 19, 2023
699a703
Allow common types with safe str encodings
AnOctopus Jul 20, 2023
d9d0ca0
Finish hacky hashing of existing safe values
AnOctopus Jul 20, 2023
1969ffe
Fix import cycle
AnOctopus Jul 20, 2023
863d8f9
Merge branch 'develop' into fix/widget-id-stability
AnOctopus Jul 21, 2023
4b2e1ed
Merge branch 'develop' into fix/widget-id-stability
AnOctopus Jul 21, 2023
8e7c7d9
Clean up union in signature
AnOctopus Jul 24, 2023
ebe180a
Partial processing of time value
AnOctopus Jul 24, 2023
e30e3b2
Date parsing
AnOctopus Jul 24, 2023
0208861
Hash multiselct index for default
AnOctopus Jul 24, 2023
e9ecdb6
Add unresolved todo
AnOctopus Jul 24, 2023
d5ffbd3
Fix dumb errors
AnOctopus Jul 25, 2023
b828da2
Use parse function for min and max
AnOctopus Jul 25, 2023
12b1fc2
Rename old widget id computation function
AnOctopus Jul 25, 2023
12e0304
Rename new widget id function
AnOctopus Jul 25, 2023
2eef088
Document safety from ensured dict iter order
AnOctopus Jul 25, 2023
18ddcb7
Update custom components hashing comments
AnOctopus Jul 25, 2023
2cc6957
Add missing widget id computation do download button
AnOctopus Jul 25, 2023
7ed310b
Remove unneeded fallback id calculation
AnOctopus Jul 25, 2023
8f1fb45
Remove old widget id function
AnOctopus Jul 25, 2023
a9b008d
Comment about late id calculation in data_editor
AnOctopus Jul 25, 2023
481c339
Update slider session_state use documentation
AnOctopus Jul 25, 2023
27498d3
Try changing name to unconfuse codeql
AnOctopus Jul 25, 2023
19bb0bc
Remove dead imports
AnOctopus Jul 25, 2023
50f7b70
Avoid impossible uninitialized variable
AnOctopus Jul 25, 2023
1ad9c16
Use `disabled` in data editor id
AnOctopus Jul 26, 2023
0da4e02
Remove dead code
AnOctopus Jul 26, 2023
25aea34
Distinguish download button id
AnOctopus Jul 26, 2023
223b2e1
Use column_config_mapping
AnOctopus Jul 27, 2023
fe30d51
Use arrow_bytes
AnOctopus Jul 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 21 additions & 0 deletions lib/streamlit/components/v1/components.py
Expand Up @@ -29,6 +29,7 @@
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.scriptrunner import get_script_run_ctx
from streamlit.runtime.state import NoValue, register_widget
from streamlit.runtime.state.common import new_compute_widget_id
from streamlit.type_util import to_bytes

LOGGER = get_logger(__name__)
Expand Down Expand Up @@ -184,6 +185,26 @@ def marshall_element_args():

if key is None:
marshall_element_args()
AnOctopus marked this conversation as resolved.
Show resolved Hide resolved
id = new_compute_widget_id(
"component_instance",
user_key=key,
name=self.name,
form_id=current_form_id(dg),
url=self.url,
key=key,
json_args=serialized_json_args,
special_args=special_args,
)
else:
id = new_compute_widget_id(
"component_instance",
user_key=key,
name=self.name,
form_id=current_form_id(dg),
url=self.url,
key=key,
)
element.component_instance.id = id

def deserialize_component(ui_value, widget_id=""):
# ui_value is an object from json, an ArrowTable proto, or a bytearray
Expand Down
14 changes: 13 additions & 1 deletion lib/streamlit/elements/button.py
Expand Up @@ -33,6 +33,7 @@
WidgetKwargs,
register_widget,
)
from streamlit.runtime.state.common import new_compute_widget_id
from streamlit.type_util import Key, to_key

if TYPE_CHECKING:
Expand Down Expand Up @@ -339,7 +340,6 @@ def _download_button(
use_container_width: bool = False,
ctx: Optional[ScriptRunContext] = None,
) -> bool:

key = to_key(key)
check_session_state_rules(default_value=None, key=key, writes_allowed=False)
if is_in_form(self.dg):
Expand Down Expand Up @@ -399,6 +399,17 @@ def _button(
check_callback_rules(self.dg, on_click)
check_session_state_rules(default_value=None, key=key, writes_allowed=False)

id = new_compute_widget_id(
"button",
user_key=key,
label=label,
key=key,
help=help,
is_form_submitter=is_form_submitter,
type=type,
use_container_width=use_container_width,
)

# It doesn't make sense to create a button inside a form (except
# for the "Form Submitter" button that's automatically created in
# every form). We throw an error to warn the user about this.
Expand All @@ -415,6 +426,7 @@ def _button(
)

button_proto = ButtonProto()
button_proto.id = id
button_proto.label = label
button_proto.default = False
button_proto.is_form_submitter = is_form_submitter
Expand Down
11 changes: 11 additions & 0 deletions lib/streamlit/elements/camera_input.py
Expand Up @@ -33,6 +33,7 @@
WidgetKwargs,
register_widget,
)
from streamlit.runtime.state.common import new_compute_widget_id
from streamlit.runtime.uploaded_file_manager import UploadedFile, UploadedFileRec
from streamlit.type_util import Key, LabelVisibility, maybe_raise_label_warnings, to_key

Expand Down Expand Up @@ -227,7 +228,17 @@ def _camera_input(
check_session_state_rules(default_value=None, key=key, writes_allowed=False)
maybe_raise_label_warnings(label, label_visibility)

id = new_compute_widget_id(
"camera_input",
user_key=key,
label=label,
key=key,
help=help,
form_id=current_form_id(self.dg),
)

camera_input_proto = CameraInputProto()
camera_input_proto.id = id
camera_input_proto.label = label
camera_input_proto.form_id = current_form_id(self.dg)

Expand Down
9 changes: 9 additions & 0 deletions lib/streamlit/elements/chat.py
Expand Up @@ -35,6 +35,7 @@
WidgetKwargs,
register_widget,
)
from streamlit.runtime.state.common import new_compute_widget_id
from streamlit.string_util import is_emoji
from streamlit.type_util import Key, to_key

Expand Down Expand Up @@ -277,6 +278,13 @@ def chat_input(
check_callback_rules(self.dg, on_submit)
check_session_state_rules(default_value=default, key=key, writes_allowed=False)

id = new_compute_widget_id(
"chat_input",
user_key=key,
placeholder=placeholder,
max_chars=max_chars,
)

# We omit this check for scripts running outside streamlit, because
# they will have no script_run_ctx.
if runtime.exists():
Expand All @@ -293,6 +301,7 @@ def chat_input(
raise StreamlitAPIException(DISALLOWED_CONTAINERS_ERROR_TEXT)

chat_input_proto = ChatInputProto()
chat_input_proto.id = id
chat_input_proto.placeholder = str(placeholder)

if max_chars is not None:
Expand Down
12 changes: 12 additions & 0 deletions lib/streamlit/elements/checkbox.py
Expand Up @@ -31,6 +31,7 @@
WidgetKwargs,
register_widget,
)
from streamlit.runtime.state.common import new_compute_widget_id
from streamlit.type_util import Key, LabelVisibility, maybe_raise_label_warnings, to_key

if TYPE_CHECKING:
Expand Down Expand Up @@ -169,7 +170,18 @@ def _checkbox(

maybe_raise_label_warnings(label, label_visibility)

id = new_compute_widget_id(
"checkbox",
user_key=key,
label=label,
value=bool(value),
key=key,
help=help,
form_id=current_form_id(self.dg),
)

checkbox_proto = CheckboxProto()
checkbox_proto.id = id
checkbox_proto.label = label
checkbox_proto.default = bool(value)
checkbox_proto.form_id = current_form_id(self.dg)
Expand Down
12 changes: 12 additions & 0 deletions lib/streamlit/elements/color_picker.py
Expand Up @@ -34,6 +34,7 @@
WidgetKwargs,
register_widget,
)
from streamlit.runtime.state.common import new_compute_widget_id
from streamlit.type_util import Key, LabelVisibility, maybe_raise_label_warnings, to_key


Expand Down Expand Up @@ -170,6 +171,16 @@ def _color_picker(
check_session_state_rules(default_value=value, key=key)
maybe_raise_label_warnings(label, label_visibility)

id = new_compute_widget_id(
"color_picker",
user_key=key,
label=label,
value=str(value),
key=key,
help=help,
form_id=current_form_id(self.dg),
)

# set value default
if value is None:
value = "#000000"
Expand Down Expand Up @@ -197,6 +208,7 @@ def _color_picker(
)

color_picker_proto = ColorPickerProto()
color_picker_proto.id = id
color_picker_proto.label = label
color_picker_proto.default = str(value)
color_picker_proto.form_id = current_form_id(self.dg)
Expand Down
20 changes: 20 additions & 0 deletions lib/streamlit/elements/data_editor.py
Expand Up @@ -65,6 +65,7 @@
WidgetKwargs,
register_widget,
)
from streamlit.runtime.state.common import new_compute_widget_id
from streamlit.type_util import DataFormat, DataFrameGenericAlias, Key, is_type, to_key
from streamlit.util import calc_md5

Expand Down Expand Up @@ -727,6 +728,9 @@ def data_editor(
check_callback_rules(self.dg, on_change)
check_session_state_rules(default_value=None, key=key, writes_allowed=False)

if column_order is not None:
column_order = list(column_order)

column_config_mapping: ColumnConfigMapping = {}

data_format = type_util.determine_data_format(data)
Expand Down Expand Up @@ -787,7 +791,23 @@ def data_editor(
# Throws an exception if any of the configured types are incompatible.
_check_type_compatibilities(data_df, column_config_mapping, dataframe_schema)

id = new_compute_widget_id(
"data_editor",
user_key=key,
data=arrow_table,
width=width,
height=height,
use_container_width=use_container_width,
hide_index=hide_index,
column_order=column_order,
column_config=str(column_config),
num_rows=num_rows,
key=key,
form_id=current_form_id(self.dg),
)

proto = ArrowProto()
proto.id = id

proto.use_container_width = use_container_width

Expand Down
13 changes: 13 additions & 0 deletions lib/streamlit/elements/file_uploader.py
Expand Up @@ -37,6 +37,7 @@
WidgetKwargs,
register_widget,
)
from streamlit.runtime.state.common import new_compute_widget_id
from streamlit.runtime.uploaded_file_manager import UploadedFile, UploadedFileRec
from streamlit.type_util import Key, LabelVisibility, maybe_raise_label_warnings, to_key

Expand Down Expand Up @@ -388,6 +389,17 @@ def _file_uploader(
check_session_state_rules(default_value=None, key=key, writes_allowed=False)
maybe_raise_label_warnings(label, label_visibility)

id = new_compute_widget_id(
"file_uploader",
user_key=key,
label=label,
type=type,
accept_multiple_files=accept_multiple_files,
key=key,
help=help,
form_id=current_form_id(self.dg),
)

if type:
if isinstance(type, str):
type = [type]
Expand All @@ -408,6 +420,7 @@ def _file_uploader(
type.append(x)

file_uploader_proto = FileUploaderProto()
file_uploader_proto.id = id
file_uploader_proto.label = label
file_uploader_proto.type[:] = type if type is not None else []
file_uploader_proto.max_upload_size_mb = config.get_option(
Expand Down
15 changes: 15 additions & 0 deletions lib/streamlit/elements/multiselect.py
Expand Up @@ -43,6 +43,7 @@
WidgetKwargs,
register_widget,
)
from streamlit.runtime.state.common import new_compute_widget_id
from streamlit.type_util import (
Key,
LabelVisibility,
Expand Down Expand Up @@ -293,8 +294,22 @@ def _multiselect(
opt = ensure_indexable(options)
maybe_raise_label_warnings(label, label_visibility)

id = new_compute_widget_id(
"multiselect",
user_key=key,
label=label,
options=[str(format_func(option)) for option in opt],
default=default,
key=key,
help=help,
max_selections=max_selections,
placeholder=placeholder,
form_id=current_form_id(self.dg),
)

indices = _check_and_convert_to_indices(opt, default)
multiselect_proto = MultiSelectProto()
multiselect_proto.id = id
multiselect_proto.label = label
default_value: List[int] = [] if indices is None else indices
multiselect_proto.default[:] = default_value
Expand Down
17 changes: 17 additions & 0 deletions lib/streamlit/elements/number_input.py
Expand Up @@ -36,6 +36,7 @@
WidgetKwargs,
register_widget,
)
from streamlit.runtime.state.common import new_compute_widget_id
from streamlit.type_util import Key, LabelVisibility, maybe_raise_label_warnings, to_key

Number = Union[int, float]
Expand Down Expand Up @@ -217,6 +218,21 @@ def _number_input(
default_value=None if isinstance(value, NoValue) else value, key=key
)
maybe_raise_label_warnings(label, label_visibility)

id = new_compute_widget_id(
"number_input",
user_key=key,
label=label,
min_value=min_value,
max_value=max_value,
value=value,
step=step,
format=format,
key=key,
help=help,
form_id=current_form_id(self.dg),
)

# Ensure that all arguments are of the same type.
number_input_args = [min_value, max_value, value, step]

Expand Down Expand Up @@ -328,6 +344,7 @@ def _number_input(
data_type = NumberInputProto.INT if all_ints else NumberInputProto.FLOAT

number_input_proto = NumberInputProto()
number_input_proto.id = id
number_input_proto.data_type = data_type
number_input_proto.label = label
number_input_proto.default = value
Expand Down
14 changes: 14 additions & 0 deletions lib/streamlit/elements/radio.py
Expand Up @@ -32,6 +32,7 @@
WidgetKwargs,
register_widget,
)
from streamlit.runtime.state.common import new_compute_widget_id
from streamlit.type_util import (
Key,
LabelVisibility,
Expand Down Expand Up @@ -220,6 +221,18 @@ def _radio(
maybe_raise_label_warnings(label, label_visibility)
opt = ensure_indexable(options)

id = new_compute_widget_id(
"radio",
user_key=key,
label=label,
options=[str(format_func(option)) for option in opt],
index=index,
key=key,
help=help,
horizontal=horizontal,
form_id=current_form_id(self.dg),
)

if not isinstance(index, int):
raise StreamlitAPIException(
"Radio Value has invalid type: %s" % type(index).__name__
Expand All @@ -231,6 +244,7 @@ def _radio(
)

radio_proto = RadioProto()
radio_proto.id = id
radio_proto.label = label
radio_proto.default = index
radio_proto.options[:] = [str(format_func(option)) for option in opt]
Expand Down