streamlit-nightly 1.53.1.dev20260117__py3-none-any.whl → 1.53.1.dev20260121__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 (113) hide show
  1. streamlit/__init__.py +0 -4
  2. streamlit/connections/snowflake_connection.py +5 -2
  3. streamlit/dataframe_util.py +4 -2
  4. streamlit/elements/lib/built_in_chart_utils.py +3 -0
  5. streamlit/elements/lib/column_config_utils.py +3 -1
  6. streamlit/elements/widgets/radio.py +91 -31
  7. streamlit/elements/widgets/time_widgets.py +58 -7
  8. streamlit/hello/dataframe_demo.py +1 -1
  9. streamlit/proto/Radio_pb2.py +4 -2
  10. streamlit/proto/Radio_pb2.pyi +20 -3
  11. streamlit/static/index.html +1 -1
  12. streamlit/static/manifest.json +308 -308
  13. streamlit/static/static/js/{ErrorOutline.esm.CqVEQ-uJ.js → ErrorOutline.esm.C6VdLmcj.js} +1 -1
  14. streamlit/static/static/js/{FileDownload.esm.G-HJG6nt.js → FileDownload.esm.g3HiTWnX.js} +1 -1
  15. streamlit/static/static/js/{FileHelper.Bd4bPp0Y.js → FileHelper.DkZahZIe.js} +1 -1
  16. streamlit/static/static/js/{FormClearHelper.B74VElyu.js → FormClearHelper.CnDHLc6s.js} +1 -1
  17. streamlit/static/static/js/{InputInstructions.ClBLOwfm.js → InputInstructions.CJwgA0Sj.js} +1 -1
  18. streamlit/static/static/js/{Particles.CyYskark.js → Particles.BA0kQsJM.js} +1 -1
  19. streamlit/static/static/js/{ProgressBar.vWX1Igzt.js → ProgressBar.N6Nzla-b.js} +1 -1
  20. streamlit/static/static/js/{StreamlitSyntaxHighlighter.BNhyeEtl.js → StreamlitSyntaxHighlighter.DF8uiu4N.js} +1 -1
  21. streamlit/static/static/js/{TableChart.esm.Mk-Hvy1t.js → TableChart.esm.Ne8F0PNs.js} +1 -1
  22. streamlit/static/static/js/{Toolbar.NhlhlTzK.js → Toolbar.h597G9NG.js} +1 -1
  23. streamlit/static/static/js/{WidgetLabelHelpIconInline.Ch9VDnrM.js → WidgetLabelHelpIconInline.BMPpOfi3.js} +1 -1
  24. streamlit/static/static/js/{base-input.DSh9H2uv.js → base-input.CTx0w60a.js} +1 -1
  25. streamlit/static/static/js/{checkbox.DCFAQrRB.js → checkbox.otYZO7w2.js} +1 -1
  26. streamlit/static/static/js/{createDownloadLinkElement.D5zpcd24.js → createDownloadLinkElement.Az9SilWU.js} +1 -1
  27. streamlit/static/static/js/{data-grid-overlay-editor.GAqbIurd.js → data-grid-overlay-editor.Blj_5PJn.js} +1 -1
  28. streamlit/static/static/js/{downloader.C1MBZFo2.js → downloader.ur6VTRo6.js} +1 -1
  29. streamlit/static/static/js/{embed.CZG6oOsc.js → embed.Bo5Mp3yp.js} +1 -1
  30. streamlit/static/static/js/{es6.toHnBzIb.js → es6.Q-xD3Lx8.js} +2 -2
  31. streamlit/static/static/js/{formatNumber.BsU-5o3V.js → formatNumber._OZtRfH-.js} +1 -1
  32. streamlit/static/static/js/{iconPosition.OKOYrH6A.js → iconPosition.owgMmPXc.js} +1 -1
  33. streamlit/static/static/js/{iframeResizer.contentWindow.D6yxU4DL.js → iframeResizer.contentWindow.CVCEnOeT.js} +1 -1
  34. streamlit/static/static/js/{index.DwgEkDLk.js → index.1wQkO9-P.js} +1 -1
  35. streamlit/static/static/js/{index.DhGqPx_f.js → index.2pNQcghF.js} +1 -1
  36. streamlit/static/static/js/{index.BHWCIpbw.js → index.9GyNEnVn.js} +1 -1
  37. streamlit/static/static/js/{index._t3w-7R0.js → index.BBFXXyOE.js} +1 -1
  38. streamlit/static/static/js/{index.DbCS2N3T.js → index.BOH_U_Ua.js} +1 -1
  39. streamlit/static/static/js/{index.YRiVxrFw.js → index.BTy77A4v.js} +1 -1
  40. streamlit/static/static/js/{index.DR8Ty3j4.js → index.BZMDU-qx.js} +1 -1
  41. streamlit/static/static/js/{index.EOvPT8N-.js → index.BdKmWvyd.js} +1 -1
  42. streamlit/static/static/js/{index.SBwNLdli.js → index.BkFxYQoo.js} +1 -1
  43. streamlit/static/static/js/{index.0iCXW13-.js → index.BlZAOJTf.js} +1 -1
  44. streamlit/static/static/js/{index.DGrQJk9x.js → index.BtKtuLWl.js} +1 -1
  45. streamlit/static/static/js/{index.C_w1nWis.js → index.Buip23l3.js} +1 -1
  46. streamlit/static/static/js/{index.CXLtvxrQ.js → index.Bw5t0A1a.js} +1 -1
  47. streamlit/static/static/js/{index.B8mzpc1l.js → index.ByHx8_go.js} +1 -1
  48. streamlit/static/static/js/{index.Bs4E_0D7.js → index.Byo1IU24.js} +1 -1
  49. streamlit/static/static/js/{index.Bh0_7PvD.js → index.ByzGGHA-.js} +1 -1
  50. streamlit/static/static/js/{index.BVbT0yWJ.js → index.C-xuUZos.js} +1 -1
  51. streamlit/static/static/js/{index.Dl2MB5Tx.js → index.C0X75KQC.js} +1 -1
  52. streamlit/static/static/js/{index.CV4niwLB.js → index.C5MX205D.js} +1 -1
  53. streamlit/static/static/js/{index.Blo8DAcU.js → index.CECvn-YK.js} +1 -1
  54. streamlit/static/static/js/{index.P0_E3iNi.js → index.CHfpPdI8.js} +1 -1
  55. streamlit/static/static/js/{index.BU8aUWlo.js → index.CT8A-jH3.js} +1 -1
  56. streamlit/static/static/js/{index.CJCEiqv2.js → index.CT93Qeic.js} +1 -1
  57. streamlit/static/static/js/{index.E4x2UBkh.js → index.CZKvO8_W.js} +1 -1
  58. streamlit/static/static/js/{index.AkleAg_w.js → index.Ccjs4i9w.js} +1 -1
  59. streamlit/static/static/js/{index.BkYhb--A.js → index.CeCHrefy.js} +1 -1
  60. streamlit/static/static/js/{index.qjO5OK90.js → index.Cpn4aUEp.js} +6 -6
  61. streamlit/static/static/js/{index.5CwvMIZq.js → index.D-VZVj73.js} +1 -1
  62. streamlit/static/static/js/{index.D94k70Hg.js → index.DA1Zwrh6.js} +1 -1
  63. streamlit/static/static/js/{index.COcj-WpN.js → index.DAddHkEE.js} +1 -1
  64. streamlit/static/static/js/{index.BXqtIpgi.js → index.DAjvUscA.js} +1 -1
  65. streamlit/static/static/js/{index.D7Hgmilz.js → index.DPa8c-h5.js} +1 -1
  66. streamlit/static/static/js/{index.XgBfXgN1.js → index.DUoWDMOZ.js} +1 -1
  67. streamlit/static/static/js/{index.CqywI2eW.js → index.DYqiNZKP.js} +1 -1
  68. streamlit/static/static/js/{index.BW0F_Pbs.js → index.DcZ6Y6tq.js} +1 -1
  69. streamlit/static/static/js/{index.dGk9EWLh.js → index.DfDGWP9w.js} +1 -1
  70. streamlit/static/static/js/{index.D0h2EJ_T.js → index.DiEHlHgD.js} +1 -1
  71. streamlit/static/static/js/{index.Ch4X_YdF.js → index.DkmkM-3F.js} +1 -1
  72. streamlit/static/static/js/{index.Dq0ZbGYZ.js → index.Vo6SwZW9.js} +1 -1
  73. streamlit/static/static/js/{index.Bhr3IHkD.js → index.X86xhkRz.js} +1 -1
  74. streamlit/static/static/js/index.Z5FSW1PS.js +3 -0
  75. streamlit/static/static/js/{index.DJ8OTQbk.js → index.d4eN08zp.js} +1 -1
  76. streamlit/static/static/js/{index.1sBZt_1I.js → index.hri4IXS4.js} +5 -5
  77. streamlit/static/static/js/{index.CGt5hnLi.js → index.jADyDjmJ.js} +1 -1
  78. streamlit/static/static/js/{index.ia5Ub9p7.js → index.jZuoBudL.js} +1 -1
  79. streamlit/static/static/js/{index.C6LOxzFC.js → index.oieFJFkX.js} +1 -1
  80. streamlit/static/static/js/{index.-pgbbiGJ.js → index.rpudgXQV.js} +1 -1
  81. streamlit/static/static/js/{index.MZ7ugsN-.js → index.zR1le1Pe.js} +1 -1
  82. streamlit/static/static/js/{input.Bavj6HHJ.js → input.FrKOgyiF.js} +1 -1
  83. streamlit/static/static/js/{main.dnUTEH_j.js → main.DRehwjQT.js} +1 -1
  84. streamlit/static/static/js/{memory.Czf1Sxzc.js → memory.B2wgPAbQ.js} +1 -1
  85. streamlit/static/static/js/{number-overlay-editor.CRQIke3a.js → number-overlay-editor.CnTj7Q_W.js} +1 -1
  86. streamlit/static/static/js/{pandasStylerUtils.agmr-LQ2.js → pandasStylerUtils.CWancxgS.js} +1 -1
  87. streamlit/static/static/js/{sandbox.BlCiIomw.js → sandbox.DuleaI7G.js} +1 -1
  88. streamlit/static/static/js/{styled-components.Bz3KSbhj.js → styled-components.g3zaW9ch.js} +1 -1
  89. streamlit/static/static/js/{throttle.B1o314FW.js → throttle.Dta7xRnz.js} +1 -1
  90. streamlit/static/static/js/{timepicker.3x4ndo9E.js → timepicker.Bg1hWKQe.js} +1 -1
  91. streamlit/static/static/js/{toConsumableArray.BeHbBK8g.js → toConsumableArray.C3xlVjBM.js} +1 -1
  92. streamlit/static/static/js/uniqueId.2p-nMqYS.js +1 -0
  93. streamlit/static/static/js/{useBasicWidgetState.CFP4_PTk.js → useBasicWidgetState.CERZ06R-.js} +1 -1
  94. streamlit/static/static/js/{useIntlLocale.BJubkaPQ.js → useIntlLocale.DTs1RN9G.js} +1 -1
  95. streamlit/static/static/js/{useTextInputAutoExpand.DBGwhM4R.js → useTextInputAutoExpand.B6f9P3jg.js} +1 -1
  96. streamlit/static/static/js/{useUpdateUiValue.CyufNQfR.js → useUpdateUiValue.ByVBXV8J.js} +1 -1
  97. streamlit/static/static/js/{useWaveformController.Dj5h0D8Y.js → useWaveformController.D6wABMbB.js} +1 -1
  98. streamlit/static/static/js/{withCalculatedWidth.D0IRb-7w.js → withCalculatedWidth.Bd4vFDHU.js} +1 -1
  99. streamlit/static/static/js/{withFullScreenWrapper.CHBnw0Co.js → withFullScreenWrapper.CDnDcnIh.js} +1 -1
  100. streamlit/testing/v1/element_tree.py +2 -2
  101. streamlit/user_info.py +0 -39
  102. streamlit/watcher/event_based_path_watcher.py +37 -7
  103. streamlit/watcher/path_watcher.py +60 -1
  104. streamlit/watcher/util.py +26 -10
  105. streamlit/web/bootstrap.py +11 -2
  106. {streamlit_nightly-1.53.1.dev20260117.dist-info → streamlit_nightly-1.53.1.dev20260121.dist-info}/METADATA +1 -1
  107. {streamlit_nightly-1.53.1.dev20260117.dist-info → streamlit_nightly-1.53.1.dev20260121.dist-info}/RECORD +111 -111
  108. {streamlit_nightly-1.53.1.dev20260117.dist-info → streamlit_nightly-1.53.1.dev20260121.dist-info}/WHEEL +1 -1
  109. streamlit/static/static/js/index.DRGDE5oo.js +0 -3
  110. streamlit/static/static/js/uniqueId.Zn1vZBLX.js +0 -1
  111. {streamlit_nightly-1.53.1.dev20260117.data → streamlit_nightly-1.53.1.dev20260121.data}/scripts/streamlit.cmd +0 -0
  112. {streamlit_nightly-1.53.1.dev20260117.dist-info → streamlit_nightly-1.53.1.dev20260121.dist-info}/entry_points.txt +0 -0
  113. {streamlit_nightly-1.53.1.dev20260117.dist-info → streamlit_nightly-1.53.1.dev20260121.dist-info}/top_level.txt +0 -0
streamlit/__init__.py CHANGED
@@ -111,7 +111,6 @@ from streamlit.runtime.state import (
111
111
  )
112
112
  from streamlit.user_info import (
113
113
  UserInfoProxy as _UserInfoProxy,
114
- DeprecatedUserInfoProxy as _DeprecatedUserInfoProxy,
115
114
  login as _login,
116
115
  logout as _logout,
117
116
  )
@@ -275,9 +274,6 @@ logout = _logout
275
274
  # User
276
275
  user = _UserInfoProxy()
277
276
 
278
- # Experimental APIs
279
- experimental_user = _DeprecatedUserInfoProxy()
280
-
281
277
  # make it possible to call streamlit.components.v1.html etc. by importing it here
282
278
  # import in the very end to avoid partially-initialized module import errors, because
283
279
  # streamlit.components.v1 also uses some streamlit imports
@@ -135,7 +135,9 @@ class BaseSnowflakeConnection(BaseConnection["InternalSnowflakeConnection"]):
135
135
  ),
136
136
  wait=wait_fixed(1),
137
137
  )
138
- def _query(sql: str) -> DataFrame:
138
+ # `params` must be an explicit parameter (not captured from closure) so that
139
+ # `@st.cache_data` includes it in the cache key.
140
+ def _query(sql: str, params: Any = None) -> DataFrame:
139
141
  cur = self._instance.cursor()
140
142
  cur.execute(sql, params=params, **kwargs)
141
143
  return cur.fetch_pandas_all() # type: ignore
@@ -153,7 +155,7 @@ class BaseSnowflakeConnection(BaseConnection["InternalSnowflakeConnection"]):
153
155
  ttl=ttl,
154
156
  )(_query)
155
157
 
156
- return _query(sql)
158
+ return _query(sql, params)
157
159
 
158
160
  def write_pandas(
159
161
  self,
@@ -349,6 +351,7 @@ class BaseSnowflakeConnection(BaseConnection["InternalSnowflakeConnection"]):
349
351
  """Closes the underlying Snowflake connection."""
350
352
  if self._raw_instance is not None:
351
353
  self._raw_instance.close()
354
+ self._raw_instance = None
352
355
 
353
356
 
354
357
  class SnowflakeConnection(BaseSnowflakeConnection):
@@ -1072,8 +1072,10 @@ def is_colum_type_arrow_incompatible(column: Series[Any] | Index[Any]) -> bool:
1072
1072
  return True
1073
1073
 
1074
1074
  if column.dtype == "object":
1075
- # The dtype of mixed type columns is always object, the actual type of the column
1076
- # values can be determined via the infer_dtype function:
1075
+ # The dtype of mixed type columns is always object. In pandas 3.0+, pure
1076
+ # string columns use StringDtype instead of object, so they won't enter
1077
+ # this block (and they're Arrow-compatible anyway). The actual type of
1078
+ # object dtype column values can be determined via the infer_dtype function:
1077
1079
  # https://pandas.pydata.org/docs/reference/api/pandas.api.types.infer_dtype.html
1078
1080
  inferred_type = infer_dtype(column, skipna=True)
1079
1081
 
@@ -611,6 +611,9 @@ def _melt_data(
611
611
 
612
612
  y_series = melted_df[new_y_column_name]
613
613
  if (
614
+ # After melting columns of different dtypes, the result has object dtype.
615
+ # In pandas 3.0+, melting columns with the same StringDtype keeps StringDtype,
616
+ # so this check correctly identifies only truly mixed-type scenarios.
614
617
  y_series.dtype == "object"
615
618
  and "mixed" in infer_dtype(y_series)
616
619
  and len(y_series.unique()) > 100
@@ -255,7 +255,9 @@ def _determine_data_kind_via_pandas_dtype(
255
255
  if pd.api.types.is_object_dtype(
256
256
  column_dtype
257
257
  ) is False and pd.api.types.is_string_dtype(column_dtype):
258
- # The is_string_dtype
258
+ # This handles pandas 3.0+ StringDtype (and PyArrow-backed string types).
259
+ # We exclude object dtype here because object columns with string values
260
+ # are handled via _determine_data_kind_via_inferred_type in the caller.
259
261
  return ColumnDataKind.STRING
260
262
 
261
263
  return ColumnDataKind.UNKNOWN
@@ -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 TYPE_CHECKING, Any, Generic, TypeVar, cast, overload
20
19
 
@@ -27,7 +26,11 @@ from streamlit.elements.lib.layout_utils import (
27
26
  Width,
28
27
  validate_width,
29
28
  )
30
- from streamlit.elements.lib.options_selector_utils import index_, maybe_coerce_enum
29
+ from streamlit.elements.lib.options_selector_utils import (
30
+ create_mappings,
31
+ maybe_coerce_enum,
32
+ validate_and_sync_value_with_options,
33
+ )
31
34
  from streamlit.elements.lib.policies import (
32
35
  check_widget_policies,
33
36
  maybe_raise_label_warnings,
@@ -63,27 +66,70 @@ if TYPE_CHECKING:
63
66
  T = TypeVar("T")
64
67
 
65
68
 
66
- @dataclass
67
69
  class RadioSerde(Generic[T]):
70
+ """Serializer/deserializer for Radio widget values.
71
+
72
+ Uses string-based values (formatted option strings) for robust handling
73
+ of dynamic option changes, similar to SelectboxSerde.
74
+ """
75
+
68
76
  options: Sequence[T]
69
- index: int | None
77
+ formatted_options: list[str]
78
+ formatted_option_to_option_index: dict[str, int]
79
+ default_option_index: int | None
80
+ format_func: Callable[[Any], str]
70
81
 
71
- def serialize(self, v: object) -> int | None:
82
+ def __init__(
83
+ self,
84
+ options: Sequence[T],
85
+ *,
86
+ formatted_options: list[str],
87
+ formatted_option_to_option_index: dict[str, int],
88
+ default_option_index: int | None = None,
89
+ format_func: Callable[[Any], str] = str,
90
+ ) -> None:
91
+ self.options = options
92
+ self.formatted_options = formatted_options
93
+ self.formatted_option_to_option_index = formatted_option_to_option_index
94
+ self.default_option_index = default_option_index
95
+ self.format_func = format_func
96
+
97
+ def serialize(self, v: T | str | None) -> str | None:
72
98
  if v is None:
73
99
  return None
100
+ if len(self.options) == 0:
101
+ return None
74
102
 
75
- return 0 if len(self.options) == 0 else index_(self.options, v)
103
+ # Use format_func to find the formatted option instead of using
104
+ # index_(self.options, v) which relies on == comparison. This is necessary
105
+ # because widget values are deepcopied, and for custom classes without
106
+ # __eq__, the deepcopied instances would fail identity comparison.
107
+ try:
108
+ formatted_value = self.format_func(v)
109
+ except Exception:
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)
113
+
114
+ if formatted_value in self.formatted_option_to_option_index:
115
+ return formatted_value
116
+ # Value not found in options - return as raw string
117
+ return cast("str", v)
118
+
119
+ def deserialize(self, ui_value: str | None) -> T | str | None:
120
+ # If no options, there's no valid value - return None
121
+ if len(self.options) == 0:
122
+ return None
76
123
 
77
- def deserialize(self, ui_value: int | None) -> T | None:
78
- idx = ui_value if ui_value is not None else self.index
124
+ if ui_value is None:
125
+ return (
126
+ self.options[self.default_option_index]
127
+ if self.default_option_index is not None
128
+ else None
129
+ )
79
130
 
80
- return (
81
- self.options[idx]
82
- if idx is not None
83
- and len(self.options) > 0
84
- and self.options[idx] is not None
85
- else None
86
- )
131
+ option_index = self.formatted_option_to_option_index.get(ui_value)
132
+ return self.options[option_index] if option_index is not None else ui_value
87
133
 
88
134
 
89
135
  class RadioMixin:
@@ -368,18 +414,17 @@ class RadioMixin:
368
414
  opt = convert_anything_to_list(options)
369
415
  check_python_comparable(opt)
370
416
 
417
+ formatted_options, formatted_option_to_option_index = create_mappings(
418
+ opt, format_func
419
+ )
420
+
371
421
  element_id = compute_and_register_element_id(
372
422
  "radio",
373
423
  user_key=key,
374
- # Treat provided key as the main widget identity. Only include the
375
- # following parameters in the identity computation since they can
376
- # invalidate the current selection mapping.
377
- # Changes to format_func also invalidate the current selection,
378
- # but this is already handled via the `options` parameter below:
379
- key_as_main_identity={"options"},
424
+ key_as_main_identity=True,
380
425
  dg=self.dg,
381
426
  label=label,
382
- options=[str(format_func(option)) for option in opt],
427
+ options=formatted_options,
383
428
  index=index,
384
429
  help=help,
385
430
  horizontal=horizontal,
@@ -415,7 +460,7 @@ class RadioMixin:
415
460
  radio_proto.label = label
416
461
  if index is not None:
417
462
  radio_proto.default = index
418
- radio_proto.options[:] = [str(format_func(option)) for option in opt]
463
+ radio_proto.options[:] = formatted_options
419
464
  radio_proto.form_id = current_form_id(self.dg)
420
465
  radio_proto.horizontal = horizontal
421
466
  radio_proto.disabled = disabled
@@ -429,7 +474,13 @@ class RadioMixin:
429
474
  if help is not None:
430
475
  radio_proto.help = dedent(help)
431
476
 
432
- serde = RadioSerde(opt, index)
477
+ serde = RadioSerde(
478
+ opt,
479
+ formatted_options=formatted_options,
480
+ formatted_option_to_option_index=formatted_option_to_option_index,
481
+ default_option_index=index,
482
+ format_func=format_func,
483
+ )
433
484
 
434
485
  widget_state = register_widget(
435
486
  radio_proto.id,
@@ -439,21 +490,30 @@ class RadioMixin:
439
490
  deserializer=serde.deserialize,
440
491
  serializer=serde.serialize,
441
492
  ctx=ctx,
442
- value_type="int_value",
493
+ value_type="string_value",
443
494
  )
444
495
  widget_state = maybe_coerce_enum(widget_state, options, opt)
445
496
 
446
- if widget_state.value_changed:
447
- if widget_state.value is not None:
448
- serialized_value = serde.serialize(widget_state.value)
449
- if serialized_value is not None:
450
- radio_proto.value = serialized_value
497
+ # Validate the current value against the new options.
498
+ # If the value is no longer valid (not in options), reset to default.
499
+ # This handles the case where options change dynamically and the
500
+ # previously selected value is no longer available.
501
+ # Cast to T | None since radio doesn't support accept_new_options,
502
+ # so string values that aren't in options will be reset to default.
503
+ current_value, value_needs_reset = validate_and_sync_value_with_options(
504
+ cast("T | None", widget_state.value), opt, index, key
505
+ )
506
+
507
+ if value_needs_reset or widget_state.value_changed:
508
+ serialized_value = serde.serialize(current_value)
509
+ if serialized_value is not None:
510
+ radio_proto.raw_value = serialized_value
451
511
  radio_proto.set_value = True
452
512
 
453
513
  if ctx:
454
514
  save_for_app_testing(ctx, element_id, format_func)
455
515
  self.dg._enqueue("radio", radio_proto, layout_config=layout_config)
456
- return widget_state.value
516
+ return current_value
457
517
 
458
518
  @property
459
519
  def dg(self) -> DeltaGenerator:
@@ -539,6 +539,42 @@ def _validate_date_value(
539
539
  return cast("DateWidgetReturn", tuple(parsed_values.value)), True
540
540
 
541
541
 
542
+ def _validate_datetime_value(
543
+ current_value: datetime | None,
544
+ parsed_values: _DateTimeInputValues,
545
+ has_explicit_bounds: bool,
546
+ ) -> tuple[datetime | None, bool]:
547
+ """Validate current datetime value against min/max bounds and determine if reset is needed.
548
+
549
+ Only validates when has_explicit_bounds is True (user provided min_value or max_value).
550
+ This avoids incorrectly determining if reset is needed against computed default bounds.
551
+
552
+ Parameters
553
+ ----------
554
+ current_value : datetime | None
555
+ The current value of the datetime input widget.
556
+ parsed_values : _DateTimeInputValues
557
+ Parsed configuration containing min, max, and default value.
558
+ has_explicit_bounds : bool
559
+ Whether the user explicitly provided min_value or max_value. If False, validation
560
+ is skipped to avoid resetting against computed default bounds.
561
+
562
+ Returns
563
+ -------
564
+ tuple[datetime | None, bool]
565
+ A tuple of (validated_value, was_reset) where validated_value is either the
566
+ original value (if valid) or the default value (if reset was needed), and
567
+ was_reset indicates whether a reset occurred.
568
+ """
569
+ if current_value is None or not has_explicit_bounds:
570
+ return current_value, False
571
+
572
+ if current_value < parsed_values.min or current_value > parsed_values.max:
573
+ return parsed_values.value, True
574
+
575
+ return current_value, False
576
+
577
+
542
578
  @dataclass
543
579
  class DateInputSerde:
544
580
  value: _DateInputValues
@@ -1132,9 +1168,10 @@ class TimeWidgetsMixin:
1132
1168
  element_id = compute_and_register_element_id(
1133
1169
  "date_time_input",
1134
1170
  user_key=key,
1135
- # Ensure stable IDs when the key is provided; whitelist parameters that
1136
- # affect the selectable range or formatting.
1137
- key_as_main_identity={"min_value", "max_value", "format", "step"},
1171
+ # Format is whitelisted because of a bug in the BaseWeb date input component.
1172
+ # Step is whitelisted because it invalidates the current selection.
1173
+ # We might be able to unlock this as a follow-up.
1174
+ key_as_main_identity={"format", "step"},
1138
1175
  dg=self.dg,
1139
1176
  label=label,
1140
1177
  value=value_for_id,
@@ -1145,7 +1182,9 @@ class TimeWidgetsMixin:
1145
1182
  step=step,
1146
1183
  width=width,
1147
1184
  )
1148
- del value
1185
+ # Track if user explicitly set bounds (before del)
1186
+ has_explicit_bounds = min_value is not None or max_value is not None
1187
+ del value, min_value, max_value
1149
1188
 
1150
1189
  if not bool(ALLOWED_DATE_FORMATS.match(format)):
1151
1190
  raise StreamlitAPIException(
@@ -1208,8 +1247,20 @@ class TimeWidgetsMixin:
1208
1247
  value_type="string_array_value",
1209
1248
  )
1210
1249
 
1211
- if widget_state.value_changed:
1212
- date_time_input_proto.value[:] = serde.serialize(widget_state.value)
1250
+ # Validate the current value against the new min/max bounds.
1251
+ # Only validate when user explicitly provided min_value or max_value.
1252
+ current_value, value_needs_reset = _validate_datetime_value(
1253
+ widget_state.value, datetime_values, has_explicit_bounds
1254
+ )
1255
+
1256
+ if value_needs_reset and key is not None:
1257
+ # Update session_state so subsequent accesses in this run
1258
+ # return the corrected value. Use reset_state_value to avoid
1259
+ # the "cannot be modified after widget instantiated" error.
1260
+ get_session_state().reset_state_value(key, current_value)
1261
+
1262
+ if value_needs_reset or widget_state.value_changed:
1263
+ date_time_input_proto.value[:] = serde.serialize(current_value)
1213
1264
  date_time_input_proto.set_value = True
1214
1265
 
1215
1266
  validate_width(width)
@@ -1218,7 +1269,7 @@ class TimeWidgetsMixin:
1218
1269
  self.dg._enqueue(
1219
1270
  "date_time_input", date_time_input_proto, layout_config=layout_config
1220
1271
  )
1221
- return widget_state.value
1272
+ return current_value
1222
1273
 
1223
1274
  @overload
1224
1275
  def date_input(
@@ -54,7 +54,7 @@ def data_frame_demo() -> None:
54
54
  color="Region:N",
55
55
  )
56
56
  )
57
- st.altair_chart(chart, use_container_width=True)
57
+ st.altair_chart(chart, width="stretch")
58
58
  except URLError as e:
59
59
  st.error(f"This demo requires internet access. Connection error: {e.reason}")
60
60
 
@@ -15,7 +15,7 @@ _sym_db = _symbol_database.Default()
15
15
  from streamlit.proto import LabelVisibilityMessage_pb2 as streamlit_dot_proto_dot_LabelVisibilityMessage__pb2
16
16
 
17
17
 
18
- DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bstreamlit/proto/Radio.proto\x1a,streamlit/proto/LabelVisibilityMessage.proto\"\x90\x02\n\x05Radio\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05label\x18\x02 \x01(\t\x12\x14\n\x07\x64\x65\x66\x61ult\x18\x03 \x01(\x05H\x00\x88\x01\x01\x12\x0f\n\x07options\x18\x04 \x03(\t\x12\x0c\n\x04help\x18\x05 \x01(\t\x12\x0f\n\x07\x66orm_id\x18\x06 \x01(\t\x12\x12\n\x05value\x18\x07 \x01(\x05H\x01\x88\x01\x01\x12\x11\n\tset_value\x18\x08 \x01(\x08\x12\x10\n\x08\x64isabled\x18\t \x01(\x08\x12\x12\n\nhorizontal\x18\n \x01(\x08\x12\x31\n\x10label_visibility\x18\x0b \x01(\x0b\x32\x17.LabelVisibilityMessage\x12\x10\n\x08\x63\x61ptions\x18\x0c \x03(\tB\n\n\x08_defaultB\x08\n\x06_valueB*\n\x1c\x63om.snowflake.apps.streamlitB\nRadioProtob\x06proto3')
18
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1bstreamlit/proto/Radio.proto\x1a,streamlit/proto/LabelVisibilityMessage.proto\"\xba\x02\n\x05Radio\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05label\x18\x02 \x01(\t\x12\x14\n\x07\x64\x65\x66\x61ult\x18\x03 \x01(\x05H\x00\x88\x01\x01\x12\x0f\n\x07options\x18\x04 \x03(\t\x12\x0c\n\x04help\x18\x05 \x01(\t\x12\x0f\n\x07\x66orm_id\x18\x06 \x01(\t\x12\x16\n\x05value\x18\x07 \x01(\x05\x42\x02\x18\x01H\x01\x88\x01\x01\x12\x16\n\traw_value\x18\r \x01(\tH\x02\x88\x01\x01\x12\x11\n\tset_value\x18\x08 \x01(\x08\x12\x10\n\x08\x64isabled\x18\t \x01(\x08\x12\x12\n\nhorizontal\x18\n \x01(\x08\x12\x31\n\x10label_visibility\x18\x0b \x01(\x0b\x32\x17.LabelVisibilityMessage\x12\x10\n\x08\x63\x61ptions\x18\x0c \x03(\tB\n\n\x08_defaultB\x08\n\x06_valueB\x0c\n\n_raw_valueB*\n\x1c\x63om.snowflake.apps.streamlitB\nRadioProtob\x06proto3')
19
19
 
20
20
  _globals = globals()
21
21
  _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -23,6 +23,8 @@ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'streamlit.proto.Radio_pb2',
23
23
  if not _descriptor._USE_C_DESCRIPTORS:
24
24
  _globals['DESCRIPTOR']._loaded_options = None
25
25
  _globals['DESCRIPTOR']._serialized_options = b'\n\034com.snowflake.apps.streamlitB\nRadioProto'
26
+ _globals['_RADIO'].fields_by_name['value']._loaded_options = None
27
+ _globals['_RADIO'].fields_by_name['value']._serialized_options = b'\030\001'
26
28
  _globals['_RADIO']._serialized_start=78
27
- _globals['_RADIO']._serialized_end=350
29
+ _globals['_RADIO']._serialized_end=392
28
30
  # @@protoc_insertion_point(module_scope)
@@ -31,6 +31,11 @@ if sys.version_info >= (3, 10):
31
31
  else:
32
32
  from typing_extensions import TypeAlias as _TypeAlias
33
33
 
34
+ if sys.version_info >= (3, 13):
35
+ from warnings import deprecated as _deprecated
36
+ else:
37
+ from typing_extensions import deprecated as _deprecated
38
+
34
39
  DESCRIPTOR: _descriptor.FileDescriptor
35
40
 
36
41
  @_typing.final
@@ -44,6 +49,7 @@ class Radio(_message.Message):
44
49
  HELP_FIELD_NUMBER: _builtins.int
45
50
  FORM_ID_FIELD_NUMBER: _builtins.int
46
51
  VALUE_FIELD_NUMBER: _builtins.int
52
+ RAW_VALUE_FIELD_NUMBER: _builtins.int
47
53
  SET_VALUE_FIELD_NUMBER: _builtins.int
48
54
  DISABLED_FIELD_NUMBER: _builtins.int
49
55
  HORIZONTAL_FIELD_NUMBER: _builtins.int
@@ -54,7 +60,13 @@ class Radio(_message.Message):
54
60
  default: _builtins.int
55
61
  help: _builtins.str
56
62
  form_id: _builtins.str
57
- value: _builtins.int
63
+ @_builtins.property
64
+ @_deprecated("""This field has been marked as deprecated using proto field options.""")
65
+ def value(self) -> _builtins.int: ...
66
+ @value.setter
67
+ @_deprecated("""This field has been marked as deprecated using proto field options.""")
68
+ def value(self, value: _builtins.int) -> None: ...
69
+ raw_value: _builtins.str
58
70
  set_value: _builtins.bool
59
71
  disabled: _builtins.bool
60
72
  horizontal: _builtins.bool
@@ -74,23 +86,28 @@ class Radio(_message.Message):
74
86
  help: _builtins.str = ...,
75
87
  form_id: _builtins.str = ...,
76
88
  value: _builtins.int | None = ...,
89
+ raw_value: _builtins.str | None = ...,
77
90
  set_value: _builtins.bool = ...,
78
91
  disabled: _builtins.bool = ...,
79
92
  horizontal: _builtins.bool = ...,
80
93
  label_visibility: _LabelVisibilityMessage_pb2.LabelVisibilityMessage | None = ...,
81
94
  captions: _abc.Iterable[_builtins.str] | None = ...,
82
95
  ) -> None: ...
83
- _HasFieldArgType: _TypeAlias = _typing.Literal["_default", b"_default", "_value", b"_value", "default", b"default", "label_visibility", b"label_visibility", "value", b"value"] # noqa: Y015
96
+ _HasFieldArgType: _TypeAlias = _typing.Literal["_default", b"_default", "_raw_value", b"_raw_value", "_value", b"_value", "default", b"default", "label_visibility", b"label_visibility", "raw_value", b"raw_value", "value", b"value"] # noqa: Y015
84
97
  def HasField(self, field_name: _HasFieldArgType) -> _builtins.bool: ...
85
- _ClearFieldArgType: _TypeAlias = _typing.Literal["_default", b"_default", "_value", b"_value", "captions", b"captions", "default", b"default", "disabled", b"disabled", "form_id", b"form_id", "help", b"help", "horizontal", b"horizontal", "id", b"id", "label", b"label", "label_visibility", b"label_visibility", "options", b"options", "set_value", b"set_value", "value", b"value"] # noqa: Y015
98
+ _ClearFieldArgType: _TypeAlias = _typing.Literal["_default", b"_default", "_raw_value", b"_raw_value", "_value", b"_value", "captions", b"captions", "default", b"default", "disabled", b"disabled", "form_id", b"form_id", "help", b"help", "horizontal", b"horizontal", "id", b"id", "label", b"label", "label_visibility", b"label_visibility", "options", b"options", "raw_value", b"raw_value", "set_value", b"set_value", "value", b"value"] # noqa: Y015
86
99
  def ClearField(self, field_name: _ClearFieldArgType) -> None: ...
87
100
  _WhichOneofReturnType__default: _TypeAlias = _typing.Literal["default"] # noqa: Y015
88
101
  _WhichOneofArgType__default: _TypeAlias = _typing.Literal["_default", b"_default"] # noqa: Y015
102
+ _WhichOneofReturnType__raw_value: _TypeAlias = _typing.Literal["raw_value"] # noqa: Y015
103
+ _WhichOneofArgType__raw_value: _TypeAlias = _typing.Literal["_raw_value", b"_raw_value"] # noqa: Y015
89
104
  _WhichOneofReturnType__value: _TypeAlias = _typing.Literal["value"] # noqa: Y015
90
105
  _WhichOneofArgType__value: _TypeAlias = _typing.Literal["_value", b"_value"] # noqa: Y015
91
106
  @_typing.overload
92
107
  def WhichOneof(self, oneof_group: _WhichOneofArgType__default) -> _WhichOneofReturnType__default | None: ...
93
108
  @_typing.overload
109
+ def WhichOneof(self, oneof_group: _WhichOneofArgType__raw_value) -> _WhichOneofReturnType__raw_value | None: ...
110
+ @_typing.overload
94
111
  def WhichOneof(self, oneof_group: _WhichOneofArgType__value) -> _WhichOneofReturnType__value | None: ...
95
112
 
96
113
  Global___Radio: _TypeAlias = Radio # noqa: Y015
@@ -37,7 +37,7 @@
37
37
  <script>
38
38
  window.prerenderReady = false
39
39
  </script>
40
- <script type="module" crossorigin src="./static/js/index.qjO5OK90.js"></script>
40
+ <script type="module" crossorigin src="./static/js/index.Cpn4aUEp.js"></script>
41
41
  <link rel="stylesheet" crossorigin href="./static/css/index.BUP6fTcR.css">
42
42
  </head>
43
43
  <body>