streamlit-nightly 1.53.2.dev20260203__py3-none-any.whl → 1.54.1.dev20260204__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.
Files changed (127) hide show
  1. streamlit/elements/arrow.py +66 -26
  2. streamlit/elements/lib/built_in_chart_utils.py +1 -1
  3. streamlit/elements/lib/column_config_utils.py +4 -4
  4. streamlit/elements/lib/options_selector_utils.py +4 -0
  5. streamlit/elements/lib/pandas_styler_utils.py +22 -22
  6. streamlit/elements/widgets/button_group.py +221 -86
  7. streamlit/elements/widgets/data_editor.py +9 -9
  8. streamlit/elements/widgets/radio.py +8 -4
  9. streamlit/hello/dataframe_demo.py +1 -1
  10. streamlit/proto/ArrowData_pb2.py +5 -3
  11. streamlit/proto/ArrowData_pb2.pyi +44 -4
  12. streamlit/proto/ArrowNamedDataSet_pb2.py +4 -4
  13. streamlit/proto/ArrowNamedDataSet_pb2.pyi +3 -3
  14. streamlit/proto/ArrowVegaLiteChart_pb2.py +4 -4
  15. streamlit/proto/ArrowVegaLiteChart_pb2.pyi +3 -3
  16. streamlit/proto/ButtonGroup_pb2.py +8 -8
  17. streamlit/proto/ButtonGroup_pb2.pyi +9 -7
  18. streamlit/proto/Dataframe_pb2.py +31 -0
  19. streamlit/proto/{Arrow_pb2.pyi → Dataframe_pb2.pyi} +39 -102
  20. streamlit/proto/Element_pb2.py +5 -4
  21. streamlit/proto/Element_pb2.pyi +11 -10
  22. streamlit/proto/Table_pb2.py +29 -0
  23. streamlit/proto/Table_pb2.pyi +83 -0
  24. streamlit/static/index.html +1 -1
  25. streamlit/static/manifest.json +308 -308
  26. streamlit/static/static/js/{ErrorOutline.esm.C9uHPmIj.js → ErrorOutline.esm.DWBqsdHn.js} +1 -1
  27. streamlit/static/static/js/{FileDownload.esm.D-YPxF3t.js → FileDownload.esm.D5koxJhf.js} +1 -1
  28. streamlit/static/static/js/{FileHelper.DQSH0zYW.js → FileHelper.kJKftfu4.js} +5 -5
  29. streamlit/static/static/js/{FormClearHelper.DQoXcOWo.js → FormClearHelper.CyGGPn10.js} +1 -1
  30. streamlit/static/static/js/{InputInstructions.C7VMyGT7.js → InputInstructions.oezYb8Lm.js} +1 -1
  31. streamlit/static/static/js/{Particles.BdQSRZde.js → Particles.BIpBmEwi.js} +1 -1
  32. streamlit/static/static/js/{ProgressBar.DNF_pWKr.js → ProgressBar.BRAS_FJc.js} +1 -1
  33. streamlit/static/static/js/{StreamlitSyntaxHighlighter.Cys9Bt18.js → StreamlitSyntaxHighlighter.c2qcx-xG.js} +1 -1
  34. streamlit/static/static/js/{TableChart.esm.B9SMgSK4.js → TableChart.esm.BuemQLVW.js} +1 -1
  35. streamlit/static/static/js/{Toolbar.BXfC9Z-W.js → Toolbar.mUe2Nmta.js} +1 -1
  36. streamlit/static/static/js/{WidgetLabelHelpIconInline.gkreC55g.js → WidgetLabelHelpIconInline.if89y2mu.js} +1 -1
  37. streamlit/static/static/js/{base-input.iB32RS3w.js → base-input.AJ4KxBTh.js} +1 -1
  38. streamlit/static/static/js/{checkbox.Bqz68SYq.js → checkbox.GY4JoJkM.js} +1 -1
  39. streamlit/static/static/js/{createDownloadLinkElement.YxVC9Qur.js → createDownloadLinkElement.BWJh90jh.js} +1 -1
  40. streamlit/static/static/js/{data-grid-overlay-editor.sBsdk5Xg.js → data-grid-overlay-editor.C9AINtRf.js} +1 -1
  41. streamlit/static/static/js/{downloader.Bzxqt3AW.js → downloader.BZY8OE4f.js} +1 -1
  42. streamlit/static/static/js/{embed.CDzakah3.js → embed.BjO7Ez0y.js} +1 -1
  43. streamlit/static/static/js/{es6.CxCc4bfn.js → es6.eb5oR8iN.js} +2 -2
  44. streamlit/static/static/js/{formatNumber.L8T7D36k.js → formatNumber.CCeQsvJQ.js} +1 -1
  45. streamlit/static/static/js/{iconPosition.C47DkA-1.js → iconPosition.D6eEnKvO.js} +1 -1
  46. streamlit/static/static/js/{iframeResizer.contentWindow.uEFLXEg3.js → iframeResizer.contentWindow.DTsWJRTo.js} +1 -1
  47. streamlit/static/static/js/{index.C6dhwBSe.js → index.B6l4FdUv.js} +1 -1
  48. streamlit/static/static/js/{index.BV_YgIHe.js → index.B7H2q9vo.js} +2 -2
  49. streamlit/static/static/js/{index.DjgdCvlK.js → index.BDdmrM58.js} +1 -1
  50. streamlit/static/static/js/{index.CAbQaUvi.js → index.BMbQnwRD.js} +1 -1
  51. streamlit/static/static/js/{index.CCLteRW6.js → index.BQ5MOzHu.js} +1 -1
  52. streamlit/static/static/js/{index.CzwJzgQs.js → index.BSgo_bkv.js} +1 -1
  53. streamlit/static/static/js/{index.B60AZFRh.js → index.BU5M9DsN.js} +1 -1
  54. streamlit/static/static/js/{index.BVTW_o-a.js → index.BYIxnU34.js} +1 -1
  55. streamlit/static/static/js/{index.DqhZqWYB.js → index.BZL2hIBz.js} +1 -1
  56. streamlit/static/static/js/{index.CAbafj7s.js → index.BZVrJlnq.js} +1 -1
  57. streamlit/static/static/js/{index.Ck0ZkOfK.js → index.Bd7QK46M.js} +1 -1
  58. streamlit/static/static/js/{index.BzdcdLDK.js → index.BfHA_i34.js} +1 -1
  59. streamlit/static/static/js/{index.kBgXO7Vv.js → index.Bgry-Ek_.js} +1 -1
  60. streamlit/static/static/js/{index.aZRhdEuX.js → index.Bi25zaXA.js} +1 -1
  61. streamlit/static/static/js/{index.QXukCzoh.js → index.Bj3M1xBC.js} +1 -1
  62. streamlit/static/static/js/{index.Bnwh8oZr.js → index.Bjrvlqx5.js} +6 -6
  63. streamlit/static/static/js/{index.h2N-W51I.js → index.BnOSeM5K.js} +1 -1
  64. streamlit/static/static/js/index.BoL6J1jK.js +2 -0
  65. streamlit/static/static/js/{index.BwvxzVOo.js → index.BoORyxOa.js} +1 -1
  66. streamlit/static/static/js/{index.Ba8L-ulI.js → index.Bpd7GPeH.js} +1 -1
  67. streamlit/static/static/js/{index.BG4RxMOI.js → index.BpeJX018.js} +1 -1
  68. streamlit/static/static/js/index.BriH7JVk.js +2 -0
  69. streamlit/static/static/js/{index.DO55kRo5.js → index.BwkeObMo.js} +1 -1
  70. streamlit/static/static/js/{index.C1d2QjTR.js → index.C0o85qmd.js} +1 -1
  71. streamlit/static/static/js/{index.D7L9gHlE.js → index.C7_wNJTH.js} +1 -1
  72. streamlit/static/static/js/{index.BwTkGOAy.js → index.CGZP_w9b.js} +1 -1
  73. streamlit/static/static/js/{index.CjBDVb1a.js → index.CGw52-0-.js} +1 -1
  74. streamlit/static/static/js/{index.D_cvtSlg.js → index.CUvtJj0a.js} +1 -1
  75. streamlit/static/static/js/{index.Dtbj_oyb.js → index.Cfx1ZHWt.js} +1 -1
  76. streamlit/static/static/js/{index.BvHsyiyy.js → index.Chl2kALe.js} +1 -1
  77. streamlit/static/static/js/index.Crlx_wdE.js +1 -0
  78. streamlit/static/static/js/{index.B_LfkwqU.js → index.D9A-8ebQ.js} +1 -1
  79. streamlit/static/static/js/{index.BHyzKS4e.js → index.DBIoNOen.js} +7 -7
  80. streamlit/static/static/js/{index.DgqmsDCJ.js → index.DMKTAe4F.js} +1 -1
  81. streamlit/static/static/js/{index.DEKnCsY-.js → index.DNrpqKVt.js} +1 -1
  82. streamlit/static/static/js/{index.BsrhCS7f.js → index.DSRvF_8e.js} +1 -1
  83. streamlit/static/static/js/{index.DN_oudQl.js → index.DhB1m_xG.js} +1 -1
  84. streamlit/static/static/js/{index.XJY9qZ6a.js → index.DiES30vM.js} +1 -1
  85. streamlit/static/static/js/{index.DHrByikW.js → index.DlJ4Y1xc.js} +1 -1
  86. streamlit/static/static/js/{index.C5-TpWis.js → index.J61yByUR.js} +1 -1
  87. streamlit/static/static/js/{index.iUHLeAvv.js → index.JxDTXE8N.js} +1 -1
  88. streamlit/static/static/js/{index.C1uZrWog.js → index.KGr28TP8.js} +1 -1
  89. streamlit/static/static/js/{index.BuJPJSD7.js → index.Pgm3rRpH.js} +1 -1
  90. streamlit/static/static/js/{index.fUsWkW8E.js → index.XzcYOc9I.js} +1 -1
  91. streamlit/static/static/js/{index.CQ713nQM.js → index.Z_u7ZS4h.js} +1 -1
  92. streamlit/static/static/js/{index.D-9VyyiA.js → index.hDg7x0Tp.js} +1 -1
  93. streamlit/static/static/js/{index.D2R3Co5f.js → index.k7p0hmaU.js} +1 -1
  94. streamlit/static/static/js/{index.Bh5BZaHG.js → index.syoxWolF.js} +1 -1
  95. streamlit/static/static/js/{index.kEL0HsUR.js → index.xZ651bTg.js} +1 -1
  96. streamlit/static/static/js/{input.BCHJn1Cw.js → input.C-PwAMG6.js} +1 -1
  97. streamlit/static/static/js/{main.23ZP6f1E.js → main.fMmyxXOf.js} +1 -1
  98. streamlit/static/static/js/{memory.D8f8Q4mO.js → memory.BG__eDEj.js} +1 -1
  99. streamlit/static/static/js/{number-overlay-editor.ZWvSpjJ5.js → number-overlay-editor.C0n-91sR.js} +1 -1
  100. streamlit/static/static/js/{pandasStylerUtils.DlZ2GBs_.js → pandasStylerUtils.BqWaUzvh.js} +1 -1
  101. streamlit/static/static/js/{sandbox.BH6D3KL9.js → sandbox.CHhc-txg.js} +1 -1
  102. streamlit/static/static/js/{sprintfjs.CtrdaGLQ.js → sprintfjs.BlFBKfMf.js} +1 -1
  103. streamlit/static/static/js/{styled-components.iD1HRMLc.js → styled-components.D5uOQqN2.js} +1 -1
  104. streamlit/static/static/js/{throttle.DR7d9vp_.js → throttle.DlZC3xNA.js} +1 -1
  105. streamlit/static/static/js/{timepicker.Bd34xjcG.js → timepicker._TfRUaDL.js} +1 -1
  106. streamlit/static/static/js/{toConsumableArray.BDTTq1c5.js → toConsumableArray.CZW4AmuW.js} +1 -1
  107. streamlit/static/static/js/uniqueId.CTTDAAaF.js +1 -0
  108. streamlit/static/static/js/{useBasicWidgetState.BXKaD8DQ.js → useBasicWidgetState.Bx27912z.js} +1 -1
  109. streamlit/static/static/js/{useIntlLocale.CysOvegI.js → useIntlLocale.DsOvysl7.js} +1 -1
  110. streamlit/static/static/js/{useTextInputAutoExpand.CVd5Hf2S.js → useTextInputAutoExpand.C9g8px1W.js} +1 -1
  111. streamlit/static/static/js/{useUpdateUiValue.CIUgfO8X.js → useUpdateUiValue.fF-Cntkp.js} +1 -1
  112. streamlit/static/static/js/{useWaveformController.CDLqlnLv.js → useWaveformController.C5EtFoJE.js} +1 -1
  113. streamlit/static/static/js/{withCalculatedWidth.Ce1Zblb2.js → withCalculatedWidth.CcvaXQb0.js} +1 -1
  114. streamlit/static/static/js/{withFullScreenWrapper.DBm7N75M.js → withFullScreenWrapper.DP61hzLF.js} +1 -1
  115. streamlit/testing/v1/element_tree.py +20 -15
  116. streamlit/web/server/routes.py +14 -0
  117. streamlit/web/server/server.py +4 -0
  118. {streamlit_nightly-1.53.2.dev20260203.dist-info → streamlit_nightly-1.54.1.dev20260204.dist-info}/METADATA +1 -1
  119. {streamlit_nightly-1.53.2.dev20260203.dist-info → streamlit_nightly-1.54.1.dev20260204.dist-info}/RECORD +122 -120
  120. streamlit/proto/Arrow_pb2.py +0 -34
  121. streamlit/static/static/js/index.B3zULhHv.js +0 -1
  122. streamlit/static/static/js/index.BrZtYm2A.js +0 -2
  123. streamlit/static/static/js/index.CcBYyLfq.js +0 -2
  124. streamlit/static/static/js/uniqueId.Bd_Iuzvc.js +0 -1
  125. {streamlit_nightly-1.53.2.dev20260203.dist-info → streamlit_nightly-1.54.1.dev20260204.dist-info}/WHEEL +0 -0
  126. {streamlit_nightly-1.53.2.dev20260203.dist-info → streamlit_nightly-1.54.1.dev20260204.dist-info}/entry_points.txt +0 -0
  127. {streamlit_nightly-1.53.2.dev20260203.dist-info → streamlit_nightly-1.54.1.dev20260204.dist-info}/top_level.txt +0 -0
@@ -15,7 +15,6 @@
15
15
  from __future__ import annotations
16
16
 
17
17
  from collections.abc import Callable, Sequence
18
- from dataclasses import dataclass, field
19
18
  from typing import (
20
19
  TYPE_CHECKING,
21
20
  Any,
@@ -27,6 +26,7 @@ from typing import (
27
26
  overload,
28
27
  )
29
28
 
29
+ from streamlit.dataframe_util import convert_anything_to_list
30
30
  from streamlit.elements.lib.form_utils import current_form_id
31
31
  from streamlit.elements.lib.layout_utils import (
32
32
  LayoutConfig,
@@ -34,9 +34,12 @@ from streamlit.elements.lib.layout_utils import (
34
34
  validate_width,
35
35
  )
36
36
  from streamlit.elements.lib.options_selector_utils import (
37
- check_and_convert_to_indices,
38
37
  convert_to_sequence_and_check_comparable,
39
38
  get_default_indices,
39
+ maybe_coerce_enum,
40
+ maybe_coerce_enum_sequence,
41
+ validate_and_sync_multiselect_value_with_options,
42
+ validate_and_sync_value_with_options,
40
43
  )
41
44
  from streamlit.elements.lib.policies import (
42
45
  check_widget_policies,
@@ -77,96 +80,150 @@ V = TypeVar("V")
77
80
  SelectionMode: TypeAlias = Literal["single", "multi"]
78
81
 
79
82
 
80
- @dataclass
81
- class _MultiSelectSerde(Generic[T]):
82
- """Only meant to be used internally for the button_group element.
83
+ class _SingleSelectButtonGroupSerde(Generic[T]):
84
+ """String-based serde for single-select ButtonGroup widgets.
83
85
 
84
- This serde is inspired by the MultiSelectSerde from multiselect.py. That serde has
85
- been updated since then to support the accept_new_options parameter, which is not
86
- required by the button_group element. If this changes again at some point,
87
- the two elements can share the same serde again.
86
+ Uses string-based values (formatted option strings) for robust handling
87
+ of dynamic option changes.
88
88
  """
89
89
 
90
90
  options: Sequence[T]
91
- default_value: list[int] = field(default_factory=list)
92
-
93
- def serialize(self, value: list[T]) -> list[int]:
94
- indices = check_and_convert_to_indices(self.options, value)
95
- return indices if indices is not None else []
96
-
97
- def deserialize(self, ui_value: list[int] | None) -> list[T]:
98
- current_value: list[int] = (
99
- ui_value if ui_value is not None else self.default_value
100
- )
101
- return [self.options[i] for i in current_value]
102
-
103
-
104
- class _SingleSelectSerde(Generic[T]):
105
- """Only meant to be used internally for the button_group element.
106
-
107
- Uses the ButtonGroup's _MultiSelectSerde under-the-hood, but accepts a single
108
- index value and deserializes to a single index value.
109
- This is because button_group can be single and multi select, but we use the same
110
- proto for both and, thus, map single values to a list of values and a receiving
111
- value wrapped in a list to a single value.
112
-
113
- When a default_value is provided is provided, the option corresponding to the
114
- index is serialized/deserialized.
115
- """
91
+ formatted_options: list[str]
92
+ formatted_option_to_option_index: dict[str, int]
93
+ default_option_index: int | None
94
+ format_func: Callable[[Any], str]
116
95
 
117
96
  def __init__(
118
97
  self,
119
- option_indices: Sequence[T],
120
- default_value: list[int] | None = None,
98
+ options: Sequence[T],
99
+ *,
100
+ formatted_options: list[str],
101
+ formatted_option_to_option_index: dict[str, int],
102
+ default_option_index: int | None = None,
103
+ format_func: Callable[[Any], str] = str,
121
104
  ) -> None:
122
- # see docstring about why we use MultiSelectSerde here
123
- self.multiselect_serde: _MultiSelectSerde[T] = _MultiSelectSerde(
124
- option_indices, default_value if default_value is not None else []
125
- )
126
-
127
- def serialize(self, value: T | None) -> list[int]:
128
- _value = [value] if value is not None else []
129
- return self.multiselect_serde.serialize(_value)
105
+ self.options = options
106
+ self.formatted_options = formatted_options
107
+ self.formatted_option_to_option_index = formatted_option_to_option_index
108
+ self.default_option_index = default_option_index
109
+ self.format_func = format_func
110
+
111
+ def serialize(self, v: T | str | None) -> list[str]:
112
+ """Serialize single-select value to a list of strings for wire format."""
113
+ if v is None:
114
+ return []
115
+ if len(self.options) == 0:
116
+ return []
117
+
118
+ # First, try to find the option by value in the options list
119
+ for index, opt in enumerate(self.options):
120
+ if opt == v:
121
+ return [self.formatted_options[index]]
122
+
123
+ # If not found by direct comparison, try by formatted string
124
+ try:
125
+ formatted_value = self.format_func(v)
126
+ except Exception:
127
+ return [str(v)]
128
+
129
+ return [formatted_value]
130
+
131
+ def deserialize(self, ui_value: list[str] | None) -> T | str | None:
132
+ """Deserialize from a list of strings to a single value."""
133
+ if len(self.options) == 0:
134
+ return None
130
135
 
131
- def deserialize(self, ui_value: list[int] | None) -> T | None:
132
- deserialized = self.multiselect_serde.deserialize(ui_value)
136
+ # None means initial state - use default if available
137
+ if ui_value is None:
138
+ if self.default_option_index is not None:
139
+ return self.options[self.default_option_index]
140
+ return None
133
141
 
134
- if len(deserialized) == 0:
142
+ # Empty list means explicit deselection by user - return None
143
+ if len(ui_value) == 0:
135
144
  return None
136
145
 
137
- return deserialized[0]
146
+ string_value = ui_value[0]
147
+
148
+ # Look up the option index by formatted string
149
+ option_index = self.formatted_option_to_option_index.get(string_value)
150
+ if option_index is not None:
151
+ return self.options[option_index]
138
152
 
153
+ # Value not found in options - return as-is
154
+ return string_value
139
155
 
140
- class ButtonGroupSerde(Generic[T]):
141
- """A serde that can handle both single and multi select options.
142
156
 
143
- It uses the same proto to wire the data, so that we can send and receive
144
- single values via a list. We have different serdes for both cases though so
145
- that when setting / getting the value via session_state, it is mapped correctly.
146
- So for single select, the value will be a single value and for multi select, it will
147
- be a list of values.
157
+ class _MultiSelectButtonGroupSerde(Generic[T]):
158
+ """String-based serde for multi-select ButtonGroup widgets.
159
+
160
+ Uses string-based values (formatted option strings) for robust handling
161
+ of dynamic option changes.
148
162
  """
149
163
 
164
+ options: Sequence[T]
165
+ formatted_options: list[str]
166
+ formatted_option_to_option_index: dict[str, int]
167
+ default_option_indices: list[int]
168
+ format_func: Callable[[Any], str]
169
+
150
170
  def __init__(
151
171
  self,
152
172
  options: Sequence[T],
153
- default_values: list[int],
154
- type: Literal["single", "multi"],
173
+ *,
174
+ formatted_options: list[str],
175
+ formatted_option_to_option_index: dict[str, int],
176
+ default_option_indices: list[int] | None = None,
177
+ format_func: Callable[[Any], str] = str,
155
178
  ) -> None:
156
179
  self.options = options
157
- self.default_values = default_values
158
- self.type = type
159
- self.serde: _SingleSelectSerde[T] | _MultiSelectSerde[T] = (
160
- _SingleSelectSerde(options, default_value=default_values)
161
- if type == "single"
162
- else _MultiSelectSerde(options, default_values)
163
- )
164
-
165
- def serialize(self, value: T | list[T] | None) -> list[int]:
166
- return self.serde.serialize(cast("Any", value))
167
-
168
- def deserialize(self, ui_value: list[int] | None) -> list[T] | T | None:
169
- return self.serde.deserialize(ui_value)
180
+ self.formatted_options = formatted_options
181
+ self.formatted_option_to_option_index = formatted_option_to_option_index
182
+ self.default_option_indices = default_option_indices or []
183
+ self.format_func = format_func
184
+
185
+ def serialize(self, value: list[T | str] | list[T] | None) -> list[str]:
186
+ """Serialize multi-select values to list of strings for wire format."""
187
+ if value is None:
188
+ return []
189
+ converted_value = convert_anything_to_list(value)
190
+ values: list[str] = []
191
+ for v in converted_value:
192
+ # First, try to find the option by value in the options list
193
+ found = False
194
+ for index, opt in enumerate(self.options):
195
+ if opt == v:
196
+ values.append(self.formatted_options[index])
197
+ found = True
198
+ break
199
+
200
+ if found:
201
+ continue
202
+
203
+ # If not found by direct comparison, try by formatted string
204
+ try:
205
+ formatted_value = self.format_func(v)
206
+ except Exception:
207
+ values.append(str(v))
208
+ continue
209
+
210
+ values.append(formatted_value)
211
+ return values
212
+
213
+ def deserialize(self, ui_value: list[str] | None) -> list[T | str] | list[T]:
214
+ """Deserialize from list of strings to list of values."""
215
+ if ui_value is None:
216
+ return [self.options[i] for i in self.default_option_indices]
217
+
218
+ values: list[T | str] = []
219
+ for v in ui_value:
220
+ option_index = self.formatted_option_to_option_index.get(v)
221
+ if option_index is not None:
222
+ values.append(self.options[option_index])
223
+ else:
224
+ # Value not found in options - append as-is
225
+ values.append(v)
226
+ return values
170
227
 
171
228
 
172
229
  def _build_proto(
@@ -695,11 +752,14 @@ class ButtonGroupMixin:
695
752
  ) -> list[V] | V | None:
696
753
  maybe_raise_label_warnings(label, label_visibility)
697
754
 
755
+ # Use str as default format_func
756
+ actual_format_func: Callable[[Any], str] = format_func or str
757
+
698
758
  def _transformed_format_func(option: V) -> ButtonGroupProto.Option:
699
759
  """If option starts with a material icon or an emoji, we extract it to send
700
760
  it parsed to the frontend.
701
761
  """
702
- transformed = format_func(option) if format_func else str(option)
762
+ transformed = actual_format_func(option)
703
763
  transformed_parts = transformed.split(" ")
704
764
  icon: str | None = None
705
765
  if len(transformed_parts) > 0:
@@ -725,11 +785,42 @@ class ButtonGroupMixin:
725
785
  indexable_options = convert_to_sequence_and_check_comparable(options)
726
786
  default_values = get_default_indices(indexable_options, default)
727
787
 
728
- serde: ButtonGroupSerde[V] = ButtonGroupSerde[V](
729
- indexable_options, default_values, selection_mode
730
- )
788
+ # Create string-based mappings for the serde
789
+ formatted_options: list[str] = []
790
+ formatted_option_to_option_index: dict[str, int] = {}
791
+ for index, option in enumerate(indexable_options):
792
+ formatted = actual_format_func(option)
793
+ formatted_options.append(formatted)
794
+ # If formatted labels are duplicated, the last one wins. We keep this
795
+ # behavior to mirror radio/selectbox/multiselect.
796
+ formatted_option_to_option_index[formatted] = index
797
+
798
+ # Create appropriate serde based on selection mode
799
+ serializer: WidgetSerializer[Any]
800
+ deserializer: WidgetDeserializer[Any]
801
+ if selection_mode == "multi":
802
+ multi_serde = _MultiSelectButtonGroupSerde[V](
803
+ indexable_options,
804
+ formatted_options=formatted_options,
805
+ formatted_option_to_option_index=formatted_option_to_option_index,
806
+ default_option_indices=default_values,
807
+ format_func=actual_format_func,
808
+ )
809
+ serializer = multi_serde.serialize
810
+ deserializer = multi_serde.deserialize
811
+ else:
812
+ single_serde = _SingleSelectButtonGroupSerde[V](
813
+ indexable_options,
814
+ formatted_options=formatted_options,
815
+ formatted_option_to_option_index=formatted_option_to_option_index,
816
+ default_option_index=default_values[0] if default_values else None,
817
+ format_func=actual_format_func,
818
+ )
819
+ serializer = single_serde.serialize
820
+ deserializer = single_serde.deserialize
731
821
 
732
- res = self._button_group(
822
+ # Single call to _button_group with the appropriate serde
823
+ result: RegisterWidgetResult[Any] = self._button_group(
733
824
  indexable_options,
734
825
  default=default_values,
735
826
  selection_mode=selection_mode,
@@ -738,20 +829,28 @@ class ButtonGroupMixin:
738
829
  key=key,
739
830
  help=help,
740
831
  style=style,
741
- serializer=serde.serialize,
742
- deserializer=serde.deserialize,
832
+ serializer=serializer,
833
+ deserializer=deserializer,
743
834
  on_change=on_change,
744
835
  args=args,
745
836
  kwargs=kwargs,
746
837
  label=label,
747
838
  label_visibility=label_visibility,
748
839
  width=width,
840
+ options_format_func=actual_format_func,
749
841
  )
750
842
 
843
+ # Handle return type based on selection mode
751
844
  if selection_mode == "multi":
752
- return res.value
845
+ multi_res = cast("RegisterWidgetResult[list[V] | list[V | str]]", result)
846
+ multi_res = maybe_coerce_enum_sequence(
847
+ multi_res, options, indexable_options
848
+ )
849
+ return cast("list[V]", multi_res.value)
753
850
 
754
- return res.value
851
+ single_res = cast("RegisterWidgetResult[V | str | None]", result)
852
+ single_res = maybe_coerce_enum(single_res, options, indexable_options)
853
+ return cast("V | None", single_res.value)
755
854
 
756
855
  def _button_group(
757
856
  self,
@@ -772,6 +871,7 @@ class ButtonGroupMixin:
772
871
  label_visibility: LabelVisibility = "visible",
773
872
  help: str | None = None,
774
873
  width: Width = "content",
874
+ options_format_func: Callable[[Any], str] | None = None,
775
875
  ) -> RegisterWidgetResult[T]:
776
876
  _maybe_raise_selection_mode_warning(selection_mode)
777
877
 
@@ -826,10 +926,7 @@ class ButtonGroupMixin:
826
926
  element_id = compute_and_register_element_id(
827
927
  style,
828
928
  user_key=key,
829
- # Treat the provided key as the main identity for segmented_control and pills,
830
- # and only include kwargs that can invalidate the current selection.
831
- # We whitelist the formatted options and the click mode (single vs multi).
832
- key_as_main_identity={"options", "click_mode"},
929
+ key_as_main_identity={"click_mode"},
833
930
  dg=self.dg,
834
931
  options=formatted_options,
835
932
  default=default,
@@ -861,11 +958,40 @@ class ButtonGroupMixin:
861
958
  deserializer=deserializer,
862
959
  serializer=serializer,
863
960
  ctx=ctx,
864
- value_type="int_array_value",
961
+ value_type="string_array_value",
865
962
  )
866
963
 
867
- if widget_state.value_changed:
868
- proto.value[:] = serializer(widget_state.value)
964
+ # Validate and sync value with options for pills/segmented_control
965
+ value_needs_reset = False
966
+ current_value: T | list[T] | list[T | str] | None = widget_state.value
967
+ if options_format_func is not None:
968
+ if selection_mode == "single":
969
+ # Single select: validate and possibly reset to default
970
+ default_index = default[0] if default else None
971
+ current_value, value_needs_reset = validate_and_sync_value_with_options(
972
+ cast("T | None", widget_state.value),
973
+ indexable_options,
974
+ default_index,
975
+ key,
976
+ options_format_func,
977
+ )
978
+ else:
979
+ # Multi select: filter out invalid values
980
+ current_value, value_needs_reset = (
981
+ validate_and_sync_multiselect_value_with_options(
982
+ cast("list[T] | list[T | str]", widget_state.value),
983
+ indexable_options,
984
+ key,
985
+ options_format_func,
986
+ )
987
+ )
988
+
989
+ if value_needs_reset or widget_state.value_changed:
990
+ # Always use string-based raw_values field
991
+ value_for_serialization = (
992
+ current_value if value_needs_reset else widget_state.value
993
+ )
994
+ proto.raw_values[:] = serializer(cast("T", value_for_serialization))
869
995
  proto.set_value = True
870
996
 
871
997
  if ctx:
@@ -873,6 +999,15 @@ class ButtonGroupMixin:
873
999
 
874
1000
  self.dg._enqueue("button_group", proto, layout_config=layout_config)
875
1001
 
1002
+ # Return widget_state with possibly updated value
1003
+ if value_needs_reset:
1004
+ from streamlit.runtime.state.common import RegisterWidgetResult
1005
+
1006
+ return RegisterWidgetResult(
1007
+ cast("T", current_value),
1008
+ widget_state.value_changed or value_needs_reset,
1009
+ )
1010
+
876
1011
  return widget_state
877
1012
 
878
1013
  @property
@@ -63,7 +63,7 @@ from streamlit.elements.lib.pandas_styler_utils import marshall_styler
63
63
  from streamlit.elements.lib.policies import check_widget_policies
64
64
  from streamlit.elements.lib.utils import Key, compute_and_register_element_id, to_key
65
65
  from streamlit.errors import StreamlitAPIException
66
- from streamlit.proto.Arrow_pb2 import Arrow as ArrowProto
66
+ from streamlit.proto.Dataframe_pb2 import Dataframe as DataframeProto
67
67
  from streamlit.runtime.metrics_util import gather_metrics
68
68
  from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
69
69
  from streamlit.runtime.state import (
@@ -1093,7 +1093,7 @@ class DataEditorMixin:
1093
1093
  placeholder=placeholder,
1094
1094
  )
1095
1095
 
1096
- proto = ArrowProto()
1096
+ proto = DataframeProto()
1097
1097
  proto.id = element_id
1098
1098
 
1099
1099
  if row_height:
@@ -1110,13 +1110,13 @@ class DataEditorMixin:
1110
1110
  proto.disabled = disabled is True
1111
1111
 
1112
1112
  if num_rows == "dynamic":
1113
- proto.editing_mode = ArrowProto.EditingMode.DYNAMIC
1113
+ proto.editing_mode = DataframeProto.EditingMode.DYNAMIC
1114
1114
  elif num_rows == "add":
1115
- proto.editing_mode = ArrowProto.EditingMode.ADD_ONLY
1115
+ proto.editing_mode = DataframeProto.EditingMode.ADD_ONLY
1116
1116
  elif num_rows == "delete":
1117
- proto.editing_mode = ArrowProto.EditingMode.DELETE_ONLY
1117
+ proto.editing_mode = DataframeProto.EditingMode.DELETE_ONLY
1118
1118
  else:
1119
- proto.editing_mode = ArrowProto.EditingMode.FIXED
1119
+ proto.editing_mode = DataframeProto.EditingMode.FIXED
1120
1120
 
1121
1121
  proto.form_id = current_form_id(self.dg)
1122
1122
 
@@ -1132,9 +1132,9 @@ class DataEditorMixin:
1132
1132
  # rendering in the data editor.
1133
1133
  styler_uuid = calc_md5(key or self.dg._get_delta_path_str())[:10]
1134
1134
  data.set_uuid(styler_uuid) # ty: ignore[call-non-callable, possibly-missing-attribute]
1135
- marshall_styler(proto, data, styler_uuid)
1135
+ marshall_styler(proto.arrow_data, data, styler_uuid)
1136
1136
 
1137
- proto.data = arrow_bytes
1137
+ proto.arrow_data.data = arrow_bytes
1138
1138
 
1139
1139
  marshall_column_config(proto, column_config_mapping)
1140
1140
 
@@ -1159,7 +1159,7 @@ class DataEditorMixin:
1159
1159
  )
1160
1160
 
1161
1161
  _apply_dataframe_edits(data_df, widget_state.value, dataframe_schema)
1162
- self.dg._enqueue("arrow_data_frame", proto, layout_config=layout_config)
1162
+ self.dg._enqueue("dataframe", proto, layout_config=layout_config)
1163
1163
  return dataframe_util.convert_pandas_df_to_data_format(data_df, data_format)
1164
1164
 
1165
1165
  @property
@@ -108,13 +108,17 @@ class RadioSerde(Generic[T]):
108
108
  formatted_value = self.format_func(v)
109
109
  except Exception:
110
110
  # format_func failed (e.g., v is a string but format_func expects
111
- # an object with specific attributes). Treat v as a raw string.
112
- return cast("str", v)
111
+ # an object with specific attributes). Use str(v) to ensure we return
112
+ # a proper string, not the original object. This handles both cases:
113
+ # - v is already a string -> str(v) returns it unchanged
114
+ # - v is a custom object -> str(v) gives its string representation
115
+ return str(v)
113
116
 
114
117
  if formatted_value in self.formatted_option_to_option_index:
115
118
  return formatted_value
116
- # Value not found in options - return as raw string
117
- return cast("str", v)
119
+ # Value not found in options - return the formatted string (not the original
120
+ # object) to maintain type consistency since serialize() must return str|None
121
+ return formatted_value
118
122
 
119
123
  def deserialize(self, ui_value: str | None) -> T | str | None:
120
124
  # If no options, there's no valid value - return None
@@ -26,7 +26,7 @@ def data_frame_demo() -> None:
26
26
  def get_un_data() -> pd.DataFrame:
27
27
  aws_bucket_url = "https://streamlit-demo-data.s3-us-west-2.amazonaws.com"
28
28
  df = pd.read_csv(aws_bucket_url + "/agri.csv.gz")
29
- return df.set_index("Region")
29
+ return df.set_index("Region") # type: ignore[no-any-return, unused-ignore]
30
30
 
31
31
  try:
32
32
  df = get_un_data()
@@ -14,13 +14,15 @@ _sym_db = _symbol_database.Default()
14
14
 
15
15
 
16
16
 
17
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fstreamlit/proto/ArrowData.proto\"\x19\n\tArrowData\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x62\x06proto3')
17
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1fstreamlit/proto/ArrowData.proto\"\x99\x01\n\tArrowData\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\'\n\x06styler\x18\x02 \x01(\x0b\x32\x17.ArrowData.PandasStyler\x1aU\n\x0cPandasStyler\x12\x0c\n\x04uuid\x18\x01 \x01(\t\x12\x0f\n\x07\x63\x61ption\x18\x02 \x01(\t\x12\x0e\n\x06styles\x18\x03 \x01(\t\x12\x16\n\x0e\x64isplay_values\x18\x04 \x01(\x0c\x62\x06proto3')
18
18
 
19
19
  _globals = globals()
20
20
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
21
21
  _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'streamlit.proto.ArrowData_pb2', _globals)
22
22
  if not _descriptor._USE_C_DESCRIPTORS:
23
23
  DESCRIPTOR._loaded_options = None
24
- _globals['_ARROWDATA']._serialized_start=35
25
- _globals['_ARROWDATA']._serialized_end=60
24
+ _globals['_ARROWDATA']._serialized_start=36
25
+ _globals['_ARROWDATA']._serialized_end=189
26
+ _globals['_ARROWDATA_PANDASSTYLER']._serialized_start=104
27
+ _globals['_ARROWDATA_PANDASSTYLER']._serialized_end=189
26
28
  # @@protoc_insertion_point(module_scope)
@@ -32,22 +32,62 @@ DESCRIPTOR: _descriptor.FileDescriptor
32
32
 
33
33
  @_typing.final
34
34
  class ArrowData(_message.Message):
35
- """Data-only Arrow buffer. This intentionally excludes any UI- or element-
36
- specific configuration that exists in Arrow.proto. Use this message when
37
- only the serialized Arrow data is required.
35
+ """Arrow data buffer with optional styling. Use this message for serialized
36
+ Arrow data, with optional pandas styling for table-like displays.
38
37
  """
39
38
 
40
39
  DESCRIPTOR: _descriptor.Descriptor
41
40
 
41
+ @_typing.final
42
+ class PandasStyler(_message.Message):
43
+ """Pandas Styler information for formatting and styling table displays."""
44
+
45
+ DESCRIPTOR: _descriptor.Descriptor
46
+
47
+ UUID_FIELD_NUMBER: _builtins.int
48
+ CAPTION_FIELD_NUMBER: _builtins.int
49
+ STYLES_FIELD_NUMBER: _builtins.int
50
+ DISPLAY_VALUES_FIELD_NUMBER: _builtins.int
51
+ uuid: _builtins.str
52
+ """The Styler's source UUID (if the user provided one), or the path-based
53
+ hash that we generate (if no source UUID was provided).
54
+ """
55
+ caption: _builtins.str
56
+ """The table's caption."""
57
+ styles: _builtins.str
58
+ """`styles` contains the CSS for the entire source table."""
59
+ display_values: _builtins.bytes
60
+ """display_values is another Arrow/Table: a copy of the source table, but
61
+ with all the display values formatted to the user-specified rules.
62
+ """
63
+ def __init__(
64
+ self,
65
+ *,
66
+ uuid: _builtins.str = ...,
67
+ caption: _builtins.str = ...,
68
+ styles: _builtins.str = ...,
69
+ display_values: _builtins.bytes = ...,
70
+ ) -> None: ...
71
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["caption", b"caption", "display_values", b"display_values", "styles", b"styles", "uuid", b"uuid"] # noqa: Y015
72
+ def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
73
+
42
74
  DATA_FIELD_NUMBER: _builtins.int
75
+ STYLER_FIELD_NUMBER: _builtins.int
43
76
  data: _builtins.bytes
44
77
  """The serialized Apache Arrow buffer (IPC stream or file format bytes)"""
78
+ @_builtins.property
79
+ def styler(self) -> Global___ArrowData.PandasStyler:
80
+ """Optional pandas styler information (used by table-like displays)"""
81
+
45
82
  def __init__(
46
83
  self,
47
84
  *,
48
85
  data: _builtins.bytes = ...,
86
+ styler: Global___ArrowData.PandasStyler | None = ...,
49
87
  ) -> None: ...
50
- _ClearFieldArgType: _TypeAlias = _typing.Literal["data", b"data"] # noqa: Y015
88
+ _HasFieldArgType: _TypeAlias = _typing.Literal["styler", b"styler"] # noqa: Y015
89
+ def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
90
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["data", b"data", "styler", b"styler"] # noqa: Y015
51
91
  def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
52
92
 
53
93
  Global___ArrowData: _TypeAlias = ArrowData # noqa: Y015
@@ -12,16 +12,16 @@ from google.protobuf.internal import builder as _builder
12
12
  _sym_db = _symbol_database.Default()
13
13
 
14
14
 
15
- from streamlit.proto import Arrow_pb2 as streamlit_dot_proto_dot_Arrow__pb2
15
+ from streamlit.proto import ArrowData_pb2 as streamlit_dot_proto_dot_ArrowData__pb2
16
16
 
17
17
 
18
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\'streamlit/proto/ArrowNamedDataSet.proto\x1a\x1bstreamlit/proto/Arrow.proto\"I\n\x11\x41rrowNamedDataSet\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x10\n\x08has_name\x18\x03 \x01(\x08\x12\x14\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x06.Arrowb\x06proto3')
18
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\'streamlit/proto/ArrowNamedDataSet.proto\x1a\x1fstreamlit/proto/ArrowData.proto\"M\n\x11\x41rrowNamedDataSet\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x10\n\x08has_name\x18\x03 \x01(\x08\x12\x18\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\n.ArrowDatab\x06proto3')
19
19
 
20
20
  _globals = globals()
21
21
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
22
22
  _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'streamlit.proto.ArrowNamedDataSet_pb2', _globals)
23
23
  if not _descriptor._USE_C_DESCRIPTORS:
24
24
  DESCRIPTOR._loaded_options = None
25
- _globals['_ARROWNAMEDDATASET']._serialized_start=72
26
- _globals['_ARROWNAMEDDATASET']._serialized_end=145
25
+ _globals['_ARROWNAMEDDATASET']._serialized_start=76
26
+ _globals['_ARROWNAMEDDATASET']._serialized_end=153
27
27
  # @@protoc_insertion_point(module_scope)
@@ -19,7 +19,7 @@ limitations under the License.
19
19
 
20
20
  from google.protobuf import descriptor as _descriptor
21
21
  from google.protobuf import message as _message
22
- from streamlit.proto import Arrow_pb2 as _Arrow_pb2
22
+ from streamlit.proto import ArrowData_pb2 as _ArrowData_pb2
23
23
  import builtins as _builtins
24
24
  import sys
25
25
  import typing as _typing
@@ -47,7 +47,7 @@ class ArrowNamedDataSet(_message.Message):
47
47
  around proto3 not having a way to check whether something was set.
48
48
  """
49
49
  @_builtins.property
50
- def data(self) -> _Arrow_pb2.Arrow:
50
+ def data(self) -> _ArrowData_pb2.ArrowData:
51
51
  """The data itself."""
52
52
 
53
53
  def __init__(
@@ -55,7 +55,7 @@ class ArrowNamedDataSet(_message.Message):
55
55
  *,
56
56
  name: _builtins.str = ...,
57
57
  has_name: _builtins.bool = ...,
58
- data: _Arrow_pb2.Arrow | None = ...,
58
+ data: _ArrowData_pb2.ArrowData | None = ...,
59
59
  ) -> None: ...
60
60
  _HasFieldArgType: _TypeAlias = _typing.Literal["data", b"data"] # noqa: Y015
61
61
  def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...