streamlit-nightly 1.53.2.dev20260202__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.
- streamlit/commands/logo.py +6 -10
- streamlit/components/v2/component_path_utils.py +17 -29
- streamlit/config.py +4 -2
- streamlit/elements/arrow.py +66 -26
- streamlit/elements/lib/built_in_chart_utils.py +1 -1
- streamlit/elements/lib/column_config_utils.py +4 -4
- streamlit/elements/lib/options_selector_utils.py +4 -0
- streamlit/elements/lib/pandas_styler_utils.py +22 -22
- streamlit/elements/widgets/button_group.py +221 -86
- streamlit/elements/widgets/data_editor.py +9 -9
- streamlit/elements/widgets/radio.py +8 -4
- streamlit/hello/dataframe_demo.py +1 -1
- streamlit/path_security.py +98 -0
- streamlit/proto/ArrowData_pb2.py +5 -3
- streamlit/proto/ArrowData_pb2.pyi +44 -4
- streamlit/proto/ArrowNamedDataSet_pb2.py +4 -4
- streamlit/proto/ArrowNamedDataSet_pb2.pyi +3 -3
- streamlit/proto/ArrowVegaLiteChart_pb2.py +4 -4
- streamlit/proto/ArrowVegaLiteChart_pb2.pyi +3 -3
- streamlit/proto/ButtonGroup_pb2.py +8 -8
- streamlit/proto/ButtonGroup_pb2.pyi +9 -7
- streamlit/proto/Dataframe_pb2.py +31 -0
- streamlit/proto/{Arrow_pb2.pyi → Dataframe_pb2.pyi} +39 -102
- streamlit/proto/Element_pb2.py +5 -4
- streamlit/proto/Element_pb2.pyi +11 -10
- streamlit/proto/Table_pb2.py +29 -0
- streamlit/proto/Table_pb2.pyi +83 -0
- streamlit/runtime/app_session.py +9 -1
- streamlit/static/index.html +1 -1
- streamlit/static/manifest.json +307 -307
- streamlit/static/static/js/{ErrorOutline.esm.CLuz0rSD.js → ErrorOutline.esm.DWBqsdHn.js} +1 -1
- streamlit/static/static/js/{FileDownload.esm.CO68LcnZ.js → FileDownload.esm.D5koxJhf.js} +1 -1
- streamlit/static/static/js/{FileHelper.DAXgY6Ug.js → FileHelper.kJKftfu4.js} +5 -5
- streamlit/static/static/js/{FormClearHelper.zxJ53Nym.js → FormClearHelper.CyGGPn10.js} +1 -1
- streamlit/static/static/js/{InputInstructions.CCbg8esE.js → InputInstructions.oezYb8Lm.js} +1 -1
- streamlit/static/static/js/{Particles.BJw0A-zv.js → Particles.BIpBmEwi.js} +1 -1
- streamlit/static/static/js/{ProgressBar.BrrHeAEh.js → ProgressBar.BRAS_FJc.js} +1 -1
- streamlit/static/static/js/{StreamlitSyntaxHighlighter.DvPLy3zk.js → StreamlitSyntaxHighlighter.c2qcx-xG.js} +2 -2
- streamlit/static/static/js/{TableChart.esm.BawvAi5p.js → TableChart.esm.BuemQLVW.js} +1 -1
- streamlit/static/static/js/{Toolbar.CbpscbNb.js → Toolbar.mUe2Nmta.js} +1 -1
- streamlit/static/static/js/{WidgetLabelHelpIconInline.DA4S2HFP.js → WidgetLabelHelpIconInline.if89y2mu.js} +1 -1
- streamlit/static/static/js/{base-input.BKKl2eBF.js → base-input.AJ4KxBTh.js} +4 -4
- streamlit/static/static/js/{checkbox.CB43AKV4.js → checkbox.GY4JoJkM.js} +1 -1
- streamlit/static/static/js/{createDownloadLinkElement.jVwF96ey.js → createDownloadLinkElement.BWJh90jh.js} +1 -1
- streamlit/static/static/js/{data-grid-overlay-editor.CxQizSv7.js → data-grid-overlay-editor.C9AINtRf.js} +1 -1
- streamlit/static/static/js/{downloader.CKlIgsy5.js → downloader.BZY8OE4f.js} +1 -1
- streamlit/static/static/js/{embed.BhRb_2n8.js → embed.BjO7Ez0y.js} +1 -1
- streamlit/static/static/js/{es6.DPyfPmWm.js → es6.eb5oR8iN.js} +2 -2
- streamlit/static/static/js/{formatNumber.DtfMnnPx.js → formatNumber.CCeQsvJQ.js} +1 -1
- streamlit/static/static/js/{iconPosition.DkZAlu_k.js → iconPosition.D6eEnKvO.js} +1 -1
- streamlit/static/static/js/{iframeResizer.contentWindow.l5sQWLra.js → iframeResizer.contentWindow.DTsWJRTo.js} +1 -1
- streamlit/static/static/js/{index.rvbQETlC.js → index.B6l4FdUv.js} +1 -1
- streamlit/static/static/js/{index.BUlT_mOL.js → index.B7H2q9vo.js} +2 -2
- streamlit/static/static/js/{index.CsIENsKH.js → index.BDdmrM58.js} +1 -1
- streamlit/static/static/js/{index.e6Ry6-Ft.js → index.BMbQnwRD.js} +1 -1
- streamlit/static/static/js/{index.CL_icBXS.js → index.BQ5MOzHu.js} +1 -1
- streamlit/static/static/js/{index.Ccye_uLl.js → index.BSgo_bkv.js} +1 -1
- streamlit/static/static/js/{index.LjqoQCm5.js → index.BU5M9DsN.js} +1 -1
- streamlit/static/static/js/{index.Bc_FQ4Wb.js → index.BYIxnU34.js} +1 -1
- streamlit/static/static/js/{index.y0h42OXL.js → index.BZL2hIBz.js} +1 -1
- streamlit/static/static/js/{index.Dac9Jib-.js → index.BZVrJlnq.js} +1 -1
- streamlit/static/static/js/{index.BLD3tJ2C.js → index.Bd7QK46M.js} +1 -1
- streamlit/static/static/js/{index.Ck64OQhV.js → index.BfHA_i34.js} +1 -1
- streamlit/static/static/js/{index.BpCj2-sQ.js → index.Bgry-Ek_.js} +1 -1
- streamlit/static/static/js/{index.D83azq2w.js → index.Bi25zaXA.js} +1 -1
- streamlit/static/static/js/{index.Cwg8nWw5.js → index.Bj3M1xBC.js} +1 -1
- streamlit/static/static/js/{index.DmWUXdjc.js → index.Bjrvlqx5.js} +16 -16
- streamlit/static/static/js/index.BnOSeM5K.js +11 -0
- streamlit/static/static/js/index.BoL6J1jK.js +2 -0
- streamlit/static/static/js/{index.yHLCvUGh.js → index.BoORyxOa.js} +1 -1
- streamlit/static/static/js/{index.DiBeetOH.js → index.Bpd7GPeH.js} +1 -1
- streamlit/static/static/js/{index.BzO83wKm.js → index.BpeJX018.js} +1 -1
- streamlit/static/static/js/{index.C_19KWNs.js → index.BriH7JVk.js} +1 -1
- streamlit/static/static/js/{index.DjuMEZ6m.js → index.BwkeObMo.js} +1 -1
- streamlit/static/static/js/{index.CiS4giQ2.js → index.C0o85qmd.js} +1 -1
- streamlit/static/static/js/{index.L9pOjBEn.js → index.C7_wNJTH.js} +2 -2
- streamlit/static/static/js/{index.DzdPUxsx.js → index.CGZP_w9b.js} +1 -1
- streamlit/static/static/js/{index.DKoJr0Se.js → index.CGw52-0-.js} +1 -1
- streamlit/static/static/js/{index.6J1N4is3.js → index.CUvtJj0a.js} +1 -1
- streamlit/static/static/js/{index.CBbYMKZp.js → index.Cfx1ZHWt.js} +1 -1
- streamlit/static/static/js/{index.BAK0CG1Q.js → index.Chl2kALe.js} +1 -1
- streamlit/static/static/js/index.Crlx_wdE.js +1 -0
- streamlit/static/static/js/{index.WIMtx3m0.js → index.D9A-8ebQ.js} +1 -1
- streamlit/static/static/js/{index.y5HxPwg9.js → index.DBIoNOen.js} +21 -21
- streamlit/static/static/js/{index.AZ9T4EqJ.js → index.DMKTAe4F.js} +1 -1
- streamlit/static/static/js/{index.YutgmD9x.js → index.DNrpqKVt.js} +2 -2
- streamlit/static/static/js/{index.Y9wPTZIf.js → index.DSRvF_8e.js} +1 -1
- streamlit/static/static/js/{index.DZOGT9vX.js → index.DhB1m_xG.js} +1 -1
- streamlit/static/static/js/{index.DR6V0uBJ.js → index.DiES30vM.js} +1 -1
- streamlit/static/static/js/{index.CmbqbRMZ.js → index.DlJ4Y1xc.js} +1 -1
- streamlit/static/static/js/{index.FioS1Y9m.js → index.J61yByUR.js} +1 -1
- streamlit/static/static/js/{index.hnu9U-5g.js → index.JxDTXE8N.js} +1 -1
- streamlit/static/static/js/{index.CaU3Uv_L.js → index.KGr28TP8.js} +1 -1
- streamlit/static/static/js/{index.465nmxtO.js → index.Pgm3rRpH.js} +1 -1
- streamlit/static/static/js/{index.DpSc4e1j.js → index.XzcYOc9I.js} +1 -1
- streamlit/static/static/js/{index.OizPL4jg.js → index.Z_u7ZS4h.js} +1 -1
- streamlit/static/static/js/{index.CL7it7tU.js → index.hDg7x0Tp.js} +1 -1
- streamlit/static/static/js/{index.b_f-McZ5.js → index.k7p0hmaU.js} +1 -1
- streamlit/static/static/js/{index.CsEZTo6L.js → index.syoxWolF.js} +1 -1
- streamlit/static/static/js/{index.Q2t_iBn0.js → index.xZ651bTg.js} +1 -1
- streamlit/static/static/js/{input.CAtwSQ27.js → input.C-PwAMG6.js} +1 -1
- streamlit/static/static/js/{main.VeMVx6VI.js → main.fMmyxXOf.js} +1 -1
- streamlit/static/static/js/{memory.CxS_lIUn.js → memory.BG__eDEj.js} +1 -1
- streamlit/static/static/js/{number-overlay-editor.CoqRgZW_.js → number-overlay-editor.C0n-91sR.js} +1 -1
- streamlit/static/static/js/{pandasStylerUtils.DGWgd5sM.js → pandasStylerUtils.BqWaUzvh.js} +1 -1
- streamlit/static/static/js/{sandbox.2mSqEau0.js → sandbox.CHhc-txg.js} +1 -1
- streamlit/static/static/js/{sprintfjs.CsoVVZ9k.js → sprintfjs.BlFBKfMf.js} +1 -1
- streamlit/static/static/js/{styled-components.BShfh7J8.js → styled-components.D5uOQqN2.js} +1 -1
- streamlit/static/static/js/{throttle.Fq1DQK4p.js → throttle.DlZC3xNA.js} +1 -1
- streamlit/static/static/js/{timepicker.DmyY-qtn.js → timepicker._TfRUaDL.js} +4 -4
- streamlit/static/static/js/{toConsumableArray.Dvd9AIqV.js → toConsumableArray.CZW4AmuW.js} +2 -2
- streamlit/static/static/js/uniqueId.CTTDAAaF.js +1 -0
- streamlit/static/static/js/useBasicWidgetState.Bx27912z.js +1 -0
- streamlit/static/static/js/{useIntlLocale.kdIj0ego.js → useIntlLocale.DsOvysl7.js} +1 -1
- streamlit/static/static/js/{useTextInputAutoExpand.DkuG1C1S.js → useTextInputAutoExpand.C9g8px1W.js} +1 -1
- streamlit/static/static/js/{useUpdateUiValue.-UI_JsjT.js → useUpdateUiValue.fF-Cntkp.js} +1 -1
- streamlit/static/static/js/{useWaveformController.C5PTwL6I.js → useWaveformController.C5EtFoJE.js} +1 -1
- streamlit/static/static/js/{withCalculatedWidth.BREyS0pJ.js → withCalculatedWidth.CcvaXQb0.js} +1 -1
- streamlit/static/static/js/{withFullScreenWrapper.DYDiEOrW.js → withFullScreenWrapper.DP61hzLF.js} +1 -1
- streamlit/testing/v1/element_tree.py +20 -15
- streamlit/web/server/app_static_file_handler.py +9 -0
- streamlit/web/server/bidi_component_request_handler.py +4 -4
- streamlit/web/server/component_file_utils.py +14 -6
- streamlit/web/server/component_request_handler.py +2 -2
- streamlit/web/server/routes.py +14 -0
- streamlit/web/server/server.py +4 -0
- streamlit/web/server/starlette/starlette_app.py +7 -1
- streamlit/web/server/starlette/starlette_path_security_middleware.py +97 -0
- streamlit/web/server/starlette/starlette_routes.py +6 -3
- streamlit/web/server/starlette/starlette_static_routes.py +14 -4
- {streamlit_nightly-1.53.2.dev20260202.dist-info → streamlit_nightly-1.54.1.dev20260204.dist-info}/METADATA +1 -1
- {streamlit_nightly-1.53.2.dev20260202.dist-info → streamlit_nightly-1.54.1.dev20260204.dist-info}/RECORD +135 -131
- streamlit/proto/Arrow_pb2.py +0 -34
- streamlit/static/static/js/index.CAoX2tlo.js +0 -2
- streamlit/static/static/js/index.DDr-BLbJ.js +0 -1
- streamlit/static/static/js/index.DL_ywOgf.js +0 -11
- streamlit/static/static/js/uniqueId.CCajdEK8.js +0 -1
- streamlit/static/static/js/useBasicWidgetState.COLQ5AFB.js +0 -1
- {streamlit_nightly-1.53.2.dev20260202.dist-info → streamlit_nightly-1.54.1.dev20260204.dist-info}/WHEEL +0 -0
- {streamlit_nightly-1.53.2.dev20260202.dist-info → streamlit_nightly-1.54.1.dev20260204.dist-info}/entry_points.txt +0 -0
- {streamlit_nightly-1.53.2.dev20260202.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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
123
|
-
self.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
|
|
142
|
+
# Empty list means explicit deselection by user - return None
|
|
143
|
+
if len(ui_value) == 0:
|
|
135
144
|
return None
|
|
136
145
|
|
|
137
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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.
|
|
158
|
-
self.
|
|
159
|
-
self.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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 =
|
|
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
|
-
|
|
729
|
-
|
|
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
|
-
|
|
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=
|
|
742
|
-
deserializer=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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="
|
|
961
|
+
value_type="string_array_value",
|
|
865
962
|
)
|
|
866
963
|
|
|
867
|
-
|
|
868
|
-
|
|
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.
|
|
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 =
|
|
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 =
|
|
1113
|
+
proto.editing_mode = DataframeProto.EditingMode.DYNAMIC
|
|
1114
1114
|
elif num_rows == "add":
|
|
1115
|
-
proto.editing_mode =
|
|
1115
|
+
proto.editing_mode = DataframeProto.EditingMode.ADD_ONLY
|
|
1116
1116
|
elif num_rows == "delete":
|
|
1117
|
-
proto.editing_mode =
|
|
1117
|
+
proto.editing_mode = DataframeProto.EditingMode.DELETE_ONLY
|
|
1118
1118
|
else:
|
|
1119
|
-
proto.editing_mode =
|
|
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("
|
|
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).
|
|
112
|
-
|
|
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
|
|
117
|
-
return
|
|
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()
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2026)
|
|
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
|
+
"""Shared path security utilities for preventing path traversal and SSRF attacks.
|
|
16
|
+
|
|
17
|
+
This module provides a centralized implementation for path validation that is
|
|
18
|
+
used by multiple parts of the codebase. Having a single implementation ensures
|
|
19
|
+
consistent security checks and avoids divergent behavior between components.
|
|
20
|
+
|
|
21
|
+
Security Context
|
|
22
|
+
----------------
|
|
23
|
+
These checks are designed to run BEFORE any filesystem operations (like
|
|
24
|
+
``os.path.realpath()``) to prevent Windows from triggering SMB connections
|
|
25
|
+
to attacker-controlled servers when resolving UNC paths. This prevents
|
|
26
|
+
SSRF attacks and NTLM hash disclosure.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import os
|
|
32
|
+
import string
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def is_unsafe_path_pattern(path: str) -> bool:
|
|
36
|
+
r"""Return True if path contains UNC, absolute, drive, or traversal patterns.
|
|
37
|
+
|
|
38
|
+
This function checks for dangerous path patterns that could lead to:
|
|
39
|
+
- SSRF attacks via Windows UNC path resolution
|
|
40
|
+
- NTLM hash disclosure via SMB connections
|
|
41
|
+
- Path traversal outside intended directories
|
|
42
|
+
- Path truncation via null bytes
|
|
43
|
+
|
|
44
|
+
IMPORTANT: This check must run BEFORE any ``os.path.realpath()`` calls
|
|
45
|
+
to prevent Windows from triggering SMB connections to attacker-controlled
|
|
46
|
+
servers.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
path : str
|
|
51
|
+
The path string to validate.
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
bool
|
|
56
|
+
True if the path contains unsafe patterns, False if it appears safe
|
|
57
|
+
for further processing.
|
|
58
|
+
|
|
59
|
+
Examples
|
|
60
|
+
--------
|
|
61
|
+
>>> is_unsafe_path_pattern("subdir/file.js")
|
|
62
|
+
False
|
|
63
|
+
>>> is_unsafe_path_pattern("\\\\server\\share")
|
|
64
|
+
True
|
|
65
|
+
>>> is_unsafe_path_pattern("../../../etc/passwd")
|
|
66
|
+
True
|
|
67
|
+
>>> is_unsafe_path_pattern("C:\\Windows\\system32")
|
|
68
|
+
True
|
|
69
|
+
"""
|
|
70
|
+
# Null bytes can be used for path truncation attacks
|
|
71
|
+
if "\x00" in path:
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
# UNC paths (Windows network shares, including \\?\ and \\.\ prefixes)
|
|
75
|
+
if path.startswith(("\\\\", "//")):
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
# Windows drive paths (e.g. C:\, D:foo) - on Windows, os.path.realpath() on a
|
|
79
|
+
# drive path can trigger SMB connections if the drive is mapped to a network share.
|
|
80
|
+
# This enables SSRF attacks and NTLM hash disclosure. We reject all drive-qualified
|
|
81
|
+
# paths including drive-relative paths like "C:foo" which resolve against the current
|
|
82
|
+
# directory of that drive. Checked on all platforms for defense-in-depth and
|
|
83
|
+
# testability (CI runs on Linux).
|
|
84
|
+
if len(path) >= 2 and path[0] in string.ascii_letters and path[1] == ":":
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
# Rooted backslash or forward slash (absolute paths)
|
|
88
|
+
if path.startswith(("\\", "/")):
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
# Also check os.path.isabs for platform-specific absolute path detection
|
|
92
|
+
if os.path.isabs(path):
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
# Path traversal - check segments after normalizing separators
|
|
96
|
+
normalized = path.replace("\\", "/")
|
|
97
|
+
segments = [seg for seg in normalized.split("/") if seg]
|
|
98
|
+
return ".." in segments
|
streamlit/proto/ArrowData_pb2.py
CHANGED
|
@@ -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\"\
|
|
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=
|
|
25
|
-
_globals['_ARROWDATA']._serialized_end=
|
|
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)
|