streamlit-nightly 1.36.1.dev20240710__py2.py3-none-any.whl → 1.36.1.dev20240711__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 +1 -0
- streamlit/delta_generator.py +2 -0
- streamlit/elements/lib/options_selector_utils.py +76 -0
- streamlit/elements/widgets/button.py +9 -1
- streamlit/elements/widgets/button_group.py +411 -0
- streamlit/elements/widgets/multiselect.py +88 -115
- streamlit/proto/ButtonGroup_pb2.py +33 -0
- streamlit/proto/ButtonGroup_pb2.pyi +122 -0
- streamlit/proto/Element_pb2.py +4 -3
- streamlit/proto/Element_pb2.pyi +9 -4
- streamlit/runtime/state/common.py +2 -0
- streamlit/runtime/state/widgets.py +1 -0
- streamlit/static/asset-manifest.json +3 -2
- streamlit/static/index.html +1 -1
- streamlit/static/static/js/1116.841caf48.chunk.js +1 -0
- streamlit/static/static/js/main.917a5920.js +2 -0
- streamlit/testing/v1/app_test.py +5 -0
- streamlit/testing/v1/element_tree.py +92 -0
- {streamlit_nightly-1.36.1.dev20240710.dist-info → streamlit_nightly-1.36.1.dev20240711.dist-info}/METADATA +1 -1
- {streamlit_nightly-1.36.1.dev20240710.dist-info → streamlit_nightly-1.36.1.dev20240711.dist-info}/RECORD +25 -20
- streamlit/static/static/js/main.2bfed63a.js +0 -2
- /streamlit/static/static/js/{main.2bfed63a.js.LICENSE.txt → main.917a5920.js.LICENSE.txt} +0 -0
- {streamlit_nightly-1.36.1.dev20240710.data → streamlit_nightly-1.36.1.dev20240711.data}/scripts/streamlit.cmd +0 -0
- {streamlit_nightly-1.36.1.dev20240710.dist-info → streamlit_nightly-1.36.1.dev20240711.dist-info}/WHEEL +0 -0
- {streamlit_nightly-1.36.1.dev20240710.dist-info → streamlit_nightly-1.36.1.dev20240711.dist-info}/entry_points.txt +0 -0
- {streamlit_nightly-1.36.1.dev20240710.dist-info → streamlit_nightly-1.36.1.dev20240711.dist-info}/top_level.txt +0 -0
streamlit/__init__.py
CHANGED
streamlit/delta_generator.py
CHANGED
@@ -73,6 +73,7 @@ from streamlit.elements.text import TextMixin
|
|
73
73
|
from streamlit.elements.toast import ToastMixin
|
74
74
|
from streamlit.elements.vega_charts import VegaChartsMixin
|
75
75
|
from streamlit.elements.widgets.button import ButtonMixin
|
76
|
+
from streamlit.elements.widgets.button_group import ButtonGroupMixin
|
76
77
|
from streamlit.elements.widgets.camera_input import CameraInputMixin
|
77
78
|
from streamlit.elements.widgets.chat import ChatMixin
|
78
79
|
from streamlit.elements.widgets.checkbox import CheckboxMixin
|
@@ -148,6 +149,7 @@ class DeltaGenerator(
|
|
148
149
|
BalloonsMixin,
|
149
150
|
BokehMixin,
|
150
151
|
ButtonMixin,
|
152
|
+
ButtonGroupMixin,
|
151
153
|
CameraInputMixin,
|
152
154
|
ChatMixin,
|
153
155
|
CheckboxMixin,
|
@@ -0,0 +1,76 @@
|
|
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
|
+
from typing import (
|
18
|
+
Any,
|
19
|
+
Sequence,
|
20
|
+
cast,
|
21
|
+
)
|
22
|
+
|
23
|
+
from streamlit.dataframe_util import OptionSequence, convert_anything_to_sequence
|
24
|
+
from streamlit.errors import StreamlitAPIException
|
25
|
+
from streamlit.type_util import (
|
26
|
+
T,
|
27
|
+
check_python_comparable,
|
28
|
+
is_type,
|
29
|
+
)
|
30
|
+
|
31
|
+
|
32
|
+
def check_and_convert_to_indices(
|
33
|
+
opt: Sequence[Any], default_values: Sequence[Any] | Any | None
|
34
|
+
) -> list[int] | None:
|
35
|
+
"""Perform validation checks and return indices based on the default values."""
|
36
|
+
if default_values is None and None not in opt:
|
37
|
+
return None
|
38
|
+
|
39
|
+
if not isinstance(default_values, list):
|
40
|
+
# This if is done before others because calling if not x (done
|
41
|
+
# right below) when x is of type pd.Series() or np.array() throws a
|
42
|
+
# ValueError exception.
|
43
|
+
if is_type(default_values, "numpy.ndarray") or is_type(
|
44
|
+
default_values, "pandas.core.series.Series"
|
45
|
+
):
|
46
|
+
default_values = list(cast(Sequence[Any], default_values))
|
47
|
+
elif (
|
48
|
+
isinstance(default_values, (tuple, set))
|
49
|
+
or default_values
|
50
|
+
and default_values not in opt
|
51
|
+
):
|
52
|
+
default_values = list(default_values)
|
53
|
+
else:
|
54
|
+
default_values = [default_values]
|
55
|
+
for value in default_values:
|
56
|
+
if value not in opt:
|
57
|
+
raise StreamlitAPIException(
|
58
|
+
f"The default value '{value}' is not part of the options. "
|
59
|
+
"Please make sure that every default values also exists in the options."
|
60
|
+
)
|
61
|
+
|
62
|
+
return [opt.index(value) for value in default_values]
|
63
|
+
|
64
|
+
|
65
|
+
def convert_to_sequence_and_check_comparable(options: OptionSequence[T]) -> Sequence[T]:
|
66
|
+
indexable_options = convert_anything_to_sequence(options)
|
67
|
+
check_python_comparable(indexable_options)
|
68
|
+
return indexable_options
|
69
|
+
|
70
|
+
|
71
|
+
def get_default_indices(
|
72
|
+
indexable_options: Sequence[T], default: Sequence[Any] | Any | None = None
|
73
|
+
) -> list[int]:
|
74
|
+
default_indices = check_and_convert_to_indices(indexable_options, default)
|
75
|
+
default_indices = default_indices if default_indices is not None else []
|
76
|
+
return default_indices
|
@@ -18,7 +18,15 @@ import io
|
|
18
18
|
import os
|
19
19
|
from dataclasses import dataclass
|
20
20
|
from textwrap import dedent
|
21
|
-
from typing import
|
21
|
+
from typing import (
|
22
|
+
TYPE_CHECKING,
|
23
|
+
BinaryIO,
|
24
|
+
Final,
|
25
|
+
Literal,
|
26
|
+
TextIO,
|
27
|
+
Union,
|
28
|
+
cast,
|
29
|
+
)
|
22
30
|
|
23
31
|
from typing_extensions import TypeAlias
|
24
32
|
|
@@ -0,0 +1,411 @@
|
|
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
|
+
from typing import (
|
18
|
+
TYPE_CHECKING,
|
19
|
+
Any,
|
20
|
+
Callable,
|
21
|
+
Final,
|
22
|
+
Literal,
|
23
|
+
Sequence,
|
24
|
+
TypeVar,
|
25
|
+
cast,
|
26
|
+
get_args,
|
27
|
+
)
|
28
|
+
|
29
|
+
from typing_extensions import TypeAlias
|
30
|
+
|
31
|
+
from streamlit.elements.form import current_form_id
|
32
|
+
from streamlit.elements.lib.options_selector_utils import (
|
33
|
+
convert_to_sequence_and_check_comparable,
|
34
|
+
get_default_indices,
|
35
|
+
)
|
36
|
+
from streamlit.elements.lib.policies import check_widget_policies
|
37
|
+
from streamlit.elements.lib.utils import (
|
38
|
+
Key,
|
39
|
+
maybe_coerce_enum_sequence,
|
40
|
+
to_key,
|
41
|
+
)
|
42
|
+
from streamlit.elements.widgets.multiselect import MultiSelectSerde
|
43
|
+
from streamlit.errors import StreamlitAPIException
|
44
|
+
from streamlit.proto.ButtonGroup_pb2 import ButtonGroup as ButtonGroupProto
|
45
|
+
from streamlit.runtime.metrics_util import gather_metrics
|
46
|
+
from streamlit.runtime.scriptrunner import get_script_run_ctx
|
47
|
+
from streamlit.runtime.state import register_widget
|
48
|
+
from streamlit.runtime.state.common import (
|
49
|
+
RegisterWidgetResult,
|
50
|
+
WidgetDeserializer,
|
51
|
+
WidgetSerializer,
|
52
|
+
compute_widget_id,
|
53
|
+
save_for_app_testing,
|
54
|
+
)
|
55
|
+
|
56
|
+
if TYPE_CHECKING:
|
57
|
+
from streamlit.dataframe_util import OptionSequence
|
58
|
+
from streamlit.delta_generator import DeltaGenerator
|
59
|
+
from streamlit.runtime.state import (
|
60
|
+
WidgetArgs,
|
61
|
+
WidgetCallback,
|
62
|
+
WidgetKwargs,
|
63
|
+
)
|
64
|
+
from streamlit.type_util import T
|
65
|
+
|
66
|
+
|
67
|
+
V = TypeVar("V")
|
68
|
+
|
69
|
+
_THUMB_ICONS: Final = (":material/thumb_up:", ":material/thumb_down:")
|
70
|
+
_FACES_ICONS: Final = (
|
71
|
+
":material/sentiment_sad:",
|
72
|
+
":material/sentiment_dissatisfied:",
|
73
|
+
":material/sentiment_neutral:",
|
74
|
+
":material/sentiment_satisfied:",
|
75
|
+
":material/sentiment_very_satisfied:",
|
76
|
+
)
|
77
|
+
_NUMBER_STARS: Final = 5
|
78
|
+
_STAR_ICON: Final = ":material/star:"
|
79
|
+
# we don't have the filled-material icon library as a dependency. Hence, we have it here
|
80
|
+
# in base64 format and send it over the wire as an image.
|
81
|
+
_SELECTED_STAR_ICON: Final = (
|
82
|
+
"<img src='data:image/svg+xml;base64,"
|
83
|
+
"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGhlaWdodD0iMjRweCIgdmlld0JveD0i"
|
84
|
+
"MCAtOTYwIDk2MCA5NjAiIHdpZHRoPSIyNHB4IiBmaWxsPSIjNWY2MzY4Ij48cGF0aCBkPSJNNDgwLTI2OSAz"
|
85
|
+
"MTQtMTY5cS0xMSA3LTIzIDZ0LTIxLThxLTktNy0xNC0xNy41dC0yLTIzLjVsNDQtMTg5LTE0Ny0xMjdxLTEwL"
|
86
|
+
"TktMTIuNS0yMC41VDE0MC01NzFxNC0xMSAxMi0xOHQyMi05bDE5NC0xNyA3NS0xNzhxNS0xMiAxNS41LTE4dDI"
|
87
|
+
"xLjUtNnExMSAwIDIxLjUgNnQxNS41IDE4bDc1IDE3OCAxOTQgMTdxMTQgMiAyMiA5dDEyIDE4cTQgMTEgMS41I"
|
88
|
+
"DIyLjVUODA5LTUyOEw2NjItNDAxbDQ0IDE4OXEzIDEzLTIgMjMuNVQ2OTAtMTcxcS05IDctMjEgOHQtMjMtNkw"
|
89
|
+
"0ODAtMjY5WiIvPjwvc3ZnPg=='/>"
|
90
|
+
)
|
91
|
+
|
92
|
+
_FeedbackOptions: TypeAlias = Literal["thumbs", "faces", "stars"]
|
93
|
+
|
94
|
+
|
95
|
+
class FeedbackSerde:
|
96
|
+
"""Uses the MultiSelectSerde under-the-hood, but accepts a single index value
|
97
|
+
and deserializes to a single index value. This is because for feedback, we always
|
98
|
+
allow just a single selection.
|
99
|
+
|
100
|
+
When a sentiment_mapping is provided, the sentiment corresponding to the index is
|
101
|
+
serialized/deserialized. Otherwise, the index is used as the sentiment.
|
102
|
+
"""
|
103
|
+
|
104
|
+
def __init__(self, option_indices: list[int]) -> None:
|
105
|
+
"""Initialize the FeedbackSerde with a list of sentimets."""
|
106
|
+
self.multiselect_serde: MultiSelectSerde[int] = MultiSelectSerde(option_indices)
|
107
|
+
|
108
|
+
def serialize(self, value: int | None) -> list[int]:
|
109
|
+
"""Serialize the passed sentiment option into its corresponding index
|
110
|
+
(wrapped in a list).
|
111
|
+
"""
|
112
|
+
_value = [value] if value is not None else []
|
113
|
+
return self.multiselect_serde.serialize(_value)
|
114
|
+
|
115
|
+
def deserialize(self, ui_value: list[int], widget_id: str = "") -> int | None:
|
116
|
+
"""Receive a list of indices and return the corresponding sentiments."""
|
117
|
+
deserialized = self.multiselect_serde.deserialize(ui_value, widget_id)
|
118
|
+
|
119
|
+
if len(deserialized) == 0:
|
120
|
+
return None
|
121
|
+
|
122
|
+
return deserialized[0]
|
123
|
+
|
124
|
+
|
125
|
+
def get_mapped_options(
|
126
|
+
feedback_option: _FeedbackOptions,
|
127
|
+
) -> tuple[list[ButtonGroupProto.Option], list[int]]:
|
128
|
+
# options object understandable by the web app
|
129
|
+
options: list[ButtonGroupProto.Option] = []
|
130
|
+
# we use the option index in the webapp communication to
|
131
|
+
# indicate which option is selected
|
132
|
+
options_indices: list[int] = []
|
133
|
+
|
134
|
+
if feedback_option == "thumbs":
|
135
|
+
# reversing the index mapping to have thumbs up first (but still with the higher
|
136
|
+
# index (=sentiment) in the list)
|
137
|
+
options_indices = list(reversed(range(len(_THUMB_ICONS))))
|
138
|
+
options = [ButtonGroupProto.Option(content=icon) for icon in _THUMB_ICONS]
|
139
|
+
elif feedback_option == "faces":
|
140
|
+
options_indices = list(range(len(_FACES_ICONS)))
|
141
|
+
options = [ButtonGroupProto.Option(content=icon) for icon in _FACES_ICONS]
|
142
|
+
elif feedback_option == "stars":
|
143
|
+
options_indices = list(range(_NUMBER_STARS))
|
144
|
+
options = [
|
145
|
+
ButtonGroupProto.Option(
|
146
|
+
content=_STAR_ICON,
|
147
|
+
selected_content=_SELECTED_STAR_ICON,
|
148
|
+
)
|
149
|
+
] * _NUMBER_STARS
|
150
|
+
|
151
|
+
return options, options_indices
|
152
|
+
|
153
|
+
|
154
|
+
def _build_proto(
|
155
|
+
widget_id: str,
|
156
|
+
formatted_options: Sequence[ButtonGroupProto.Option],
|
157
|
+
default_values: list[int],
|
158
|
+
disabled: bool,
|
159
|
+
current_form_id: str,
|
160
|
+
click_mode: ButtonGroupProto.ClickMode.ValueType,
|
161
|
+
selection_visualization: ButtonGroupProto.SelectionVisualization.ValueType = (
|
162
|
+
ButtonGroupProto.SelectionVisualization.ONLY_SELECTED
|
163
|
+
),
|
164
|
+
) -> ButtonGroupProto:
|
165
|
+
proto = ButtonGroupProto()
|
166
|
+
|
167
|
+
proto.id = widget_id
|
168
|
+
proto.default[:] = default_values
|
169
|
+
proto.form_id = current_form_id
|
170
|
+
proto.disabled = disabled
|
171
|
+
proto.click_mode = click_mode
|
172
|
+
|
173
|
+
for formatted_option in formatted_options:
|
174
|
+
proto.options.append(formatted_option)
|
175
|
+
proto.selection_visualization = selection_visualization
|
176
|
+
return proto
|
177
|
+
|
178
|
+
|
179
|
+
class ButtonGroupMixin:
|
180
|
+
@gather_metrics("feedback")
|
181
|
+
def feedback(
|
182
|
+
self,
|
183
|
+
options: _FeedbackOptions = "thumbs",
|
184
|
+
*,
|
185
|
+
key: str | None = None,
|
186
|
+
disabled: bool = False,
|
187
|
+
on_change: WidgetCallback | None = None,
|
188
|
+
args: Any | None = None,
|
189
|
+
kwargs: Any | None = None,
|
190
|
+
) -> int | None:
|
191
|
+
"""Display a feedback widget.
|
192
|
+
|
193
|
+
This is useful to collect user feedback, especially in chat-based apps.
|
194
|
+
|
195
|
+
Parameters:
|
196
|
+
-----------
|
197
|
+
options: "thumbs", "faces", or "stars"
|
198
|
+
The feedback options displayed to the user. The options are:
|
199
|
+
- "thumbs" (default): displays a row of thumbs-up and thumbs-down buttons.
|
200
|
+
- "faces": displays a row of five buttons with facial expressions, each
|
201
|
+
depicting increasing satisfaction from left to right.
|
202
|
+
- "stars": displays a row of star icons typically used for ratings.
|
203
|
+
key : str or int
|
204
|
+
An optional string or integer to use as the unique key for the widget.
|
205
|
+
If this is omitted, a key will be generated for the widget
|
206
|
+
based on its content. Multiple widgets of the same type may
|
207
|
+
not share the same key.
|
208
|
+
disabled : bool
|
209
|
+
An optional boolean, which disables the multiselect widget if set
|
210
|
+
to True. The default is False. This argument can only be supplied
|
211
|
+
by keyword.
|
212
|
+
on_change : callable
|
213
|
+
An optional callback invoked when this multiselect's value changes.
|
214
|
+
args : tuple
|
215
|
+
An optional tuple of args to pass to the callback.
|
216
|
+
kwargs : dict
|
217
|
+
An optional dict of kwargs to pass to the callback.
|
218
|
+
|
219
|
+
Returns
|
220
|
+
-------
|
221
|
+
An integer indicating the user's selection, where 0 is the lowest
|
222
|
+
feedback and higher values indicate more positive feedback.
|
223
|
+
If no option was selected, returns None.
|
224
|
+
- For "thumbs": a return value of 0 is for thumbs-down and 1 for thumbs-up.
|
225
|
+
- For "faces" and "stars":
|
226
|
+
values range from 0 (least satisfied) to 4 (most satisfied).
|
227
|
+
|
228
|
+
|
229
|
+
Examples
|
230
|
+
--------
|
231
|
+
Example 1: Display a feedback widget with stars and show the selected sentiment
|
232
|
+
|
233
|
+
>>> import streamlit as st
|
234
|
+
>>> sentiment_mapping: = [0.0, 0.25, 0.5, 0.75, 1.0]
|
235
|
+
>>> selected = st.feedback("stars")
|
236
|
+
>>> st.write(f"You selected: {sentiment_mapping[selected]}")
|
237
|
+
|
238
|
+
Example 2: Display a feedback widget with thumbs and show the selected sentiment
|
239
|
+
|
240
|
+
>>> import streamlit as st
|
241
|
+
>>> sentiment_mapping: = [0.0, 1.0]
|
242
|
+
>>> selected = st.feedback("thumbs")
|
243
|
+
>>> st.write(f"You selected: {sentiment_mapping[selected]}")
|
244
|
+
"""
|
245
|
+
|
246
|
+
if not isinstance(options, list) and options not in get_args(_FeedbackOptions):
|
247
|
+
raise StreamlitAPIException(
|
248
|
+
"The options argument to st.feedback must be one of "
|
249
|
+
"['thumbs', 'faces', 'stars']. "
|
250
|
+
f"The argument passed was '{options}'."
|
251
|
+
)
|
252
|
+
transformed_options, options_indices = get_mapped_options(options)
|
253
|
+
serde = FeedbackSerde(options_indices)
|
254
|
+
|
255
|
+
selection_visualization = ButtonGroupProto.SelectionVisualization.ONLY_SELECTED
|
256
|
+
if options == "stars":
|
257
|
+
selection_visualization = (
|
258
|
+
ButtonGroupProto.SelectionVisualization.ALL_UP_TO_SELECTED
|
259
|
+
)
|
260
|
+
|
261
|
+
sentiment = self._button_group(
|
262
|
+
transformed_options,
|
263
|
+
default=None,
|
264
|
+
key=key,
|
265
|
+
click_mode=ButtonGroupProto.SINGLE_SELECT,
|
266
|
+
disabled=disabled,
|
267
|
+
deserializer=serde.deserialize,
|
268
|
+
serializer=serde.serialize,
|
269
|
+
on_change=on_change,
|
270
|
+
args=args,
|
271
|
+
kwargs=kwargs,
|
272
|
+
selection_visualization=selection_visualization,
|
273
|
+
)
|
274
|
+
return sentiment.value
|
275
|
+
|
276
|
+
# Disable this more generic widget for now
|
277
|
+
# @gather_metrics("button_group")
|
278
|
+
def _internal_button_group(
|
279
|
+
self,
|
280
|
+
options: OptionSequence[V],
|
281
|
+
*,
|
282
|
+
key: Key | None = None,
|
283
|
+
default: Sequence[Any] | None = None,
|
284
|
+
click_mode: Literal["select", "multiselect"] = "select",
|
285
|
+
disabled: bool = False,
|
286
|
+
format_func: Callable[[V], dict[str, str]] | None = None,
|
287
|
+
on_change: WidgetCallback | None = None,
|
288
|
+
args: WidgetArgs | None = None,
|
289
|
+
kwargs: WidgetKwargs | None = None,
|
290
|
+
) -> list[V]:
|
291
|
+
def _transformed_format_func(x: V) -> ButtonGroupProto.Option:
|
292
|
+
if format_func is None:
|
293
|
+
return ButtonGroupProto.Option(content=str(x))
|
294
|
+
|
295
|
+
transformed = format_func(x)
|
296
|
+
return ButtonGroupProto.Option(
|
297
|
+
content=transformed["content"],
|
298
|
+
selected_content=transformed["selected_content"],
|
299
|
+
)
|
300
|
+
|
301
|
+
indexable_options = convert_to_sequence_and_check_comparable(options)
|
302
|
+
default_values = get_default_indices(indexable_options, default)
|
303
|
+
serde = MultiSelectSerde(indexable_options, default_values)
|
304
|
+
|
305
|
+
res = self._button_group(
|
306
|
+
indexable_options,
|
307
|
+
key=key,
|
308
|
+
default=default_values,
|
309
|
+
click_mode=ButtonGroupProto.ClickMode.MULTI_SELECT
|
310
|
+
if click_mode == "multiselect"
|
311
|
+
else ButtonGroupProto.SINGLE_SELECT,
|
312
|
+
disabled=disabled,
|
313
|
+
format_func=_transformed_format_func,
|
314
|
+
serializer=serde.serialize,
|
315
|
+
deserializer=serde.deserialize,
|
316
|
+
on_change=on_change,
|
317
|
+
args=args,
|
318
|
+
kwargs=kwargs,
|
319
|
+
after_register_callback=lambda widget_state: maybe_coerce_enum_sequence(
|
320
|
+
widget_state, options, indexable_options
|
321
|
+
),
|
322
|
+
)
|
323
|
+
return res.value
|
324
|
+
|
325
|
+
def _button_group(
|
326
|
+
self,
|
327
|
+
indexable_options: Sequence[Any],
|
328
|
+
*,
|
329
|
+
key: Key | None = None,
|
330
|
+
default: list[int] | None = None,
|
331
|
+
click_mode: ButtonGroupProto.ClickMode.ValueType = (
|
332
|
+
ButtonGroupProto.SINGLE_SELECT
|
333
|
+
),
|
334
|
+
disabled: bool = False,
|
335
|
+
format_func: Callable[[V], ButtonGroupProto.Option] | None = None,
|
336
|
+
deserializer: WidgetDeserializer[T],
|
337
|
+
serializer: WidgetSerializer[T],
|
338
|
+
on_change: WidgetCallback | None = None,
|
339
|
+
args: WidgetArgs | None = None,
|
340
|
+
kwargs: WidgetKwargs | None = None,
|
341
|
+
selection_visualization: ButtonGroupProto.SelectionVisualization.ValueType = (
|
342
|
+
ButtonGroupProto.SelectionVisualization.ONLY_SELECTED
|
343
|
+
),
|
344
|
+
after_register_callback: Callable[
|
345
|
+
[RegisterWidgetResult[T]], RegisterWidgetResult[T]
|
346
|
+
]
|
347
|
+
| None = None,
|
348
|
+
) -> RegisterWidgetResult[T]:
|
349
|
+
key = to_key(key)
|
350
|
+
|
351
|
+
check_widget_policies(self.dg, key, on_change, default_value=default)
|
352
|
+
|
353
|
+
widget_name = "button_group"
|
354
|
+
ctx = get_script_run_ctx()
|
355
|
+
form_id = current_form_id(self.dg)
|
356
|
+
formatted_options = (
|
357
|
+
indexable_options
|
358
|
+
if format_func is None
|
359
|
+
else [format_func(option) for option in indexable_options]
|
360
|
+
)
|
361
|
+
widget_id = compute_widget_id(
|
362
|
+
widget_name,
|
363
|
+
user_key=key,
|
364
|
+
key=key,
|
365
|
+
options=formatted_options,
|
366
|
+
default=default,
|
367
|
+
form_id=form_id,
|
368
|
+
click_mode=click_mode,
|
369
|
+
page=ctx.active_script_hash if ctx else None,
|
370
|
+
)
|
371
|
+
|
372
|
+
proto = _build_proto(
|
373
|
+
widget_id,
|
374
|
+
formatted_options,
|
375
|
+
default or [],
|
376
|
+
disabled,
|
377
|
+
form_id,
|
378
|
+
click_mode=click_mode,
|
379
|
+
selection_visualization=selection_visualization,
|
380
|
+
)
|
381
|
+
|
382
|
+
widget_state = register_widget(
|
383
|
+
widget_name,
|
384
|
+
proto,
|
385
|
+
# user_key=key,
|
386
|
+
on_change_handler=on_change,
|
387
|
+
args=args,
|
388
|
+
kwargs=kwargs,
|
389
|
+
deserializer=deserializer,
|
390
|
+
serializer=serializer,
|
391
|
+
ctx=ctx,
|
392
|
+
)
|
393
|
+
|
394
|
+
if after_register_callback is not None:
|
395
|
+
widget_state = after_register_callback(widget_state)
|
396
|
+
|
397
|
+
if widget_state.value_changed:
|
398
|
+
proto.value[:] = serializer(widget_state.value)
|
399
|
+
proto.set_value = True
|
400
|
+
|
401
|
+
if ctx:
|
402
|
+
save_for_app_testing(ctx, widget_id, format_func)
|
403
|
+
|
404
|
+
self.dg._enqueue(widget_name, proto)
|
405
|
+
|
406
|
+
return widget_state
|
407
|
+
|
408
|
+
@property
|
409
|
+
def dg(self) -> DeltaGenerator:
|
410
|
+
"""Get our DeltaGenerator."""
|
411
|
+
return cast("DeltaGenerator", self)
|