streamlit-nightly 1.38.1.dev20240918__py2.py3-none-any.whl → 1.38.1.dev20240920__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/elements/deck_gl_json_chart.py +26 -2
- streamlit/elements/map.py +27 -1
- streamlit/elements/widgets/button_group.py +269 -68
- streamlit/navigation/page.py +10 -6
- streamlit/proto/ButtonGroup_pb2.py +12 -9
- streamlit/proto/ButtonGroup_pb2.pyi +50 -3
- streamlit/proto/DeckGlJsonChart_pb2.py +3 -3
- streamlit/proto/DeckGlJsonChart_pb2.pyi +9 -1
- streamlit/static/asset-manifest.json +9 -8
- streamlit/static/index.html +1 -1
- streamlit/static/static/js/2055.bca43613.chunk.js +1 -0
- streamlit/static/static/js/5625.fe6c22ad.chunk.js +1 -0
- streamlit/static/static/js/{7077.e21833ae.chunk.js → 6789.f8dde736.chunk.js} +2 -2
- streamlit/static/static/js/8485.81bdf474.chunk.js +1 -0
- streamlit/static/static/js/9943.d18fdff1.chunk.js +1 -0
- streamlit/static/static/js/main.829dd23b.js +28 -0
- streamlit/web/server/routes.py +6 -0
- streamlit/web/server/server.py +9 -1
- streamlit/web/server/server_util.py +10 -3
- {streamlit_nightly-1.38.1.dev20240918.dist-info → streamlit_nightly-1.38.1.dev20240920.dist-info}/METADATA +1 -1
- {streamlit_nightly-1.38.1.dev20240918.dist-info → streamlit_nightly-1.38.1.dev20240920.dist-info}/RECORD +30 -29
- streamlit/static/static/js/3156.002c6ee0.chunk.js +0 -1
- streamlit/static/static/js/5625.3a8dc81f.chunk.js +0 -1
- streamlit/static/static/js/7493.95e79b96.chunk.js +0 -1
- streamlit/static/static/js/main.e8447cae.js +0 -28
- /streamlit/static/static/css/{7077.81b3d18f.chunk.css → 6789.81b3d18f.chunk.css} +0 -0
- /streamlit/static/static/css/{3156.93909c7e.chunk.css → 9943.93909c7e.chunk.css} +0 -0
- /streamlit/static/static/js/{7077.e21833ae.chunk.js.LICENSE.txt → 6789.f8dde736.chunk.js.LICENSE.txt} +0 -0
- /streamlit/static/static/js/{main.e8447cae.js.LICENSE.txt → main.829dd23b.js.LICENSE.txt} +0 -0
- {streamlit_nightly-1.38.1.dev20240918.data → streamlit_nightly-1.38.1.dev20240920.data}/scripts/streamlit.cmd +0 -0
- {streamlit_nightly-1.38.1.dev20240918.dist-info → streamlit_nightly-1.38.1.dev20240920.dist-info}/WHEEL +0 -0
- {streamlit_nightly-1.38.1.dev20240918.dist-info → streamlit_nightly-1.38.1.dev20240920.dist-info}/entry_points.txt +0 -0
- {streamlit_nightly-1.38.1.dev20240918.dist-info → streamlit_nightly-1.38.1.dev20240920.dist-info}/top_level.txt +0 -0
streamlit/__init__.py
CHANGED
@@ -39,6 +39,8 @@ class PydeckMixin:
|
|
39
39
|
self,
|
40
40
|
pydeck_obj: Deck | None = None,
|
41
41
|
use_container_width: bool = False,
|
42
|
+
width: int | None = None,
|
43
|
+
height: int | None = None,
|
42
44
|
) -> DeltaGenerator:
|
43
45
|
"""Draw a chart using the PyDeck library.
|
44
46
|
|
@@ -68,7 +70,7 @@ class PydeckMixin:
|
|
68
70
|
|
69
71
|
Parameters
|
70
72
|
----------
|
71
|
-
pydeck_obj: pydeck.Deck or None
|
73
|
+
pydeck_obj : pydeck.Deck or None
|
72
74
|
Object specifying the PyDeck chart to draw.
|
73
75
|
use_container_width : bool
|
74
76
|
Whether to override the figure's native width with the width of
|
@@ -77,6 +79,19 @@ class PydeckMixin:
|
|
77
79
|
according to the plotting library, up to the width of the parent
|
78
80
|
container. If ``use_container_width`` is ``True``, Streamlit sets
|
79
81
|
the width of the figure to match the width of the parent container.
|
82
|
+
width : int or None
|
83
|
+
Desired width of the chart expressed in pixels. If ``width`` is
|
84
|
+
``None`` (default), Streamlit sets the width of the chart to fit
|
85
|
+
its contents according to the plotting library, up to the width of
|
86
|
+
the parent container. If ``width`` is greater than the width of the
|
87
|
+
parent container, Streamlit sets the chart width to match the width
|
88
|
+
of the parent container.
|
89
|
+
|
90
|
+
To use ``width``, you must set ``use_container_width=False``.
|
91
|
+
height : int or None
|
92
|
+
Desired height of the chart expressed in pixels. If ``height`` is
|
93
|
+
``None`` (default), Streamlit sets the height of the chart to fit
|
94
|
+
its contents according to the plotting library.
|
80
95
|
|
81
96
|
Example
|
82
97
|
-------
|
@@ -134,7 +149,9 @@ class PydeckMixin:
|
|
134
149
|
|
135
150
|
"""
|
136
151
|
pydeck_proto = PydeckProto()
|
137
|
-
marshall(
|
152
|
+
marshall(
|
153
|
+
pydeck_proto, pydeck_obj, use_container_width, width=width, height=height
|
154
|
+
)
|
138
155
|
return self.dg._enqueue("deck_gl_json_chart", pydeck_proto)
|
139
156
|
|
140
157
|
@property
|
@@ -165,6 +182,8 @@ def marshall(
|
|
165
182
|
pydeck_proto: PydeckProto,
|
166
183
|
pydeck_obj: Deck | None,
|
167
184
|
use_container_width: bool,
|
185
|
+
width: int | None = None,
|
186
|
+
height: int | None = None,
|
168
187
|
) -> None:
|
169
188
|
if pydeck_obj is None:
|
170
189
|
spec = json.dumps(EMPTY_MAP)
|
@@ -174,6 +193,11 @@ def marshall(
|
|
174
193
|
pydeck_proto.json = spec
|
175
194
|
pydeck_proto.use_container_width = use_container_width
|
176
195
|
|
196
|
+
if width:
|
197
|
+
pydeck_proto.width = width
|
198
|
+
if height:
|
199
|
+
pydeck_proto.height = height
|
200
|
+
|
177
201
|
pydeck_proto.id = ""
|
178
202
|
|
179
203
|
tooltip = _get_pydeck_tooltip(pydeck_obj)
|
streamlit/elements/map.py
CHANGED
@@ -84,6 +84,8 @@ class MapMixin:
|
|
84
84
|
size: None | str | float = None,
|
85
85
|
zoom: int | None = None,
|
86
86
|
use_container_width: bool = True,
|
87
|
+
width: int | None = None,
|
88
|
+
height: int | None = None,
|
87
89
|
) -> DeltaGenerator:
|
88
90
|
"""Display a map with a scatterplot overlaid onto it.
|
89
91
|
|
@@ -162,6 +164,21 @@ class MapMixin:
|
|
162
164
|
Streamlit sets the width of the chart to fit its contents according
|
163
165
|
to the plotting library, up to the width of the parent container.
|
164
166
|
|
167
|
+
width : int or None
|
168
|
+
Desired width of the chart expressed in pixels. If ``width`` is
|
169
|
+
``None`` (default), Streamlit sets the width of the chart to fit
|
170
|
+
its contents according to the plotting library, up to the width of
|
171
|
+
the parent container. If ``width`` is greater than the width of the
|
172
|
+
parent container, Streamlit sets the chart width to match the width
|
173
|
+
of the parent container.
|
174
|
+
|
175
|
+
To use ``width``, you must set ``use_container_width=False``.
|
176
|
+
|
177
|
+
height : int or None
|
178
|
+
Desired height of the chart expressed in pixels. If ``height`` is
|
179
|
+
``None`` (default), Streamlit sets the height of the chart to fit
|
180
|
+
its contents according to the plotting library.
|
181
|
+
|
165
182
|
Examples
|
166
183
|
--------
|
167
184
|
>>> import streamlit as st
|
@@ -223,7 +240,9 @@ class MapMixin:
|
|
223
240
|
deck_gl_json = to_deckgl_json(
|
224
241
|
data, latitude, longitude, size, color, map_style, zoom
|
225
242
|
)
|
226
|
-
marshall(
|
243
|
+
marshall(
|
244
|
+
map_proto, deck_gl_json, use_container_width, width=width, height=height
|
245
|
+
)
|
227
246
|
return self.dg._enqueue("deck_gl_json_chart", map_proto)
|
228
247
|
|
229
248
|
@property
|
@@ -471,8 +490,15 @@ def marshall(
|
|
471
490
|
pydeck_proto: DeckGlJsonChartProto,
|
472
491
|
pydeck_json: str,
|
473
492
|
use_container_width: bool,
|
493
|
+
height: int | None = None,
|
494
|
+
width: int | None = None,
|
474
495
|
) -> None:
|
475
496
|
pydeck_proto.json = pydeck_json
|
476
497
|
pydeck_proto.use_container_width = use_container_width
|
477
498
|
|
499
|
+
if width:
|
500
|
+
pydeck_proto.width = width
|
501
|
+
if height:
|
502
|
+
pydeck_proto.height = height
|
503
|
+
|
478
504
|
pydeck_proto.id = ""
|
@@ -19,6 +19,7 @@ from typing import (
|
|
19
19
|
Any,
|
20
20
|
Callable,
|
21
21
|
Final,
|
22
|
+
Generic,
|
22
23
|
Literal,
|
23
24
|
Sequence,
|
24
25
|
TypeVar,
|
@@ -26,16 +27,22 @@ from typing import (
|
|
26
27
|
overload,
|
27
28
|
)
|
28
29
|
|
30
|
+
from typing_extensions import TypeAlias
|
31
|
+
|
29
32
|
from streamlit.elements.lib.form_utils import current_form_id
|
30
33
|
from streamlit.elements.lib.options_selector_utils import (
|
31
34
|
convert_to_sequence_and_check_comparable,
|
32
35
|
get_default_indices,
|
33
|
-
maybe_coerce_enum_sequence,
|
34
36
|
)
|
35
|
-
from streamlit.elements.lib.policies import
|
37
|
+
from streamlit.elements.lib.policies import (
|
38
|
+
check_widget_policies,
|
39
|
+
maybe_raise_label_warnings,
|
40
|
+
)
|
36
41
|
from streamlit.elements.lib.utils import (
|
37
42
|
Key,
|
43
|
+
LabelVisibility,
|
38
44
|
compute_and_register_element_id,
|
45
|
+
get_label_visibility_proto_value,
|
39
46
|
save_for_app_testing,
|
40
47
|
to_key,
|
41
48
|
)
|
@@ -45,6 +52,8 @@ from streamlit.proto.ButtonGroup_pb2 import ButtonGroup as ButtonGroupProto
|
|
45
52
|
from streamlit.runtime.metrics_util import gather_metrics
|
46
53
|
from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
|
47
54
|
from streamlit.runtime.state import register_widget
|
55
|
+
from streamlit.string_util import validate_material_icon
|
56
|
+
from streamlit.type_util import T
|
48
57
|
|
49
58
|
if TYPE_CHECKING:
|
50
59
|
from streamlit.dataframe_util import OptionSequence
|
@@ -59,7 +68,6 @@ if TYPE_CHECKING:
|
|
59
68
|
WidgetDeserializer,
|
60
69
|
WidgetSerializer,
|
61
70
|
)
|
62
|
-
from streamlit.type_util import T
|
63
71
|
|
64
72
|
|
65
73
|
V = TypeVar("V")
|
@@ -78,29 +86,35 @@ _STAR_ICON: Final = ":material/star:"
|
|
78
86
|
# in base64 format and send it over the wire as an image.
|
79
87
|
_SELECTED_STAR_ICON: Final = ":material/star_filled:"
|
80
88
|
|
89
|
+
SelectionMode: TypeAlias = Literal["single", "multiple"]
|
90
|
+
|
81
91
|
|
82
|
-
class
|
92
|
+
class SingleSelectSerde(Generic[T]):
|
83
93
|
"""Uses the MultiSelectSerde under-the-hood, but accepts a single index value
|
84
|
-
and deserializes to a single index value.
|
85
|
-
|
94
|
+
and deserializes to a single index value.
|
95
|
+
This is because button_group can be single and multi select, but we use the same
|
96
|
+
proto for both and, thus, map single values to a list of values and a receiving
|
97
|
+
value wrapped in a list to a single value.
|
86
98
|
|
87
|
-
When a
|
88
|
-
|
99
|
+
When a default_value is provided is provided, the option corresponding to the
|
100
|
+
index is serialized/deserialized.
|
89
101
|
"""
|
90
102
|
|
91
|
-
def __init__(
|
92
|
-
|
93
|
-
|
103
|
+
def __init__(
|
104
|
+
self,
|
105
|
+
option_indices: Sequence[T],
|
106
|
+
default_value: list[int] | None = None,
|
107
|
+
) -> None:
|
108
|
+
# see docstring about why we use MultiSelectSerde here
|
109
|
+
self.multiselect_serde: MultiSelectSerde[T] = MultiSelectSerde(
|
110
|
+
option_indices, default_value if default_value is not None else []
|
111
|
+
)
|
94
112
|
|
95
|
-
def serialize(self, value:
|
96
|
-
"""Serialize the passed sentiment option into its corresponding index
|
97
|
-
(wrapped in a list).
|
98
|
-
"""
|
113
|
+
def serialize(self, value: T | None) -> list[int]:
|
99
114
|
_value = [value] if value is not None else []
|
100
115
|
return self.multiselect_serde.serialize(_value)
|
101
116
|
|
102
|
-
def deserialize(self, ui_value: list[int], widget_id: str = "") ->
|
103
|
-
"""Receive a list of indices and return the corresponding sentiments."""
|
117
|
+
def deserialize(self, ui_value: list[int] | None, widget_id: str = "") -> T | None:
|
104
118
|
deserialized = self.multiselect_serde.deserialize(ui_value, widget_id)
|
105
119
|
|
106
120
|
if len(deserialized) == 0:
|
@@ -109,6 +123,40 @@ class FeedbackSerde:
|
|
109
123
|
return deserialized[0]
|
110
124
|
|
111
125
|
|
126
|
+
class SingleOrMultiSelectSerde(Generic[T]):
|
127
|
+
"""A serde that can handle both single and multi select options.
|
128
|
+
|
129
|
+
It uses the same proto to wire the data, so that we can send and receive
|
130
|
+
single values via a list. We have different serdes for both cases though so
|
131
|
+
that when setting / getting the value via session_state, it is mapped correctly.
|
132
|
+
So for single select, the value will be a single value and for multi select, it will
|
133
|
+
be a list of values.
|
134
|
+
"""
|
135
|
+
|
136
|
+
def __init__(
|
137
|
+
self,
|
138
|
+
options: Sequence[T],
|
139
|
+
default_values: list[int],
|
140
|
+
type: Literal["single", "multiple"],
|
141
|
+
):
|
142
|
+
self.options = options
|
143
|
+
self.default_values = default_values
|
144
|
+
self.type = type
|
145
|
+
self.serde: SingleSelectSerde[T] | MultiSelectSerde[T] = (
|
146
|
+
SingleSelectSerde(options, default_value=default_values)
|
147
|
+
if type == "single"
|
148
|
+
else MultiSelectSerde(options, default_values)
|
149
|
+
)
|
150
|
+
|
151
|
+
def serialize(self, value: T | list[T] | None) -> list[int]:
|
152
|
+
return self.serde.serialize(cast(Any, value))
|
153
|
+
|
154
|
+
def deserialize(
|
155
|
+
self, ui_value: list[int] | None, widget_id: str = ""
|
156
|
+
) -> list[T] | T | None:
|
157
|
+
return self.serde.deserialize(ui_value, widget_id)
|
158
|
+
|
159
|
+
|
112
160
|
def get_mapped_options(
|
113
161
|
feedback_option: Literal["thumbs", "faces", "stars"],
|
114
162
|
) -> tuple[list[ButtonGroupProto.Option], list[int]]:
|
@@ -122,16 +170,16 @@ def get_mapped_options(
|
|
122
170
|
# reversing the index mapping to have thumbs up first (but still with the higher
|
123
171
|
# index (=sentiment) in the list)
|
124
172
|
options_indices = list(reversed(range(len(_THUMB_ICONS))))
|
125
|
-
options = [ButtonGroupProto.Option(
|
173
|
+
options = [ButtonGroupProto.Option(content_icon=icon) for icon in _THUMB_ICONS]
|
126
174
|
elif feedback_option == "faces":
|
127
175
|
options_indices = list(range(len(_FACES_ICONS)))
|
128
|
-
options = [ButtonGroupProto.Option(
|
176
|
+
options = [ButtonGroupProto.Option(content_icon=icon) for icon in _FACES_ICONS]
|
129
177
|
elif feedback_option == "stars":
|
130
178
|
options_indices = list(range(_NUMBER_STARS))
|
131
179
|
options = [
|
132
180
|
ButtonGroupProto.Option(
|
133
|
-
|
134
|
-
|
181
|
+
content_icon=_STAR_ICON,
|
182
|
+
selected_content_icon=_SELECTED_STAR_ICON,
|
135
183
|
)
|
136
184
|
] * _NUMBER_STARS
|
137
185
|
|
@@ -148,6 +196,10 @@ def _build_proto(
|
|
148
196
|
selection_visualization: ButtonGroupProto.SelectionVisualization.ValueType = (
|
149
197
|
ButtonGroupProto.SelectionVisualization.ONLY_SELECTED
|
150
198
|
),
|
199
|
+
style: Literal["segment", "pills", "borderless"] = "segment",
|
200
|
+
label: str | None = None,
|
201
|
+
label_visibility: LabelVisibility = "visible",
|
202
|
+
help: str | None = None,
|
151
203
|
) -> ButtonGroupProto:
|
152
204
|
proto = ButtonGroupProto()
|
153
205
|
|
@@ -156,6 +208,16 @@ def _build_proto(
|
|
156
208
|
proto.form_id = current_form_id
|
157
209
|
proto.disabled = disabled
|
158
210
|
proto.click_mode = click_mode
|
211
|
+
proto.style = ButtonGroupProto.Style.Value(style.upper())
|
212
|
+
|
213
|
+
# not passing the label looks the same as a collapsed label
|
214
|
+
if label is not None:
|
215
|
+
proto.label = label
|
216
|
+
proto.label_visibility.value = get_label_visibility_proto_value(
|
217
|
+
label_visibility
|
218
|
+
)
|
219
|
+
if help is not None:
|
220
|
+
proto.help = help
|
159
221
|
|
160
222
|
for formatted_option in formatted_options:
|
161
223
|
proto.options.append(formatted_option)
|
@@ -163,40 +225,54 @@ def _build_proto(
|
|
163
225
|
return proto
|
164
226
|
|
165
227
|
|
228
|
+
def _maybe_raise_selection_mode_warning(selection_mode: SelectionMode):
|
229
|
+
"""Check if the selection_mode value is valid or raise exception otherwise."""
|
230
|
+
if selection_mode not in ["single", "multiple"]:
|
231
|
+
raise StreamlitAPIException(
|
232
|
+
"The selection_mode argument must be one of ['single', 'multiple']. "
|
233
|
+
f"The argument passed was '{selection_mode}'."
|
234
|
+
)
|
235
|
+
|
236
|
+
|
166
237
|
class ButtonGroupMixin:
|
167
|
-
|
168
|
-
#
|
238
|
+
# These overloads are not documented in the docstring, at least not at this time, on
|
239
|
+
# the theory that most people won't know what it means. And the Literals here are a
|
240
|
+
# subclass of int anyway. Usually, we would make a type alias for
|
241
|
+
# Literal["thumbs", "faces", "stars"]; but, in this case, we don't use it in too
|
242
|
+
# many other places, and it's a more helpful autocomplete if we just enumerate the
|
243
|
+
# values explicitly, so a decision has been made to keep it as not an alias.
|
244
|
+
@overload
|
169
245
|
def feedback(
|
170
246
|
self,
|
171
247
|
options: Literal["thumbs"] = ...,
|
172
248
|
*,
|
173
|
-
key:
|
249
|
+
key: Key | None = None,
|
174
250
|
disabled: bool = False,
|
175
251
|
on_change: WidgetCallback | None = None,
|
176
|
-
args:
|
177
|
-
kwargs:
|
252
|
+
args: WidgetArgs | None = None,
|
253
|
+
kwargs: WidgetKwargs | None = None,
|
178
254
|
) -> Literal[0, 1] | None: ...
|
179
255
|
@overload
|
180
256
|
def feedback(
|
181
257
|
self,
|
182
258
|
options: Literal["faces", "stars"] = ...,
|
183
259
|
*,
|
184
|
-
key:
|
260
|
+
key: Key | None = None,
|
185
261
|
disabled: bool = False,
|
186
262
|
on_change: WidgetCallback | None = None,
|
187
|
-
args:
|
188
|
-
kwargs:
|
263
|
+
args: WidgetArgs | None = None,
|
264
|
+
kwargs: WidgetKwargs | None = None,
|
189
265
|
) -> Literal[0, 1, 2, 3, 4] | None: ...
|
190
266
|
@gather_metrics("feedback")
|
191
267
|
def feedback(
|
192
268
|
self,
|
193
269
|
options: Literal["thumbs", "faces", "stars"] = "thumbs",
|
194
270
|
*,
|
195
|
-
key:
|
271
|
+
key: Key | None = None,
|
196
272
|
disabled: bool = False,
|
197
273
|
on_change: WidgetCallback | None = None,
|
198
|
-
args:
|
199
|
-
kwargs:
|
274
|
+
args: WidgetArgs | None = None,
|
275
|
+
kwargs: WidgetKwargs | None = None,
|
200
276
|
) -> int | None:
|
201
277
|
"""Display a feedback widget.
|
202
278
|
|
@@ -288,7 +364,7 @@ class ButtonGroupMixin:
|
|
288
364
|
f"The argument passed was '{options}'."
|
289
365
|
)
|
290
366
|
transformed_options, options_indices = get_mapped_options(options)
|
291
|
-
serde =
|
367
|
+
serde = SingleSelectSerde[int](options_indices)
|
292
368
|
|
293
369
|
selection_visualization = ButtonGroupProto.SelectionVisualization.ONLY_SELECTED
|
294
370
|
if options == "stars":
|
@@ -300,7 +376,7 @@ class ButtonGroupMixin:
|
|
300
376
|
transformed_options,
|
301
377
|
default=None,
|
302
378
|
key=key,
|
303
|
-
|
379
|
+
selection_mode="single",
|
304
380
|
disabled=disabled,
|
305
381
|
deserializer=serde.deserialize,
|
306
382
|
serializer=serde.serialize,
|
@@ -308,56 +384,146 @@ class ButtonGroupMixin:
|
|
308
384
|
args=args,
|
309
385
|
kwargs=kwargs,
|
310
386
|
selection_visualization=selection_visualization,
|
387
|
+
style="borderless",
|
311
388
|
)
|
312
389
|
return sentiment.value
|
313
390
|
|
314
|
-
|
315
|
-
|
391
|
+
@gather_metrics("pills")
|
392
|
+
def pills(
|
393
|
+
self,
|
394
|
+
label: str,
|
395
|
+
options: OptionSequence[V],
|
396
|
+
*,
|
397
|
+
selection_mode: Literal["single", "multiple"] = "single",
|
398
|
+
default: Sequence[V] | V | None = None,
|
399
|
+
format_func: Callable[[Any], str] | None = None,
|
400
|
+
key: Key | None = None,
|
401
|
+
help: str | None = None,
|
402
|
+
on_change: WidgetCallback | None = None,
|
403
|
+
args: WidgetArgs | None = None,
|
404
|
+
kwargs: WidgetKwargs | None = None,
|
405
|
+
disabled: bool = False,
|
406
|
+
label_visibility: LabelVisibility = "visible",
|
407
|
+
) -> list[V] | V | None:
|
408
|
+
maybe_raise_label_warnings(label, label_visibility)
|
409
|
+
|
410
|
+
def _transformed_format_func(option: V) -> ButtonGroupProto.Option:
|
411
|
+
"""If option starts with a material icon or an emoji, we extract it to send
|
412
|
+
it parsed to the frontend."""
|
413
|
+
transformed = format_func(option) if format_func else str(option)
|
414
|
+
transformed_parts = transformed.split(" ")
|
415
|
+
icon: str | None = None
|
416
|
+
if len(transformed_parts) > 0:
|
417
|
+
maybe_icon = transformed_parts[0].strip()
|
418
|
+
try:
|
419
|
+
# we only want to extract material icons because we treat them
|
420
|
+
# differently than emojis visually
|
421
|
+
if maybe_icon.startswith(":material"):
|
422
|
+
icon = validate_material_icon(maybe_icon)
|
423
|
+
# reassamble the option string without the icon - also
|
424
|
+
# works if len(transformed_parts) == 1
|
425
|
+
transformed = " ".join(transformed_parts[1:])
|
426
|
+
except StreamlitAPIException:
|
427
|
+
# we don't have a valid icon or emoji, so we just pass
|
428
|
+
pass
|
429
|
+
return ButtonGroupProto.Option(
|
430
|
+
content=transformed,
|
431
|
+
content_icon=icon,
|
432
|
+
)
|
433
|
+
|
434
|
+
indexable_options = convert_to_sequence_and_check_comparable(options)
|
435
|
+
default_values = get_default_indices(indexable_options, default)
|
436
|
+
|
437
|
+
serde: SingleOrMultiSelectSerde[V] = SingleOrMultiSelectSerde[V](
|
438
|
+
indexable_options, default_values, selection_mode
|
439
|
+
)
|
440
|
+
res = self._button_group(
|
441
|
+
indexable_options,
|
442
|
+
key=key,
|
443
|
+
default=default_values,
|
444
|
+
selection_mode=selection_mode,
|
445
|
+
disabled=disabled,
|
446
|
+
format_func=_transformed_format_func,
|
447
|
+
serializer=serde.serialize,
|
448
|
+
deserializer=serde.deserialize,
|
449
|
+
on_change=on_change,
|
450
|
+
args=args,
|
451
|
+
kwargs=kwargs,
|
452
|
+
style="pills",
|
453
|
+
label=label,
|
454
|
+
label_visibility=label_visibility,
|
455
|
+
help=help,
|
456
|
+
)
|
457
|
+
|
458
|
+
if selection_mode == "multiple":
|
459
|
+
return res.value
|
460
|
+
|
461
|
+
return res.value
|
462
|
+
|
463
|
+
@gather_metrics("_internal_button_group")
|
316
464
|
def _internal_button_group(
|
317
465
|
self,
|
318
466
|
options: OptionSequence[V],
|
319
467
|
*,
|
320
468
|
key: Key | None = None,
|
321
|
-
default: Sequence[
|
322
|
-
|
469
|
+
default: Sequence[V] | V | None = None,
|
470
|
+
selection_mode: Literal["single", "multiple"] = "single",
|
323
471
|
disabled: bool = False,
|
324
|
-
format_func: Callable[[
|
472
|
+
format_func: Callable[[Any], str] | None = None,
|
473
|
+
style: Literal["segment", "pills"] = "segment",
|
325
474
|
on_change: WidgetCallback | None = None,
|
326
475
|
args: WidgetArgs | None = None,
|
327
476
|
kwargs: WidgetKwargs | None = None,
|
328
|
-
) -> list[V]:
|
329
|
-
def _transformed_format_func(
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
477
|
+
) -> list[V] | V | None:
|
478
|
+
def _transformed_format_func(option: V) -> ButtonGroupProto.Option:
|
479
|
+
"""If option starts with a material icon or an emoji, we extract it to send
|
480
|
+
it parsed to the frontend."""
|
481
|
+
transformed = format_func(option) if format_func else str(option)
|
482
|
+
transformed_parts = transformed.split(" ")
|
483
|
+
icon: str | None = None
|
484
|
+
if len(transformed_parts) > 0:
|
485
|
+
maybe_icon = transformed_parts[0].strip()
|
486
|
+
try:
|
487
|
+
# we only want to extract material icons because we treat them
|
488
|
+
# differently than emojis visually
|
489
|
+
if maybe_icon.startswith(":material"):
|
490
|
+
icon = validate_material_icon(maybe_icon)
|
491
|
+
# reassamble the option string without the icon - also
|
492
|
+
# works if len(transformed_parts) == 1
|
493
|
+
transformed = " ".join(transformed_parts[1:])
|
494
|
+
except StreamlitAPIException:
|
495
|
+
# we don't have a valid icon or emoji, so we just pass
|
496
|
+
pass
|
334
497
|
return ButtonGroupProto.Option(
|
335
|
-
content=transformed
|
336
|
-
|
498
|
+
content=transformed,
|
499
|
+
content_icon=icon,
|
337
500
|
)
|
338
501
|
|
339
502
|
indexable_options = convert_to_sequence_and_check_comparable(options)
|
340
503
|
default_values = get_default_indices(indexable_options, default)
|
341
|
-
|
504
|
+
|
505
|
+
serde: SingleOrMultiSelectSerde[V] = SingleOrMultiSelectSerde[V](
|
506
|
+
indexable_options, default_values, selection_mode
|
507
|
+
)
|
342
508
|
|
343
509
|
res = self._button_group(
|
344
510
|
indexable_options,
|
345
511
|
key=key,
|
346
512
|
default=default_values,
|
347
|
-
|
348
|
-
if click_mode == "multiselect"
|
349
|
-
else ButtonGroupProto.SINGLE_SELECT,
|
513
|
+
selection_mode=selection_mode,
|
350
514
|
disabled=disabled,
|
351
515
|
format_func=_transformed_format_func,
|
516
|
+
style=style,
|
352
517
|
serializer=serde.serialize,
|
353
518
|
deserializer=serde.deserialize,
|
354
519
|
on_change=on_change,
|
355
520
|
args=args,
|
356
521
|
kwargs=kwargs,
|
357
|
-
after_register_callback=lambda widget_state: maybe_coerce_enum_sequence(
|
358
|
-
widget_state, options, indexable_options
|
359
|
-
),
|
360
522
|
)
|
523
|
+
|
524
|
+
if selection_mode == "multiple":
|
525
|
+
return res.value
|
526
|
+
|
361
527
|
return res.value
|
362
528
|
|
363
529
|
def _button_group(
|
@@ -366,10 +532,9 @@ class ButtonGroupMixin:
|
|
366
532
|
*,
|
367
533
|
key: Key | None = None,
|
368
534
|
default: list[int] | None = None,
|
369
|
-
|
370
|
-
ButtonGroupProto.SINGLE_SELECT
|
371
|
-
),
|
535
|
+
selection_mode: SelectionMode = "single",
|
372
536
|
disabled: bool = False,
|
537
|
+
style: Literal["segment", "pills", "borderless"] = "segment",
|
373
538
|
format_func: Callable[[V], ButtonGroupProto.Option] | None = None,
|
374
539
|
deserializer: WidgetDeserializer[T],
|
375
540
|
serializer: WidgetSerializer[T],
|
@@ -379,14 +544,45 @@ class ButtonGroupMixin:
|
|
379
544
|
selection_visualization: ButtonGroupProto.SelectionVisualization.ValueType = (
|
380
545
|
ButtonGroupProto.SelectionVisualization.ONLY_SELECTED
|
381
546
|
),
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
| None = None,
|
547
|
+
label: str | None = None,
|
548
|
+
label_visibility: LabelVisibility = "visible",
|
549
|
+
help: str | None = None,
|
386
550
|
) -> RegisterWidgetResult[T]:
|
551
|
+
_maybe_raise_selection_mode_warning(selection_mode)
|
552
|
+
|
553
|
+
parsed_selection_mode: ButtonGroupProto.ClickMode.ValueType = (
|
554
|
+
ButtonGroupProto.SINGLE_SELECT
|
555
|
+
if selection_mode == "single"
|
556
|
+
else ButtonGroupProto.MULTI_SELECT
|
557
|
+
)
|
558
|
+
|
559
|
+
# when selection mode is a single-value selection, the default must be a single
|
560
|
+
# value too.
|
561
|
+
if (
|
562
|
+
parsed_selection_mode == ButtonGroupProto.SINGLE_SELECT
|
563
|
+
and default is not None
|
564
|
+
and isinstance(default, Sequence)
|
565
|
+
and len(default) > 1
|
566
|
+
):
|
567
|
+
# add more commands to the error message
|
568
|
+
raise StreamlitAPIException(
|
569
|
+
"The default argument to `st.pills` must be a single value when "
|
570
|
+
"`selection_mode='single'`."
|
571
|
+
)
|
572
|
+
|
573
|
+
if style not in ["segment", "pills", "borderless"]:
|
574
|
+
raise StreamlitAPIException(
|
575
|
+
"The style argument must be one of ['segment', 'pills', 'borderless']. "
|
576
|
+
f"The argument passed was '{style}'."
|
577
|
+
)
|
578
|
+
|
387
579
|
key = to_key(key)
|
388
580
|
|
389
|
-
|
581
|
+
_default = default
|
582
|
+
if default is not None and len(default) == 0:
|
583
|
+
_default = None
|
584
|
+
|
585
|
+
check_widget_policies(self.dg, key, on_change, default_value=_default)
|
390
586
|
|
391
587
|
widget_name = "button_group"
|
392
588
|
ctx = get_script_run_ctx()
|
@@ -394,7 +590,10 @@ class ButtonGroupMixin:
|
|
394
590
|
formatted_options = (
|
395
591
|
indexable_options
|
396
592
|
if format_func is None
|
397
|
-
else [
|
593
|
+
else [
|
594
|
+
format_func(indexable_options[index])
|
595
|
+
for index, _ in enumerate(indexable_options)
|
596
|
+
]
|
398
597
|
)
|
399
598
|
element_id = compute_and_register_element_id(
|
400
599
|
widget_name,
|
@@ -402,7 +601,8 @@ class ButtonGroupMixin:
|
|
402
601
|
form_id=form_id,
|
403
602
|
options=formatted_options,
|
404
603
|
default=default,
|
405
|
-
click_mode=
|
604
|
+
click_mode=parsed_selection_mode,
|
605
|
+
style=style,
|
406
606
|
)
|
407
607
|
|
408
608
|
proto = _build_proto(
|
@@ -411,8 +611,12 @@ class ButtonGroupMixin:
|
|
411
611
|
default or [],
|
412
612
|
disabled,
|
413
613
|
form_id,
|
414
|
-
click_mode=
|
614
|
+
click_mode=parsed_selection_mode,
|
415
615
|
selection_visualization=selection_visualization,
|
616
|
+
style=style,
|
617
|
+
label=label,
|
618
|
+
label_visibility=label_visibility,
|
619
|
+
help=help,
|
416
620
|
)
|
417
621
|
|
418
622
|
widget_state = register_widget(
|
@@ -426,9 +630,6 @@ class ButtonGroupMixin:
|
|
426
630
|
ctx=ctx,
|
427
631
|
)
|
428
632
|
|
429
|
-
if after_register_callback is not None:
|
430
|
-
widget_state = after_register_callback(widget_state)
|
431
|
-
|
432
633
|
if widget_state.value_changed:
|
433
634
|
proto.value[:] = serializer(widget_state.value)
|
434
635
|
proto.set_value = True
|