streamlit-nightly 1.53.2.dev20260125__py3-none-any.whl → 1.53.2.dev20260127__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 (119) hide show
  1. streamlit/commands/logo.py +81 -25
  2. streamlit/deprecation_util.py +19 -1
  3. streamlit/elements/arrow.py +2 -1
  4. streamlit/elements/lib/built_in_chart_utils.py +2 -2
  5. streamlit/elements/lib/options_selector_utils.py +72 -22
  6. streamlit/elements/widgets/select_slider.py +123 -37
  7. streamlit/hello/plotting_demo.py +19 -12
  8. streamlit/proto/Logo_pb2.py +5 -3
  9. streamlit/proto/Logo_pb2.pyi +25 -1
  10. streamlit/proto/Slider_pb2.py +6 -6
  11. streamlit/proto/Slider_pb2.pyi +9 -1
  12. streamlit/runtime/scriptrunner/script_runner.py +17 -0
  13. streamlit/runtime/scriptrunner_utils/script_run_context.py +13 -10
  14. streamlit/runtime/state/__init__.py +7 -1
  15. streamlit/runtime/state/common.py +13 -0
  16. streamlit/runtime/state/query_params.py +494 -6
  17. streamlit/runtime/state/session_state.py +178 -3
  18. streamlit/runtime/state/widgets.py +26 -1
  19. streamlit/static/index.html +1 -1
  20. streamlit/static/manifest.json +304 -304
  21. streamlit/static/static/js/{ErrorOutline.esm.CIFYUdwC.js → ErrorOutline.esm.DiaGWPsE.js} +1 -1
  22. streamlit/static/static/js/{FileDownload.esm.DWVTnTHm.js → FileDownload.esm.D-Qgpk5d.js} +1 -1
  23. streamlit/static/static/js/{FileHelper.BPYQIPd1.js → FileHelper.DmtDDGp0.js} +1 -1
  24. streamlit/static/static/js/{FormClearHelper.CypmvhYZ.js → FormClearHelper.BM3uDLnU.js} +1 -1
  25. streamlit/static/static/js/{InputInstructions.Bi62hDTQ.js → InputInstructions.B2YZ5Lle.js} +1 -1
  26. streamlit/static/static/js/{Particles.yebG0VuV.js → Particles.BzfZrl-0.js} +1 -1
  27. streamlit/static/static/js/{ProgressBar.Dy9CI6w4.js → ProgressBar.DWErD3j3.js} +1 -1
  28. streamlit/static/static/js/{StreamlitSyntaxHighlighter.Btk92CPv.js → StreamlitSyntaxHighlighter.DB31rUPf.js} +1 -1
  29. streamlit/static/static/js/{TableChart.esm.DBeVaFNt.js → TableChart.esm.D3oUYYYd.js} +1 -1
  30. streamlit/static/static/js/{Toolbar.DC2Tp-qb.js → Toolbar.CPyjMABY.js} +1 -1
  31. streamlit/static/static/js/{WidgetLabelHelpIconInline.3DnEd9BK.js → WidgetLabelHelpIconInline.C1NBCx-y.js} +1 -1
  32. streamlit/static/static/js/{base-input.7Sj6pVk0.js → base-input.C_CrNgNd.js} +1 -1
  33. streamlit/static/static/js/{checkbox.CcUx3XuQ.js → checkbox.S6O8DPFl.js} +1 -1
  34. streamlit/static/static/js/{createDownloadLinkElement.DZuwkCqy.js → createDownloadLinkElement.B-5tIJBw.js} +1 -1
  35. streamlit/static/static/js/{data-grid-overlay-editor.Dw-AewlN.js → data-grid-overlay-editor.BW5aAEJN.js} +1 -1
  36. streamlit/static/static/js/{downloader.Bsx5M2Du.js → downloader.BswLlj7p.js} +1 -1
  37. streamlit/static/static/js/embed.QjYZXbaW.js +193 -0
  38. streamlit/static/static/js/{es6.BpAqZaR_.js → es6.Dm_kl6Cp.js} +2 -2
  39. streamlit/static/static/js/{formatNumber.DjehVPVS.js → formatNumber.CH39oSMw.js} +1 -1
  40. streamlit/static/static/js/{iconPosition.D02OPE-d.js → iconPosition.Dp9f2jam.js} +1 -1
  41. streamlit/static/static/js/{iframeResizer.contentWindow.xtstqPd7.js → iframeResizer.contentWindow.DOprY46-.js} +1 -1
  42. streamlit/static/static/js/{index.S-mjkUeF.js → index.-7yn3zqE.js} +1 -1
  43. streamlit/static/static/js/{index.Bri1T2TS.js → index.4QSKV4yi.js} +1 -1
  44. streamlit/static/static/js/{index.ZIA43eTF.js → index.6OwBTJdz.js} +1 -1
  45. streamlit/static/static/js/{index.x1B588Xu.js → index.6UQ__fP7.js} +1 -1
  46. streamlit/static/static/js/{index.CEwnDCn9.js → index.6iOQDEVC.js} +1 -1
  47. streamlit/static/static/js/index.B0Wcvg8a.js +2 -0
  48. streamlit/static/static/js/{index.DO2T-QzF.js → index.B6Sk5Jwr.js} +1 -1
  49. streamlit/static/static/js/{index.D_TIyPF4.js → index.B9XI6y3j.js} +1 -1
  50. streamlit/static/static/js/{index.B9gbSNsw.js → index.BBVcEzWu.js} +1 -1
  51. streamlit/static/static/js/{index.CA0RmxJF.js → index.BFxjXeYb.js} +1 -1
  52. streamlit/static/static/js/{index.B5tD5YeV.js → index.BNsCI-Q1.js} +1 -1
  53. streamlit/static/static/js/{index.DJjSqPAx.js → index.BQu5Qf_i.js} +1 -1
  54. streamlit/static/static/js/{index.BvZbnSMC.js → index.BUOjbZl6.js} +1 -1
  55. streamlit/static/static/js/{index.XFMDBL5n.js → index.BUbgW8RJ.js} +1 -1
  56. streamlit/static/static/js/{index.BOkpEbJS.js → index.BYjec3-R.js} +1 -1
  57. streamlit/static/static/js/{index.D6Z9hKJY.js → index.BaKZ4ira.js} +1 -1
  58. streamlit/static/static/js/{index.BIcJe97b.js → index.BcUwq6kI.js} +1 -1
  59. streamlit/static/static/js/{index.C9v49R-a.js → index.BlchQrZL.js} +1 -1
  60. streamlit/static/static/js/{index.D9RL5sRp.js → index.BlgpfTUq.js} +1 -1
  61. streamlit/static/static/js/{index.BV6XgCij.js → index.Bwaj-9Zv.js} +1 -1
  62. streamlit/static/static/js/{index.DDu_qTm0.js → index.BxtweGcn.js} +1 -1
  63. streamlit/static/static/js/{index.CGbvkEtg.js → index.C57ViuAX.js} +1 -1
  64. streamlit/static/static/js/{index.CdRwiHPm.js → index.CAzk7fUP.js} +1 -1
  65. streamlit/static/static/js/{index.BK9S5qug.js → index.CDZffl1q.js} +1 -1
  66. streamlit/static/static/js/{index.JL0uGAeJ.js → index.CYxcFtYt.js} +1 -1
  67. streamlit/static/static/js/index.Cb7V19H9.js +2 -0
  68. streamlit/static/static/js/{index.CgARjn28.js → index.CfXTal6g.js} +1 -1
  69. streamlit/static/static/js/{index.CyDHwK5P.js → index.Ci3x7GcT.js} +1 -1
  70. streamlit/static/static/js/{index.BVhVdVeE.js → index.CivdhE_9.js} +1 -1
  71. streamlit/static/static/js/{index.m3dn5Bai.js → index.CkSzTTpO.js} +5 -5
  72. streamlit/static/static/js/{index.8FPw0_gD.js → index.CuJ82OKQ.js} +1 -1
  73. streamlit/static/static/js/{index.BDlI2pRp.js → index.Cw5Xlisw.js} +1 -1
  74. streamlit/static/static/js/{index.DdxofXV8.js → index.D1tkD8Lg.js} +3 -3
  75. streamlit/static/static/js/{index.iXzAofuY.js → index.DK9Im19R.js} +2 -2
  76. streamlit/static/static/js/{index.iF5zYERg.js → index.DMwRlpNi.js} +1 -1
  77. streamlit/static/static/js/index.DTUAvbJ0.js +1 -0
  78. streamlit/static/static/js/{index.DSSapl3Q.js → index.DoRYpzHm.js} +1 -1
  79. streamlit/static/static/js/{index.D6J2UPzF.js → index.DsZNm1_D.js} +1 -1
  80. streamlit/static/static/js/{index.5H98WqjT.js → index.DypwtfIh.js} +1 -1
  81. streamlit/static/static/js/{index.DgLRJfs3.js → index.JKOtpaMf.js} +1 -1
  82. streamlit/static/static/js/{index.B8-HOwf1.js → index.OI2eh_me.js} +1 -1
  83. streamlit/static/static/js/{index.CKUBdVQ9.js → index.TTO_Lb69.js} +1 -1
  84. streamlit/static/static/js/{index.-faJDV20.js → index.X1r5cenD.js} +1 -1
  85. streamlit/static/static/js/{index.BqfJJr3c.js → index.gsc49XzN.js} +1 -1
  86. streamlit/static/static/js/{index.BB_iwaVr.js → index.lMhsw-5K.js} +32 -32
  87. streamlit/static/static/js/{index.DZv5AoR1.js → index.s9zpEF8P.js} +1 -1
  88. streamlit/static/static/js/{index.BGTMh3Uu.js → index.tjRGlTlQ.js} +1 -1
  89. streamlit/static/static/js/{index.Bo1ztye0.js → index.wKGUZfH4.js} +1 -1
  90. streamlit/static/static/js/{input.VYKyGuhi.js → input.CIKqvWjB.js} +1 -1
  91. streamlit/static/static/js/{main.u5Bb3MY7.js → main.BkNqoTrd.js} +1 -1
  92. streamlit/static/static/js/{memory.BOMt4yAV.js → memory.DNcbFok2.js} +1 -1
  93. streamlit/static/static/js/{number-overlay-editor.CihlAHgl.js → number-overlay-editor.DwjiYKav.js} +1 -1
  94. streamlit/static/static/js/{pandasStylerUtils.BuqSgXpk.js → pandasStylerUtils.BSc50we3.js} +1 -1
  95. streamlit/static/static/js/{sandbox.COGR4pqz.js → sandbox.DJL9Gdcf.js} +1 -1
  96. streamlit/static/static/js/{styled-components.BEf3c4IJ.js → styled-components.D4jE1G9j.js} +1 -1
  97. streamlit/static/static/js/{throttle.Bl-XsA9N.js → throttle.dHeXiPIK.js} +1 -1
  98. streamlit/static/static/js/{timepicker.B-HgBYlK.js → timepicker.CIpoSUyW.js} +1 -1
  99. streamlit/static/static/js/{toConsumableArray.BrQebwtE.js → toConsumableArray.AOXQx2YY.js} +1 -1
  100. streamlit/static/static/js/uniqueId.DRPc21MO.js +1 -0
  101. streamlit/static/static/js/{useBasicWidgetState.8WwISl9r.js → useBasicWidgetState.BwQxG7iA.js} +1 -1
  102. streamlit/static/static/js/{useIntlLocale.D37LWdCR.js → useIntlLocale.JXPZPWaJ.js} +1 -1
  103. streamlit/static/static/js/{useTextInputAutoExpand.Bb_KqJvq.js → useTextInputAutoExpand.CD_nhoE_.js} +1 -1
  104. streamlit/static/static/js/{useUpdateUiValue.D1BLS5t7.js → useUpdateUiValue.Cl1Y22Ao.js} +1 -1
  105. streamlit/static/static/js/{useWaveformController.Ce0-qTws.js → useWaveformController.DJhScSDn.js} +1 -1
  106. streamlit/static/static/js/{withCalculatedWidth.BX2K3UVv.js → withCalculatedWidth.CrAfFzO9.js} +1 -1
  107. streamlit/static/static/js/{withFullScreenWrapper.CqfGs8T2.js → withFullScreenWrapper.EdE3zOz5.js} +1 -1
  108. streamlit/testing/v1/element_tree.py +23 -8
  109. {streamlit_nightly-1.53.2.dev20260125.dist-info → streamlit_nightly-1.53.2.dev20260127.dist-info}/METADATA +1 -1
  110. {streamlit_nightly-1.53.2.dev20260125.dist-info → streamlit_nightly-1.53.2.dev20260127.dist-info}/RECORD +114 -114
  111. streamlit/static/static/js/embed.C7by6AoE.js +0 -195
  112. streamlit/static/static/js/index.Bhy8EBYI.js +0 -2
  113. streamlit/static/static/js/index.C5ehUqNt.js +0 -2
  114. streamlit/static/static/js/index.m4WkwGMu.js +0 -1
  115. streamlit/static/static/js/uniqueId.8R4hbkYl.js +0 -1
  116. {streamlit_nightly-1.53.2.dev20260125.data → streamlit_nightly-1.53.2.dev20260127.data}/scripts/streamlit.cmd +0 -0
  117. {streamlit_nightly-1.53.2.dev20260125.dist-info → streamlit_nightly-1.53.2.dev20260127.dist-info}/WHEEL +0 -0
  118. {streamlit_nightly-1.53.2.dev20260125.dist-info → streamlit_nightly-1.53.2.dev20260127.dist-info}/entry_points.txt +0 -0
  119. {streamlit_nightly-1.53.2.dev20260125.dist-info → streamlit_nightly-1.53.2.dev20260127.dist-info}/top_level.txt +0 -0
@@ -23,8 +23,10 @@ from streamlit.elements.lib.image_utils import AtomicImage, image_to_url
23
23
  from streamlit.elements.lib.layout_utils import LayoutConfig
24
24
  from streamlit.errors import StreamlitAPIException
25
25
  from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
26
+ from streamlit.proto.Logo_pb2 import Logo as LogoProto
26
27
  from streamlit.runtime.metrics_util import gather_metrics
27
28
  from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
29
+ from streamlit.string_util import is_emoji, validate_material_icon
28
30
 
29
31
 
30
32
  def _invalid_logo_text(field_name: str) -> str:
@@ -35,13 +37,59 @@ def _invalid_logo_text(field_name: str) -> str:
35
37
  )
36
38
 
37
39
 
40
+ def _process_logo_image(
41
+ image: AtomicImage | str, image_id: str
42
+ ) -> tuple[LogoProto.ImageType.ValueType, str]:
43
+ """Detects the image type and prepares the image data for the frontend.
44
+
45
+ Parameters
46
+ ----------
47
+ image :
48
+ The image that was provided by the user. Can be an image file,
49
+ emoji, or material icon string.
50
+ image_id : str
51
+ The image ID used when serving local images via the media file manager.
52
+
53
+ Returns
54
+ -------
55
+ tuple[ImageType, str]
56
+ The detected image type and the prepared image data.
57
+
58
+ Raises
59
+ ------
60
+ StreamlitAPIException
61
+ If the image is an empty string or a plain text string that is not
62
+ a valid file path, URL, emoji, or material icon.
63
+ """
64
+ ImageType = LogoProto.ImageType # noqa: N806
65
+
66
+ # Check if it's a material icon
67
+ if isinstance(image, str) and image.startswith(":material"):
68
+ return ImageType.ICON, validate_material_icon(image)
69
+
70
+ # Check if it's an emoji
71
+ if isinstance(image, str) and is_emoji(image):
72
+ return ImageType.EMOJI, image
73
+
74
+ # Otherwise, treat it as an image file
75
+ image_url = image_to_url(
76
+ image,
77
+ layout_config=LayoutConfig(width="content"),
78
+ clamp=False,
79
+ channels="RGB",
80
+ output_format="auto",
81
+ image_id=image_id,
82
+ )
83
+ return ImageType.IMAGE, image_url
84
+
85
+
38
86
  @gather_metrics("logo")
39
87
  def logo(
40
- image: AtomicImage,
88
+ image: AtomicImage | str,
41
89
  *, # keyword-only args:
42
90
  size: Literal["small", "medium", "large"] = "medium",
43
91
  link: str | None = None,
44
- icon_image: AtomicImage | None = None,
92
+ icon_image: AtomicImage | str | None = None,
45
93
  ) -> None:
46
94
  r"""
47
95
  Renders a logo in the upper-left corner of your app and its sidebar.
@@ -60,12 +108,26 @@ def logo(
60
108
 
61
109
  Parameters
62
110
  ----------
63
- image: Anything supported by st.image (except list)
111
+ image: Anything supported by st.image (except list), emoji, or icon
64
112
  The image to display in the upper-left corner of your app and its
65
- sidebar. This can be any of the types supported by |st.image|_ except
66
- a list. If ``icon_image`` is also provided, then Streamlit will only
113
+ sidebar. If ``icon_image`` is also provided, then Streamlit will only
67
114
  display ``image`` in the sidebar.
68
115
 
116
+ In addition to any of the types supported by |st.image|_ (except list),
117
+ the following strings are valid:
118
+
119
+ - A single-character emoji. For example, you can set ``image="🏠"``
120
+ or ``image="🚀"``. Emoji short codes are not supported.
121
+
122
+ - An icon from the Material Symbols library (rounded style) in the
123
+ format ``":material/icon_name:"`` where "icon_name" is the name
124
+ of the icon in snake case.
125
+
126
+ For example, ``image=":material/home:"`` will display the
127
+ Home icon. Find additional icons in the `Material Symbols \
128
+ <https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded>`_
129
+ font library.
130
+
69
131
  Streamlit scales the image to a max height set by ``size`` and a max
70
132
  width to fit within the sidebar.
71
133
 
@@ -84,15 +146,19 @@ def logo(
84
146
  The external URL to open when a user clicks on the logo. The URL must
85
147
  start with "\http://" or "\https://". If ``link`` is ``None`` (default),
86
148
  the logo will not include a hyperlink.
87
- icon_image: Anything supported by st.image (except list) or None
149
+ icon_image: Anything supported by st.image (except list), emoji, icon, or None
88
150
  An optional, typically smaller image to replace ``image`` in the
89
- upper-left corner when the sidebar is closed. This can be any of the
90
- types supported by ``st.image`` except a list. If ``icon_image`` is
151
+ upper-left corner when the sidebar is closed. If ``icon_image`` is
91
152
  ``None`` (default), Streamlit will always display ``image`` in the
92
153
  upper-left corner, regardless of whether the sidebar is open or closed.
93
154
  Otherwise, Streamlit will render ``icon_image`` in the upper-left
94
155
  corner of the app when the sidebar is closed.
95
156
 
157
+ In addition to any of the types supported by ``st.image`` (except list),
158
+ this also accepts single-character emojis (e.g., ``"🏠"``) and Material
159
+ icons (e.g., ``":material/home:"``). See the ``image`` parameter for
160
+ more details on these formats.
161
+
96
162
  Streamlit scales the image to a max height set by ``size`` and a max
97
163
  width to fit within the sidebar. If the sidebar is closed, the max
98
164
  width is retained from when it was last open.
@@ -143,15 +209,9 @@ def logo(
143
209
  fwd_msg = ForwardMsg()
144
210
 
145
211
  try:
146
- image_url = image_to_url(
147
- image,
148
- layout_config=LayoutConfig(width="content"),
149
- clamp=False,
150
- channels="RGB",
151
- output_format="auto",
152
- image_id="logo",
153
- )
154
- fwd_msg.logo.image = image_url
212
+ image_type, image_value = _process_logo_image(image, "logo")
213
+ fwd_msg.logo.image = image_value
214
+ fwd_msg.logo.image_type = image_type
155
215
  except Exception as ex:
156
216
  raise StreamlitAPIException(_invalid_logo_text("image")) from ex
157
217
 
@@ -167,15 +227,11 @@ def logo(
167
227
 
168
228
  if icon_image:
169
229
  try:
170
- icon_image_url = image_to_url(
171
- icon_image,
172
- layout_config=LayoutConfig(width="content"),
173
- clamp=False,
174
- channels="RGB",
175
- output_format="auto",
176
- image_id="icon-image",
230
+ icon_image_type, icon_image_value = _process_logo_image(
231
+ icon_image, "icon-image"
177
232
  )
178
- fwd_msg.logo.icon_image = icon_image_url
233
+ fwd_msg.logo.icon_image = icon_image_value
234
+ fwd_msg.logo.icon_image_type = icon_image_type
179
235
  except Exception as ex:
180
236
  raise StreamlitAPIException(_invalid_logo_text("icon_image")) from ex
181
237
 
@@ -21,12 +21,17 @@ from typing import Any, Final, TypeVar, cast
21
21
  import streamlit
22
22
  from streamlit import config
23
23
  from streamlit.logger import get_logger
24
+ from streamlit.util import calc_md5
24
25
 
25
26
  _LOGGER: Final = get_logger(__name__)
26
27
 
27
28
  TFunc = TypeVar("TFunc", bound=Callable[..., Any])
28
29
  TObj = TypeVar("TObj", bound=object)
29
30
 
31
+ # Set to track which deprecation warnings have been shown (by message hash)
32
+ # when show_once=True is used
33
+ _shown_warnings: set[str] = set()
34
+
30
35
 
31
36
  def _error_details_in_browser_enabled() -> bool:
32
37
  """True if we should print deprecation warnings to the browser.
@@ -42,7 +47,9 @@ def _error_details_in_browser_enabled() -> bool:
42
47
  )
43
48
 
44
49
 
45
- def show_deprecation_warning(message: str, show_in_browser: bool = True) -> None:
50
+ def show_deprecation_warning(
51
+ message: str, show_in_browser: bool = True, show_once: bool = False
52
+ ) -> None:
46
53
  """Show a deprecation warning message.
47
54
 
48
55
  Parameters
@@ -55,7 +62,18 @@ def show_deprecation_warning(message: str, show_in_browser: bool = True) -> None
55
62
  set `client.showErrorDetails` to "full" or the legacy True value. All
56
63
  other values ("stacktrace", "type", "none", False) will hide deprecation
57
64
  warnings in the browser (but still log them to the console).
65
+ show_once : bool, default=False
66
+ If True, the warning will only be shown once per unique message (based on
67
+ message hash). Subsequent calls with the same message will be skipped.
68
+ This is useful for warnings that may be triggered many times during a
69
+ script run.
58
70
  """
71
+ if show_once:
72
+ message_hash = calc_md5(message)
73
+ if message_hash in _shown_warnings:
74
+ return
75
+ _shown_warnings.add(message_hash)
76
+
59
77
  if _error_details_in_browser_enabled() and show_in_browser:
60
78
  streamlit.warning(message)
61
79
 
@@ -983,7 +983,8 @@ class ArrowMixin:
983
983
  " If you have a specific use-case that requires the `add_rows` "
984
984
  "functionality, please tell us via this "
985
985
  "[issue on Github](https://github.com/streamlit/streamlit/issues/13063).",
986
- show_in_browser=False,
986
+ show_in_browser=True,
987
+ show_once=True,
987
988
  )
988
989
 
989
990
  return _arrow_add_rows(self.dg, data, **kwargs)
@@ -862,9 +862,9 @@ def _maybe_melt(
862
862
  color_column = _MELTED_COLOR_COLUMN_NAME
863
863
 
864
864
  columns_to_leave_alone = [x_column]
865
- if size_column:
865
+ if size_column and size_column not in columns_to_leave_alone:
866
866
  columns_to_leave_alone.append(size_column)
867
- if sort_column:
867
+ if sort_column and sort_column not in columns_to_leave_alone:
868
868
  columns_to_leave_alone.append(sort_column)
869
869
 
870
870
  df = _melt_data(
@@ -297,6 +297,7 @@ def validate_and_sync_value_with_options(
297
297
  This function has a side-effect: if the value is not found in the options
298
298
  and a key is provided, it will update session state with the new value.
299
299
 
300
+
300
301
  Parameters
301
302
  ----------
302
303
  current_value
@@ -321,29 +322,16 @@ def validate_and_sync_value_with_options(
321
322
  if current_value is None:
322
323
  return current_value, False
323
324
 
324
- # For Enum values, use the original index_() approach which uses == comparison.
325
- # This correctly handles enum class identity - enums from different classes
326
- # (e.g., after script rerun) should NOT be considered equal, which is important
327
- # for enum coercion to work correctly when coercion is disabled.
328
- if isinstance(current_value, Enum):
329
- try:
330
- index_(opt, current_value)
331
- return current_value, False
332
- except ValueError:
333
- pass # Fall through to reset logic below
334
- else:
335
- # For non-Enum values, use format_func comparison. This handles custom objects
336
- # without __eq__ where widget values are deepcopied and the deepcopied instances
337
- # would fail identity comparison with ==.
338
- try:
339
- formatted_value = format_func(current_value)
340
- except Exception:
341
- # format_func failed - value is invalid
342
- formatted_value = None
343
-
344
- formatted_options_set = {format_func(o) for o in opt}
345
- if formatted_value is not None and formatted_value in formatted_options_set:
325
+ # Use format_func comparison for all values. This correctly handles:
326
+ # - Custom objects without __eq__ (deepcopied instances)
327
+ # - Enum values (already from current class due to serde deserialization)
328
+ formatted_options_set = {format_func(o) for o in opt}
329
+ try:
330
+ formatted_value = format_func(current_value)
331
+ if formatted_value in formatted_options_set:
346
332
  return current_value, False
333
+ except Exception: # noqa: S110
334
+ pass # format_func failed - value is invalid, fall through to reset
347
335
 
348
336
  # Value not in options - reset to default
349
337
  if default_index is not None and len(opt) > 0:
@@ -424,3 +412,65 @@ def validate_and_sync_multiselect_value_with_options(
424
412
  get_session_state().reset_state_value(str(key), valid_values)
425
413
 
426
414
  return valid_values, True
415
+
416
+
417
+ def validate_and_sync_range_value_with_options(
418
+ current_value: tuple[T, T],
419
+ opt: Sequence[T],
420
+ default_indices: list[int],
421
+ key: str | int | None,
422
+ format_func: Callable[[Any], str] = str,
423
+ ) -> tuple[tuple[T, T], bool]:
424
+ """Validate a range value (tuple of two values) against options.
425
+
426
+ If either value in the range is not found in options, the entire range is
427
+ reset to the default. This function has a side-effect: if the values are
428
+ invalid and a key is provided, it will update session state with the new value.
429
+
430
+ Parameters
431
+ ----------
432
+ current_value
433
+ The current range value (tuple of two values) to validate.
434
+ opt
435
+ The sequence of valid options.
436
+ default_indices
437
+ The default indices to reset to if value is invalid. Should contain
438
+ at least one index; if only one index is provided, the second default
439
+ will be the last option.
440
+ key
441
+ The widget key for session state updates.
442
+ format_func
443
+ Function to format options for comparison. Used to compare values by their
444
+ string representation instead of using == directly.
445
+
446
+ Returns
447
+ -------
448
+ tuple[tuple[T, T], bool]
449
+ A tuple of (validated_value, value_was_reset).
450
+ """
451
+ if len(opt) == 0:
452
+ return current_value, False
453
+
454
+ formatted_options_set = {format_func(o) for o in opt}
455
+
456
+ def is_valid(val: Any) -> bool:
457
+ """Check if a value exists in options via format_func comparison."""
458
+ try:
459
+ return format_func(val) in formatted_options_set
460
+ except Exception:
461
+ return False
462
+
463
+ def get_default_range() -> tuple[T, T]:
464
+ """Get the default range value."""
465
+ end_idx = default_indices[1] if len(default_indices) > 1 else len(opt) - 1
466
+ return (opt[default_indices[0]], opt[end_idx])
467
+
468
+ # Validate both values in the range.
469
+ if is_valid(current_value[0]) and is_valid(current_value[1]):
470
+ return current_value, False
471
+
472
+ # Either value is invalid - reset entire range.
473
+ new_value = get_default_range()
474
+ if key is not None:
475
+ get_session_state().reset_state_value(str(key), new_value)
476
+ return new_value, True
@@ -14,7 +14,6 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- from dataclasses import dataclass
18
17
  from textwrap import dedent
19
18
  from typing import (
20
19
  TYPE_CHECKING,
@@ -30,9 +29,12 @@ from streamlit.dataframe_util import OptionSequence, convert_anything_to_list
30
29
  from streamlit.elements.lib.form_utils import current_form_id
31
30
  from streamlit.elements.lib.layout_utils import LayoutConfig, validate_width
32
31
  from streamlit.elements.lib.options_selector_utils import (
32
+ create_mappings,
33
33
  index_,
34
34
  maybe_coerce_enum,
35
35
  maybe_coerce_enum_sequence,
36
+ validate_and_sync_range_value_with_options,
37
+ validate_and_sync_value_with_options,
36
38
  )
37
39
  from streamlit.elements.lib.policies import (
38
40
  check_widget_policies,
@@ -72,37 +74,79 @@ def _is_range_value(value: T | Sequence[T]) -> TypeGuard[Sequence[T]]:
72
74
  return isinstance(value, (list, tuple))
73
75
 
74
76
 
75
- @dataclass
76
77
  class SelectSliderSerde(Generic[T]):
77
- options: Sequence[T]
78
- value: list[int]
79
- is_range_value: bool
78
+ """Serializer/deserializer for select_slider widget values.
80
79
 
81
- def serialize(self, v: object) -> list[int]:
82
- return self._as_index_list(v)
80
+ Uses formatted option strings for robust handling of dynamic option changes.
81
+ """
83
82
 
84
- def deserialize(self, ui_value: list[int] | None) -> T | tuple[T, T]:
85
- if not ui_value:
86
- # Widget has not been used; fallback to the original value,
87
- ui_value = self.value
83
+ def __init__(
84
+ self,
85
+ options: Sequence[T],
86
+ *,
87
+ formatted_option_to_index: dict[str, int],
88
+ default_indices: list[int],
89
+ format_func: Callable[[Any], str] = str,
90
+ ) -> None:
91
+ self.options = options
92
+ self.formatted_option_to_index = formatted_option_to_index
93
+ self.default_indices = default_indices
94
+ self.format_func = format_func
95
+
96
+ def _get_default(self, is_range: bool) -> T | tuple[T, T]:
97
+ """Return the default value based on default_indices."""
98
+ if is_range or len(self.default_indices) >= 2:
99
+ end_idx = (
100
+ self.default_indices[1]
101
+ if len(self.default_indices) > 1
102
+ else len(self.options) - 1
103
+ )
104
+ return (self.options[self.default_indices[0]], self.options[end_idx])
105
+ return self.options[self.default_indices[0]]
88
106
 
89
- # The widget always returns floats, so convert to ints before indexing
90
- return_value: tuple[T, T] = cast(
91
- "tuple[T, T]",
92
- tuple(self.options[int(x)] for x in ui_value),
93
- )
107
+ def serialize(self, v: T | tuple[T, T] | list[T]) -> list[str]:
108
+ """Convert option value(s) to formatted string list."""
109
+ # Check if v is a single option (handles options that are tuples/lists)
110
+ try:
111
+ formatted = self.format_func(v)
112
+ if formatted in self.formatted_option_to_index:
113
+ return [formatted]
114
+ except Exception: # noqa: S110
115
+ pass
94
116
 
95
- # If the original value was a list/tuple, so will be the output (and vice versa)
96
- return return_value if self.is_range_value else return_value[0]
117
+ # Handle as range/sequence
118
+ if isinstance(v, (tuple, list)):
119
+ return [self.format_func(x) for x in v]
97
120
 
98
- def _as_index_list(self, v: Any) -> list[int]:
99
- if _is_range_value(v):
100
- slider_value = [index_(self.options, val) for val in v]
101
- start, end = slider_value
102
- if start > end:
103
- slider_value = [end, start]
104
- return slider_value
105
- return [index_(self.options, v)]
121
+ return [self.format_func(v)]
122
+
123
+ def deserialize(self, ui_value: list[str] | None) -> T | tuple[T, T]:
124
+ """Convert formatted string list back to option value(s)."""
125
+ is_range = ui_value is not None and len(ui_value) >= 2
126
+
127
+ if not ui_value:
128
+ return self._get_default(is_range=len(self.default_indices) >= 2)
129
+
130
+ # Look up each string value
131
+ results: list[tuple[int, T]] = []
132
+ for i, s in enumerate(ui_value):
133
+ idx = self.formatted_option_to_index.get(s)
134
+ if idx is not None and idx < len(self.options):
135
+ results.append((idx, self.options[idx]))
136
+ else:
137
+ # Fallback to default for this position
138
+ default_idx = self.default_indices[
139
+ min(i, len(self.default_indices) - 1)
140
+ ]
141
+ results.append((default_idx, self.options[default_idx]))
142
+
143
+ if is_range and len(results) >= 2:
144
+ # Ensure start <= end by returning deserialized range value in ascending order
145
+ if results[0][0] > results[1][0]:
146
+ return (results[1][1], results[0][1])
147
+ return (results[0][1], results[1][1])
148
+
149
+ return results[0][1]
106
150
 
107
151
 
108
152
  class SelectSliderMixin:
@@ -378,16 +422,18 @@ class SelectSliderMixin:
378
422
  # Convert element to index of the elements
379
423
  slider_value = as_index_list(value)
380
424
 
425
+ # Create formatted options and mapping for string-based storage
426
+ formatted_options, formatted_option_to_option_index = create_mappings(
427
+ opt, format_func
428
+ )
429
+
381
430
  element_id = compute_and_register_element_id(
382
431
  "select_slider",
383
432
  user_key=key,
384
- # Treat the provided key as the main identity; only include
385
- # changes to the options (and implicitly their formatting) in the
386
- # identity computation as those can invalidate the current value.
387
- key_as_main_identity={"options", "format_func"},
433
+ key_as_main_identity=True,
388
434
  dg=self.dg,
389
435
  label=label,
390
- options=[str(format_func(option)) for option in opt],
436
+ options=formatted_options,
391
437
  value=slider_value,
392
438
  help=help,
393
439
  width=width,
@@ -403,7 +449,7 @@ class SelectSliderMixin:
403
449
  slider_proto.max = len(opt) - 1
404
450
  slider_proto.step = 1 # default for index changes
405
451
  slider_proto.data_type = SliderProto.INT
406
- slider_proto.options[:] = [str(format_func(option)) for option in opt]
452
+ slider_proto.options[:] = formatted_options
407
453
  slider_proto.form_id = current_form_id(self.dg)
408
454
  slider_proto.disabled = disabled
409
455
  slider_proto.label_visibility.value = get_label_visibility_proto_value(
@@ -415,7 +461,12 @@ class SelectSliderMixin:
415
461
  validate_width(width)
416
462
  layout_config = LayoutConfig(width=width)
417
463
 
418
- serde = SelectSliderSerde(opt, slider_value, _is_range_value(value))
464
+ serde = SelectSliderSerde(
465
+ opt,
466
+ formatted_option_to_index=formatted_option_to_option_index,
467
+ default_indices=slider_value,
468
+ format_func=format_func,
469
+ )
419
470
 
420
471
  widget_state = register_widget(
421
472
  slider_proto.id,
@@ -425,7 +476,7 @@ class SelectSliderMixin:
425
476
  deserializer=serde.deserialize,
426
477
  serializer=serde.serialize,
427
478
  ctx=ctx,
428
- value_type="double_array_value",
479
+ value_type="string_array_value",
429
480
  )
430
481
  if isinstance(widget_state.value, tuple):
431
482
  widget_state = maybe_coerce_enum_sequence(
@@ -434,15 +485,50 @@ class SelectSliderMixin:
434
485
  else:
435
486
  widget_state = maybe_coerce_enum(widget_state, options, opt)
436
487
 
437
- if widget_state.value_changed:
438
- slider_proto.value[:] = serde.serialize(widget_state.value)
488
+ # Validate the current value against the new options.
489
+ # If the value is no longer valid (not in options), reset to default.
490
+ # This handles the case where options change dynamically and the
491
+ # previously selected value is no longer available.
492
+ # Determine if we're dealing with a range value based on the actual
493
+ # widget state value, not just the value parameter (range can come from
494
+ # session state even when value param is None).
495
+ actual_is_range = isinstance(widget_state.value, tuple)
496
+ if actual_is_range:
497
+ # Range value: validate using range-specific function.
498
+ range_value = cast("tuple[T, T]", widget_state.value)
499
+ validated_range, value_needs_reset = (
500
+ validate_and_sync_range_value_with_options(
501
+ range_value,
502
+ opt,
503
+ slider_value,
504
+ key,
505
+ format_func,
506
+ )
507
+ )
508
+ current_value: T | tuple[T, T] = validated_range
509
+ else:
510
+ # Single value: use the standard validation function.
511
+ validated_single, value_needs_reset = validate_and_sync_value_with_options(
512
+ widget_state.value,
513
+ opt,
514
+ slider_value[0],
515
+ key,
516
+ format_func,
517
+ )
518
+ # validated_single is guaranteed to be T (not None) because
519
+ # deserialize() always returns a default value, never None.
520
+ current_value = cast("T", validated_single)
521
+
522
+ if value_needs_reset or widget_state.value_changed:
523
+ serialized_value = serde.serialize(current_value)
524
+ slider_proto.raw_value[:] = serialized_value
439
525
  slider_proto.set_value = True
440
526
 
441
527
  if ctx:
442
528
  save_for_app_testing(ctx, element_id, format_func)
443
529
 
444
530
  self.dg._enqueue("slider", slider_proto, layout_config=layout_config)
445
- return widget_state.value
531
+ return current_value
446
532
 
447
533
  @property
448
534
  def dg(self) -> DeltaGenerator:
@@ -23,16 +23,24 @@ from streamlit.hello.utils import show_code
23
23
  def plotting_demo() -> None:
24
24
  progress_bar = st.sidebar.progress(0)
25
25
  status_text = st.sidebar.empty()
26
- last_rows = np.random.randn(1, 1) # noqa: NPY002
27
- chart = st.line_chart(last_rows)
28
-
29
- for i in range(1, 101):
30
- new_rows = last_rows[-1, :] + np.random.randn(5, 1).cumsum(axis=0) # noqa: NPY002
31
- status_text.text(f"{i}% complete")
32
- chart.add_rows(new_rows)
33
- progress_bar.progress(i)
34
- last_rows = new_rows
35
- time.sleep(0.05)
26
+ chart = st.empty()
27
+
28
+ # Initialize with one data point
29
+ data = np.random.randn(1, 1) # noqa: NPY002
30
+ chart.line_chart(data)
31
+
32
+ for i in range(1, 51):
33
+ # Generate new rows based on the last value (random walk)
34
+ new_rows = data[-1, :] + np.random.randn(5, 1).cumsum(axis=0) # noqa: NPY002
35
+ # Append new rows to existing data
36
+ data = np.concatenate([data, new_rows])
37
+ # Update the chart with full data
38
+ chart.line_chart(data)
39
+ # Scale progress to show 0-100% with 50 iterations
40
+ progress = i * 2
41
+ status_text.text(f"{progress}% complete")
42
+ progress_bar.progress(progress)
43
+ time.sleep(0.01)
36
44
 
37
45
  progress_bar.empty()
38
46
 
@@ -47,8 +55,7 @@ st.title("Plotting demo")
47
55
  st.write(
48
56
  """
49
57
  This demo illustrates a combination of plotting and animation with
50
- Streamlit. We're generating a bunch of random numbers in a loop for around
51
- 5 seconds. Enjoy!
58
+ Streamlit. We're generating a bunch of random numbers in a loop. Enjoy!
52
59
  """
53
60
  )
54
61
  plotting_demo()
@@ -14,7 +14,7 @@ _sym_db = _symbol_database.Default()
14
14
 
15
15
 
16
16
 
17
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1astreamlit/proto/Logo.proto\"E\n\x04Logo\x12\r\n\x05image\x18\x01 \x01(\t\x12\x0c\n\x04link\x18\x02 \x01(\t\x12\x12\n\nicon_image\x18\x03 \x01(\t\x12\x0c\n\x04size\x18\x04 \x01(\tB)\n\x1c\x63om.snowflake.apps.streamlitB\tLogoProtob\x06proto3')
17
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1astreamlit/proto/Logo.proto\"\xc1\x01\n\x04Logo\x12\r\n\x05image\x18\x01 \x01(\t\x12\x0c\n\x04link\x18\x02 \x01(\t\x12\x12\n\nicon_image\x18\x03 \x01(\t\x12\x0c\n\x04size\x18\x04 \x01(\t\x12#\n\nimage_type\x18\x05 \x01(\x0e\x32\x0f.Logo.ImageType\x12(\n\x0ficon_image_type\x18\x06 \x01(\x0e\x32\x0f.Logo.ImageType\"+\n\tImageType\x12\t\n\x05IMAGE\x10\x00\x12\t\n\x05\x45MOJI\x10\x01\x12\x08\n\x04ICON\x10\x02\x42)\n\x1c\x63om.snowflake.apps.streamlitB\tLogoProtob\x06proto3')
18
18
 
19
19
  _globals = globals()
20
20
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -22,6 +22,8 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'streamlit.proto.Logo_pb2',
22
22
  if not _descriptor._USE_C_DESCRIPTORS:
23
23
  _globals['DESCRIPTOR']._loaded_options = None
24
24
  _globals['DESCRIPTOR']._serialized_options = b'\n\034com.snowflake.apps.streamlitB\tLogoProto'
25
- _globals['_LOGO']._serialized_start=30
26
- _globals['_LOGO']._serialized_end=99
25
+ _globals['_LOGO']._serialized_start=31
26
+ _globals['_LOGO']._serialized_end=224
27
+ _globals['_LOGO_IMAGETYPE']._serialized_start=181
28
+ _globals['_LOGO_IMAGETYPE']._serialized_end=224
27
29
  # @@protoc_insertion_point(module_scope)