streamlit 1.49.1__py3-none-any.whl → 1.50.0__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 (155) hide show
  1. streamlit/column_config.py +2 -0
  2. streamlit/commands/navigation.py +3 -1
  3. streamlit/components/v1/custom_component.py +17 -42
  4. streamlit/config.py +306 -0
  5. streamlit/connections/base_connection.py +4 -2
  6. streamlit/dataframe_util.py +3 -2
  7. streamlit/delta_generator.py +2 -3
  8. streamlit/elements/arrow.py +63 -43
  9. streamlit/elements/deck_gl_json_chart.py +1 -0
  10. streamlit/elements/form.py +6 -6
  11. streamlit/elements/graphviz_chart.py +23 -6
  12. streamlit/elements/iframe.py +0 -2
  13. streamlit/elements/image.py +10 -9
  14. streamlit/elements/layouts.py +58 -11
  15. streamlit/elements/lib/built_in_chart_utils.py +95 -29
  16. streamlit/elements/lib/column_config_utils.py +5 -0
  17. streamlit/elements/lib/column_types.py +563 -144
  18. streamlit/elements/lib/dialog.py +1 -0
  19. streamlit/elements/lib/pandas_styler_utils.py +30 -14
  20. streamlit/elements/lib/utils.py +17 -5
  21. streamlit/elements/map.py +1 -3
  22. streamlit/elements/media.py +2 -0
  23. streamlit/elements/metric.py +10 -32
  24. streamlit/elements/plotly_chart.py +17 -9
  25. streamlit/elements/pyplot.py +6 -6
  26. streamlit/elements/vega_charts.py +110 -44
  27. streamlit/elements/widgets/audio_input.py +48 -0
  28. streamlit/elements/widgets/button.py +27 -25
  29. streamlit/elements/widgets/button_group.py +1 -0
  30. streamlit/elements/widgets/camera_input.py +1 -0
  31. streamlit/elements/widgets/chat.py +1 -0
  32. streamlit/elements/widgets/checkbox.py +1 -0
  33. streamlit/elements/widgets/color_picker.py +1 -0
  34. streamlit/elements/widgets/data_editor.py +6 -5
  35. streamlit/elements/widgets/file_uploader.py +1 -0
  36. streamlit/elements/widgets/multiselect.py +10 -0
  37. streamlit/elements/widgets/number_input.py +3 -0
  38. streamlit/elements/widgets/radio.py +1 -0
  39. streamlit/elements/widgets/select_slider.py +1 -0
  40. streamlit/elements/widgets/selectbox.py +4 -0
  41. streamlit/elements/widgets/slider.py +1 -0
  42. streamlit/elements/widgets/text_widgets.py +6 -0
  43. streamlit/elements/widgets/time_widgets.py +9 -0
  44. streamlit/elements/write.py +1 -17
  45. streamlit/git_util.py +65 -43
  46. streamlit/material_icon_names.py +1 -1
  47. streamlit/proto/Arrow_pb2.py +10 -8
  48. streamlit/proto/Arrow_pb2.pyi +31 -2
  49. streamlit/proto/AudioInput_pb2.py +2 -2
  50. streamlit/proto/AudioInput_pb2.pyi +6 -2
  51. streamlit/proto/Block_pb2.py +11 -11
  52. streamlit/proto/Block_pb2.pyi +5 -0
  53. streamlit/proto/NewSession_pb2.py +18 -16
  54. streamlit/proto/NewSession_pb2.pyi +135 -2
  55. streamlit/runtime/app_session.py +18 -5
  56. streamlit/runtime/theme_util.py +148 -0
  57. streamlit/static/index.html +2 -2
  58. streamlit/static/manifest.json +222 -222
  59. streamlit/static/static/css/index.CHEnSPGk.css +1 -0
  60. streamlit/static/static/css/{index.C8X8rNzw.css → index.CIiu7Ygf.css} +1 -1
  61. streamlit/static/static/js/{ErrorOutline.esm.DcGrhbBP.js → ErrorOutline.esm.DUpR0_Ka.js} +1 -1
  62. streamlit/static/static/js/{FileDownload.esm.DgBvV6Pq.js → FileDownload.esm.CN4j9-1w.js} +1 -1
  63. streamlit/static/static/js/{FileHelper.M6AAaeuA.js → FileHelper.CaIUKG91.js} +1 -1
  64. streamlit/static/static/js/{FormClearHelper.DHh1GFzm.js → FormClearHelper.DTcdrasw.js} +1 -1
  65. streamlit/static/static/js/{Hooks.DGu1od_L.js → Hooks.BRba_Own.js} +1 -1
  66. streamlit/static/static/js/InputInstructions.xnSDuYeQ.js +1 -0
  67. streamlit/static/static/js/{Particles.DDVT-6Qc.js → Particles.CElH0XX2.js} +1 -1
  68. streamlit/static/static/js/{ProgressBar.BEY0cXXV.js → ProgressBar.DetlP5aY.js} +2 -2
  69. streamlit/static/static/js/Toolbar.C77ar7rq.js +1 -0
  70. streamlit/static/static/js/{base-input.CK3UVGp1.js → base-input.BQft14La.js} +3 -3
  71. streamlit/static/static/js/{checkbox.D8W881TL.js → checkbox.yZOfXCeX.js} +1 -1
  72. streamlit/static/static/js/{createSuper.B6W-Dh9S.js → createSuper.Dh9w1cs8.js} +1 -1
  73. streamlit/static/static/js/data-grid-overlay-editor.DcuHuCyW.js +1 -0
  74. streamlit/static/static/js/{downloader.DiKpuU_S.js → downloader.MeHtkq8r.js} +1 -1
  75. streamlit/static/static/js/{es6.B8zRNPZ-.js → es6.VpBPGCnM.js} +2 -2
  76. streamlit/static/static/js/{iframeResizer.contentWindow.DIewJmmh.js → iframeResizer.contentWindow.yMw_ARIL.js} +1 -1
  77. streamlit/static/static/js/{index.B9mjBcgE.js → index.64ejlaaT.js} +1 -1
  78. streamlit/static/static/js/{index.CD8HuT3N.js → index.6xX1278W.js} +90 -91
  79. streamlit/static/static/js/index.B-hiXRzw.js +1 -0
  80. streamlit/static/static/js/{index.Ch7MBCx0.js → index.B0H9IXUJ.js} +47 -47
  81. streamlit/static/static/js/{index.4eF4NxG2.js → index.B4cAbHP6.js} +1 -1
  82. streamlit/static/static/js/{index.Dk4C7X3i.js → index.B4dUQfni.js} +1 -1
  83. streamlit/static/static/js/{index.CvYYtxD_.js → index.BPQo7BKk.js} +1 -1
  84. streamlit/static/static/js/index.Baqa90pe.js +2 -0
  85. streamlit/static/static/js/{index.D5naqx-J.js → index.Bj9JgOEC.js} +1 -1
  86. streamlit/static/static/js/index.BjCwMzj4.js +3 -0
  87. streamlit/static/static/js/{index.C_tmcx4B.js → index.Bm3VbPB5.js} +1 -1
  88. streamlit/static/static/js/{index.C7fRKRs4.js → index.Bxz2yX3P.js} +1 -1
  89. streamlit/static/static/js/{index.ho6NIXGl.js → index.BycLveZ4.js} +1 -1
  90. streamlit/static/static/js/{index.452cqrrL.js → index.C9BdUqTi.js} +1 -1
  91. streamlit/static/static/js/index.CFMf5_ez.js +197 -0
  92. streamlit/static/static/js/index.CGYqqs6j.js +1 -0
  93. streamlit/static/static/js/{index.zecpGxtj.js → index.CH1tqnSs.js} +1 -1
  94. streamlit/static/static/js/{index.CjXWwH-y.js → index.CMItVsFA.js} +1 -1
  95. streamlit/static/static/js/{index.B6U8LQo3.js → index.CTBk8Vk2.js} +1 -1
  96. streamlit/static/static/js/index.CiAQIz1H.js +7 -0
  97. streamlit/static/static/js/index.Cj7DSzVR.js +73 -0
  98. streamlit/static/static/js/index.Ck8rQ9OL.js +1 -0
  99. streamlit/static/static/js/{index.Ts_0SdB9.js → index.ClELlchS.js} +2 -2
  100. streamlit/static/static/js/{index.Bte_9Lyq.js → index.Cnpi3o3E.js} +1 -1
  101. streamlit/static/static/js/{index.CcJf6BCU.js → index.Ctn27_AE.js} +1 -1
  102. streamlit/static/static/js/{index.CP5TD2z1.js → index.D2QEXQq_.js} +1 -1
  103. streamlit/static/static/js/index.DH71Ezyj.js +1 -0
  104. streamlit/static/static/js/{index.D2-atlaQ.js → index.DHh-U0dK.js} +2 -2
  105. streamlit/static/static/js/{index.DtYN2x4k.js → index.DK7hD7_w.js} +1 -1
  106. streamlit/static/static/js/{index.qhs54UAB.js → index.DKv_lNO7.js} +1 -1
  107. streamlit/static/static/js/index.DNLrMXgm.js +12 -0
  108. streamlit/static/static/js/index.DW0Grddz.js +1 -0
  109. streamlit/static/static/js/{index.cnnXF7xQ.js → index.Dbe-Q3C-.js} +1 -1
  110. streamlit/static/static/js/index.DcPNYEUo.js +1 -0
  111. streamlit/static/static/js/index.DuxqVQpd.js +1 -0
  112. streamlit/static/static/js/{index.CejBxbg1.js → index.FFOzOWzC.js} +1 -1
  113. streamlit/static/static/js/{index.BnEpvLEz.js → index.GRUzrudl.js} +1 -1
  114. streamlit/static/static/js/{input.nzVJphXi.js → input.s6pjQ49A.js} +1 -1
  115. streamlit/static/static/js/{memory.CjCgTQz3.js → memory.Cuvsdfrl.js} +1 -1
  116. streamlit/static/static/js/{number-overlay-editor.DaRFzZEO.js → number-overlay-editor.DdgVR5m3.js} +1 -1
  117. streamlit/static/static/js/{possibleConstructorReturn.DgiPnZ9N.js → possibleConstructorReturn.CqidKeei.js} +1 -1
  118. streamlit/static/static/js/{sandbox.mithfq7Z.js → sandbox.CCQREcJx.js} +1 -1
  119. streamlit/static/static/js/{timepicker.Dbl5KFh6.js → timepicker.mkJF97Bb.js} +4 -4
  120. streamlit/static/static/js/{toConsumableArray.D-Dx88BQ.js → toConsumableArray.De7I7KVR.js} +1 -1
  121. streamlit/static/static/js/{uniqueId.Bh26R_3S.js → uniqueId.RI1LJdtz.js} +1 -1
  122. streamlit/static/static/js/{useBasicWidgetState.DeK-QJpD.js → useBasicWidgetState.CedkNjUW.js} +1 -1
  123. streamlit/static/static/js/{useTextInputAutoExpand.4iAdLWD-.js → useTextInputAutoExpand.Ca7w8dVs.js} +2 -2
  124. streamlit/static/static/js/{useUpdateUiValue.CmT7_nJN.js → useUpdateUiValue.DeXelfRH.js} +1 -1
  125. streamlit/static/static/js/withFullScreenWrapper.C3561XxJ.js +1 -0
  126. streamlit/static/static/media/MaterialSymbols-Rounded.DeCZgS-4.woff2 +0 -0
  127. streamlit/string_util.py +58 -1
  128. streamlit/web/bootstrap.py +0 -31
  129. streamlit/web/server/routes.py +17 -4
  130. streamlit/web/server/server.py +1 -0
  131. {streamlit-1.49.1.dist-info → streamlit-1.50.0.dist-info}/METADATA +1 -1
  132. {streamlit-1.49.1.dist-info → streamlit-1.50.0.dist-info}/RECORD +136 -135
  133. streamlit/static/static/css/index.COe1010n.css +0 -1
  134. streamlit/static/static/js/InputInstructions.z6sVgyYt.js +0 -1
  135. streamlit/static/static/js/Toolbar.DSnK1fUh.js +0 -1
  136. streamlit/static/static/js/data-grid-overlay-editor.DRTHOydk.js +0 -1
  137. streamlit/static/static/js/index.BXYmrqnf.js +0 -1
  138. streamlit/static/static/js/index.B_8AnktO.js +0 -1
  139. streamlit/static/static/js/index.Bl7zGQSh.js +0 -7
  140. streamlit/static/static/js/index.BnJIOYn9.js +0 -73
  141. streamlit/static/static/js/index.C1HcTl5K.js +0 -1
  142. streamlit/static/static/js/index.C7lSmSOP.js +0 -1
  143. streamlit/static/static/js/index.D3K5nOu9.js +0 -197
  144. streamlit/static/static/js/index.DkKT3LUI.js +0 -1
  145. streamlit/static/static/js/index.MTPPBDHk.js +0 -2
  146. streamlit/static/static/js/index.pqW9AMJD.js +0 -3
  147. streamlit/static/static/js/index.urHgTgMQ.js +0 -12
  148. streamlit/static/static/js/index.wzkv_11M.js +0 -1
  149. streamlit/static/static/js/index.yF5AncHY.js +0 -1
  150. streamlit/static/static/js/withFullScreenWrapper.DLp1ENGm.js +0 -1
  151. streamlit/static/static/media/MaterialSymbols-Rounded.CBxVaFdk.woff2 +0 -0
  152. {streamlit-1.49.1.data → streamlit-1.50.0.data}/scripts/streamlit.cmd +0 -0
  153. {streamlit-1.49.1.dist-info → streamlit-1.50.0.dist-info}/WHEEL +0 -0
  154. {streamlit-1.49.1.dist-info → streamlit-1.50.0.dist-info}/entry_points.txt +0 -0
  155. {streamlit-1.49.1.dist-info → streamlit-1.50.0.dist-info}/top_level.txt +0 -0
@@ -35,6 +35,7 @@ from streamlit.elements.lib.utils import (
35
35
  to_key,
36
36
  )
37
37
  from streamlit.elements.widgets.file_uploader import _get_upload_files
38
+ from streamlit.errors import StreamlitAPIException
38
39
  from streamlit.proto.AudioInput_pb2 import AudioInput as AudioInputProto
39
40
  from streamlit.proto.Common_pb2 import FileUploaderState as FileUploaderStateProto
40
41
  from streamlit.proto.Common_pb2 import UploadedFileInfo as UploadedFileInfoProto
@@ -54,6 +55,9 @@ if TYPE_CHECKING:
54
55
 
55
56
  SomeUploadedAudioFile: TypeAlias = Union[UploadedFile, DeletedFile, None]
56
57
 
58
+ # Allowed sample rates for audio recording
59
+ ALLOWED_SAMPLE_RATES = {8000, 11025, 16000, 22050, 24000, 32000, 44100, 48000}
60
+
57
61
 
58
62
  @dataclass
59
63
  class AudioInputSerde:
@@ -90,6 +94,7 @@ class AudioInputMixin:
90
94
  self,
91
95
  label: str,
92
96
  *,
97
+ sample_rate: int | None = 16000,
93
98
  key: Key | None = None,
94
99
  help: str | None = None,
95
100
  on_change: WidgetCallback | None = None,
@@ -125,6 +130,15 @@ class AudioInputMixin:
125
130
  .. |st.markdown| replace:: ``st.markdown``
126
131
  .. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
127
132
 
133
+ sample_rate : int or None
134
+ The target sample rate for the audio recording in Hz.
135
+ This defaults to 16000 Hz, which is optimal for speech recognition.
136
+
137
+ The following sample rates are supported: 8000, 11025, 16000,
138
+ 22050, 24000, 32000, 44100, or 48000. If this is ``None``, the
139
+ widget uses the browser's default sample rate (typically 44100 or
140
+ 48000 Hz).
141
+
128
142
  key : str or int
129
143
  An optional string or integer to use as the unique key for the widget.
130
144
  If this is omitted, a key will be generated for the widget
@@ -185,6 +199,10 @@ class AudioInputMixin:
185
199
 
186
200
  Examples
187
201
  --------
202
+ *Example 1:* Record a voice message and play it back.*
203
+
204
+ The default sample rate of 16000 Hz is optimal for speech recognition.
205
+
188
206
  >>> import streamlit as st
189
207
  >>>
190
208
  >>> audio_value = st.audio_input("Record a voice message")
@@ -196,10 +214,34 @@ class AudioInputMixin:
196
214
  https://doc-audio-input.streamlit.app/
197
215
  height: 260px
198
216
 
217
+ *Example 2:* Record high-fidelity audio and play it back.*
218
+
219
+ Higher sample rates can create higher-quality, larger audio files. This
220
+ might require a nicer microphone to fully appreciate the difference.
221
+
222
+ >>> import streamlit as st
223
+ >>>
224
+ >>> audio_value = st.audio_input("Record high quality audio", sample_rate=48000)
225
+ >>>
226
+ >>> if audio_value:
227
+ ... st.audio(audio_value)
228
+
229
+ .. output::
230
+ https://doc-audio-input-high-rate.streamlit.app/
231
+ height: 260px
232
+
199
233
  """
234
+ # Validate sample_rate parameter
235
+ if sample_rate is not None and sample_rate not in ALLOWED_SAMPLE_RATES:
236
+ raise StreamlitAPIException(
237
+ f"Invalid sample_rate: {sample_rate}. "
238
+ f"Must be one of {sorted(ALLOWED_SAMPLE_RATES)} Hz, or None for browser default."
239
+ )
240
+
200
241
  ctx = get_script_run_ctx()
201
242
  return self._audio_input(
202
243
  label=label,
244
+ sample_rate=sample_rate,
203
245
  key=key,
204
246
  help=help,
205
247
  on_change=on_change,
@@ -214,6 +256,7 @@ class AudioInputMixin:
214
256
  def _audio_input(
215
257
  self,
216
258
  label: str,
259
+ sample_rate: int | None = 16000,
217
260
  key: Key | None = None,
218
261
  help: str | None = None,
219
262
  on_change: WidgetCallback | None = None,
@@ -239,6 +282,7 @@ class AudioInputMixin:
239
282
  element_id = compute_and_register_element_id(
240
283
  "audio_input",
241
284
  user_key=key,
285
+ key_as_main_identity=False,
242
286
  dg=self.dg,
243
287
  label=label,
244
288
  help=help,
@@ -254,6 +298,10 @@ class AudioInputMixin:
254
298
  label_visibility
255
299
  )
256
300
 
301
+ # Set sample_rate in protobuf if specified
302
+ if sample_rate is not None:
303
+ audio_input_proto.sample_rate = sample_rate
304
+
257
305
  if label and help is not None:
258
306
  audio_input_proto.help = dedent(help)
259
307
 
@@ -188,6 +188,12 @@ class ButtonMixin:
188
188
  In both cases, if the contents of the button are wider than the
189
189
  parent container, the contents will line wrap.
190
190
 
191
+ .. deprecated::
192
+ ``use_container_width`` is deprecated and will be removed in a
193
+ future release. For ``use_container_width=True``, use
194
+ ``width="stretch"``. For ``use_container_width=False``, use
195
+ ``width="content"``.
196
+
191
197
  width : "content", "stretch", or int
192
198
  The width of the button. This can be one of the following:
193
199
 
@@ -201,12 +207,6 @@ class ButtonMixin:
201
207
  the parent container, the width of the button matches the width
202
208
  of the parent container.
203
209
 
204
- .. deprecated::
205
- ``use_container_width`` is deprecated and will be removed in a
206
- future release. For ``use_container_width=True``, use
207
- ``width="stretch"``. For ``use_container_width=False``, use
208
- ``width="content"``.
209
-
210
210
  Returns
211
211
  -------
212
212
  bool
@@ -432,6 +432,12 @@ class ButtonMixin:
432
432
  In both cases, if the contents of the button are wider than the
433
433
  parent container, the contents will line wrap.
434
434
 
435
+ .. deprecated::
436
+ ``use_container_width`` is deprecated and will be removed in a
437
+ future release. For ``use_container_width=True``, use
438
+ ``width="stretch"``. For ``use_container_width=False``, use
439
+ ``width="content"``.
440
+
435
441
  width : "content", "stretch", or int
436
442
  The width of the download button. This can be one of the following:
437
443
 
@@ -445,12 +451,6 @@ class ButtonMixin:
445
451
  the parent container, the width of the button matches the width
446
452
  of the parent container.
447
453
 
448
- .. deprecated::
449
- ``use_container_width`` is deprecated and will be removed in a
450
- future release. For ``use_container_width=True``, use
451
- ``width="stretch"``. For ``use_container_width=False``, use
452
- ``width="content"``.
453
-
454
454
  Returns
455
455
  -------
456
456
  bool
@@ -680,6 +680,12 @@ class ButtonMixin:
680
680
  In both cases, if the contents of the button are wider than the
681
681
  parent container, the contents will line wrap.
682
682
 
683
+ .. deprecated::
684
+ ``use_container_width`` is deprecated and will be removed in a
685
+ future release. For ``use_container_width=True``, use
686
+ ``width="stretch"``. For ``use_container_width=False``, use
687
+ ``width="content"``.
688
+
683
689
  width : "content", "stretch", or int
684
690
  The width of the link button. This can be one of the following:
685
691
 
@@ -693,12 +699,6 @@ class ButtonMixin:
693
699
  the parent container, the width of the button matches the width
694
700
  of the parent container.
695
701
 
696
- .. deprecated::
697
- ``use_container_width`` is deprecated and will be removed in a
698
- future release. For ``use_container_width=True``, use
699
- ``width="stretch"``. For ``use_container_width=False``, use
700
- ``width="content"``.
701
-
702
702
  Example
703
703
  -------
704
704
  >>> import streamlit as st
@@ -812,6 +812,12 @@ class ButtonMixin:
812
812
  The default is ``True`` for page links in the sidebar and ``False``
813
813
  for those in the main app.
814
814
 
815
+ .. deprecated::
816
+ ``use_container_width`` is deprecated and will be removed in a
817
+ future release. For ``use_container_width=True``, use
818
+ ``width="stretch"``. For ``use_container_width=False``, use
819
+ ``width="content"``.
820
+
815
821
  width : "content", "stretch", or int
816
822
  The width of the page-link button. This can be one of the following:
817
823
 
@@ -825,12 +831,6 @@ class ButtonMixin:
825
831
  the parent container, the width of the button matches the width
826
832
  of the parent container.
827
833
 
828
- .. deprecated::
829
- ``use_container_width`` is deprecated and will be removed in a
830
- future release. For ``use_container_width=True``, use
831
- ``width="stretch"``. For ``use_container_width=False``, use
832
- ``width="content"``.
833
-
834
834
  Example
835
835
  -------
836
836
  Consider the following example given this file structure:
@@ -914,6 +914,7 @@ class ButtonMixin:
914
914
  element_id = compute_and_register_element_id(
915
915
  "download_button",
916
916
  user_key=key,
917
+ key_as_main_identity=True,
917
918
  dg=self.dg,
918
919
  label=label,
919
920
  icon=icon,
@@ -1119,8 +1120,9 @@ class ButtonMixin:
1119
1120
  # Only the form submitter button needs a form ID at the moment.
1120
1121
  form_id = current_form_id(self.dg) if is_form_submitter else ""
1121
1122
  element_id = compute_and_register_element_id(
1122
- "button",
1123
+ "form_submit_button" if is_form_submitter else "button",
1123
1124
  user_key=key,
1125
+ key_as_main_identity=True,
1124
1126
  dg=self.dg,
1125
1127
  label=label,
1126
1128
  icon=icon,
@@ -1047,6 +1047,7 @@ class ButtonGroupMixin:
1047
1047
  # "feedback" in errors
1048
1048
  "feedback" if style == "borderless" else style,
1049
1049
  user_key=key,
1050
+ key_as_main_identity=False,
1050
1051
  dg=self.dg,
1051
1052
  options=formatted_options,
1052
1053
  default=default,
@@ -234,6 +234,7 @@ class CameraInputMixin:
234
234
  element_id = compute_and_register_element_id(
235
235
  "camera_input",
236
236
  user_key=key,
237
+ key_as_main_identity=False,
237
238
  dg=self.dg,
238
239
  label=label,
239
240
  help=help,
@@ -624,6 +624,7 @@ class ChatMixin:
624
624
  element_id = compute_and_register_element_id(
625
625
  "chat_input",
626
626
  user_key=key,
627
+ key_as_main_identity=False,
627
628
  dg=self.dg,
628
629
  placeholder=placeholder,
629
630
  max_chars=max_chars,
@@ -342,6 +342,7 @@ class CheckboxMixin:
342
342
  element_id = compute_and_register_element_id(
343
343
  "toggle" if type == CheckboxProto.StyleType.TOGGLE else "checkbox",
344
344
  user_key=key,
345
+ key_as_main_identity=True,
345
346
  dg=self.dg,
346
347
  label=label,
347
348
  value=bool(value),
@@ -219,6 +219,7 @@ class ColorPickerMixin:
219
219
  element_id = compute_and_register_element_id(
220
220
  "color_picker",
221
221
  user_key=key,
222
+ key_as_main_identity=False,
222
223
  dg=self.dg,
223
224
  label=label,
224
225
  value=str(value),
@@ -711,6 +711,11 @@ class DataEditorMixin:
711
711
  this is ``False``, Streamlit sets the data editor's width according
712
712
  to ``width``.
713
713
 
714
+ .. deprecated::
715
+ ``use_container_width`` is deprecated and will be removed in a
716
+ future release. For ``use_container_width=True``, use
717
+ ``width="stretch"``.
718
+
714
719
  hide_index : bool or None
715
720
  Whether to hide the index column(s). If ``hide_index`` is ``None``
716
721
  (default), the visibility of index columns is automatically
@@ -793,11 +798,6 @@ class DataEditorMixin:
793
798
  is ``None`` (default), Streamlit will use a default row height,
794
799
  which fits one line of text.
795
800
 
796
- .. deprecated::
797
- ``use_container_width`` is deprecated and will be removed in a
798
- future release. For ``use_container_width=True``, use
799
- ``width="stretch"``.
800
-
801
801
  Returns
802
802
  -------
803
803
  pandas.DataFrame, pandas.Series, pyarrow.Table, numpy.ndarray, list, set, tuple, or dict.
@@ -1028,6 +1028,7 @@ class DataEditorMixin:
1028
1028
  element_id = compute_and_register_element_id(
1029
1029
  "data_editor",
1030
1030
  user_key=key,
1031
+ key_as_main_identity=False,
1031
1032
  dg=self.dg,
1032
1033
  data=arrow_bytes,
1033
1034
  width=width,
@@ -488,6 +488,7 @@ class FileUploaderMixin:
488
488
  element_id = compute_and_register_element_id(
489
489
  "file_uploader",
490
490
  user_key=key,
491
+ key_as_main_identity=False,
491
492
  dg=self.dg,
492
493
  label=label,
493
494
  type=type,
@@ -492,6 +492,16 @@ class MultiSelectMixin:
492
492
  element_id = compute_and_register_element_id(
493
493
  widget_name,
494
494
  user_key=key,
495
+ # Treat the provided key as the main identity. Only include
496
+ # changes to the options, accept_new_options, and max_selections
497
+ # in the identity computation as those can invalidate the
498
+ # current selection.
499
+ key_as_main_identity={
500
+ "options",
501
+ "max_selections",
502
+ "accept_new_options",
503
+ "format_func",
504
+ },
495
505
  dg=self.dg,
496
506
  label=label,
497
507
  options=formatted_options,
@@ -452,6 +452,9 @@ class NumberInputMixin:
452
452
  element_id = compute_and_register_element_id(
453
453
  "number_input",
454
454
  user_key=key,
455
+ # Ensure stable ID when key is provided; explicitly whitelist parameters
456
+ # that might invalidate the current widget state.
457
+ key_as_main_identity={"min_value", "max_value", "step"},
455
458
  dg=self.dg,
456
459
  label=label,
457
460
  min_value=min_value,
@@ -369,6 +369,7 @@ class RadioMixin:
369
369
  element_id = compute_and_register_element_id(
370
370
  "radio",
371
371
  user_key=key,
372
+ key_as_main_identity=False,
372
373
  dg=self.dg,
373
374
  label=label,
374
375
  options=[str(format_func(option)) for option in opt],
@@ -377,6 +377,7 @@ class SelectSliderMixin:
377
377
  element_id = compute_and_register_element_id(
378
378
  "select_slider",
379
379
  user_key=key,
380
+ key_as_main_identity=False,
380
381
  dg=self.dg,
381
382
  label=label,
382
383
  options=[str(format_func(option)) for option in opt],
@@ -541,6 +541,10 @@ class SelectboxMixin:
541
541
  element_id = compute_and_register_element_id(
542
542
  "selectbox",
543
543
  user_key=key,
544
+ # Treat the provided key as the main identity. Only include
545
+ # the options and accept_new_options in the identity computation
546
+ # as those can invalidate the current selection.
547
+ key_as_main_identity={"options", "accept_new_options", "format_func"},
544
548
  dg=self.dg,
545
549
  label=label,
546
550
  options=formatted_options,
@@ -680,6 +680,7 @@ class SliderMixin:
680
680
  element_id = compute_and_register_element_id(
681
681
  "slider",
682
682
  user_key=key,
683
+ key_as_main_identity=False,
683
684
  dg=self.dg,
684
685
  label=label,
685
686
  min_value=min_value,
@@ -328,6 +328,9 @@ class TextWidgetsMixin:
328
328
  element_id = compute_and_register_element_id(
329
329
  "text_input",
330
330
  user_key=key,
331
+ # Explicitly whitelist max_chars to make sure the ID changes when it changes
332
+ # since the widget value might become invalid based on a different max_chars
333
+ key_as_main_identity={"max_chars"},
331
334
  dg=self.dg,
332
335
  label=label,
333
336
  value=value,
@@ -642,6 +645,9 @@ class TextWidgetsMixin:
642
645
  element_id = compute_and_register_element_id(
643
646
  "text_area",
644
647
  user_key=key,
648
+ # Explicitly whitelist max_chars to make sure the ID changes when it changes
649
+ # since the widget value might become invalid based on a different max_chars
650
+ key_as_main_identity={"max_chars"},
645
651
  dg=self.dg,
646
652
  label=label,
647
653
  value=value,
@@ -529,6 +529,9 @@ class TimeWidgetsMixin:
529
529
  element_id = compute_and_register_element_id(
530
530
  "time_input",
531
531
  user_key=key,
532
+ # Ensure stable ID when key is provided; only whitelist step since it
533
+ # affects the selection granularity and available options.
534
+ key_as_main_identity={"step"},
532
535
  dg=self.dg,
533
536
  label=label,
534
537
  value=parsed_time if isinstance(value, (datetime, time)) else value,
@@ -911,6 +914,12 @@ class TimeWidgetsMixin:
911
914
  element_id = compute_and_register_element_id(
912
915
  "date_input",
913
916
  user_key=key,
917
+ # Ensure stable ID when key is provided; explicitly whitelist parameters
918
+ # that might invalidate the current widget state.
919
+ # format should be supported. However, there is a bug in baseweb where
920
+ # changing the format dynamically leads to a wrongly formatted date.
921
+ # So, we whitelist it for now until we migrate this away from baseweb.
922
+ key_as_main_identity={"min_value", "max_value", "format"},
914
923
  dg=self.dg,
915
924
  label=label,
916
925
  value=parsed,
@@ -37,7 +37,6 @@ from typing import (
37
37
 
38
38
  from streamlit import dataframe_util, type_util
39
39
  from streamlit.errors import StreamlitAPIException
40
- from streamlit.logger import get_logger
41
40
  from streamlit.runtime.metrics_util import gather_metrics
42
41
  from streamlit.string_util import (
43
42
  is_mem_address_str,
@@ -56,7 +55,6 @@ HELP_TYPES: Final[tuple[type[Any], ...]] = (
56
55
  types.ModuleType,
57
56
  )
58
57
 
59
- _LOGGER: Final = get_logger(__name__)
60
58
 
61
59
  _TEXT_CURSOR: Final = " ▏"
62
60
 
@@ -254,7 +252,7 @@ class WriteMixin:
254
252
  return written_content
255
253
 
256
254
  @gather_metrics("write")
257
- def write(self, *args: Any, unsafe_allow_html: bool = False, **kwargs: Any) -> None:
255
+ def write(self, *args: Any, unsafe_allow_html: bool = False) -> None:
258
256
  """Displays arguments in the app.
259
257
 
260
258
  This is the Swiss Army knife of Streamlit commands: it does different
@@ -323,13 +321,6 @@ class WriteMixin:
323
321
  If you only want to insert HTML or CSS without Markdown text,
324
322
  we recommend using ``st.html`` instead.
325
323
 
326
- **kwargs : any
327
- Keyword arguments. Not used.
328
-
329
- .. deprecated::
330
- ``**kwargs`` is deprecated and will be removed in a later version.
331
- Use other, more specific Streamlit commands to pass additional
332
- keyword arguments.
333
324
 
334
325
  Returns
335
326
  -------
@@ -400,13 +391,6 @@ class WriteMixin:
400
391
  height: 300px
401
392
 
402
393
  """
403
- if kwargs:
404
- _LOGGER.warning(
405
- 'Invalid arguments were passed to "st.write" function. Support for '
406
- "passing such unknown keywords arguments will be dropped in future. "
407
- "Invalid arguments were: %s",
408
- kwargs,
409
- )
410
394
 
411
395
  if len(args) == 1 and isinstance(args[0], str):
412
396
  # Optimization: If there is only one arg, and it's a string,
streamlit/git_util.py CHANGED
@@ -16,21 +16,50 @@ from __future__ import annotations
16
16
 
17
17
  import os
18
18
  import re
19
- from typing import TYPE_CHECKING, cast
19
+ from typing import TYPE_CHECKING, Final, cast
20
20
 
21
21
  from streamlit import util
22
+ from streamlit.logger import get_logger
22
23
 
23
24
  if TYPE_CHECKING:
24
25
  from git import Commit, Remote, RemoteReference, Repo
25
26
 
26
- # Github has two URLs, one that is https and one that is ssh
27
- GITHUB_HTTP_URL = r"^https://(www\.)?github.com/(.+)/(.+)(?:.git)?$"
28
- GITHUB_SSH_URL = r"^git@github.com:(.+)/(.+)(?:.git)?$"
27
+ _LOGGER: Final = get_logger(__name__)
28
+
29
+ # Github repo extractor: match owner/repo for https/ssh/scp forms, with optional
30
+ # userinfo/port/trailing slash. This is just to extract the repo name, not validate the URL.
31
+ _GITHUB_URL_PATTERN: Final = re.compile(
32
+ r"github\.com(?::\d+)?[/:]([^/]+)/([^/]+?)(?:\.git)?/?$"
33
+ )
29
34
 
30
35
  # We don't support git < 2.7, because we can't get repo info without
31
36
  # talking to the remote server, which results in the user being prompted
32
37
  # for credentials.
33
- MIN_GIT_VERSION = (2, 7, 0)
38
+ _MIN_GIT_VERSION: Final = (2, 7, 0)
39
+
40
+
41
+ def _extract_github_repo_from_url(url: str) -> str | None:
42
+ """Extract the ``owner/repo`` from a GitHub remote URL.
43
+
44
+ This supports HTTPS and SSH URL forms including optional user info, port,
45
+ trailing slash, and ``.git`` suffix. Validation of the scheme is not
46
+ performed; we only extract if the URL contains ``github.com`` and ends with
47
+ a path of the shape ``owner/repo``.
48
+
49
+ Parameters
50
+ ----------
51
+ url
52
+ The remote URL string.
53
+
54
+ Returns
55
+ -------
56
+ str | None
57
+ The extracted ``owner/repo`` if found; otherwise ``None``.
58
+ """
59
+ match = _GITHUB_URL_PATTERN.search(url.strip())
60
+ if match is None:
61
+ return None
62
+ return f"{match.group(1)}/{match.group(2)}"
34
63
 
35
64
 
36
65
  class GitRepo:
@@ -40,6 +69,7 @@ class GitRepo:
40
69
  # If we have a valid repo, git_version will be a tuple
41
70
  # of 3+ ints: (major, minor, patch, possible_additional_patch_number)
42
71
  self.git_version: tuple[int, ...] | None = None
72
+ self.module: str = ""
43
73
 
44
74
  try:
45
75
  import git
@@ -47,26 +77,31 @@ class GitRepo:
47
77
  self.repo = git.Repo(path, search_parent_directories=True)
48
78
  self.git_version = self.repo.git.version_info
49
79
 
50
- if self.git_version is not None and self.git_version >= MIN_GIT_VERSION:
80
+ if self.git_version is not None and self.git_version >= _MIN_GIT_VERSION:
51
81
  git_root = self.repo.git.rev_parse("--show-toplevel")
52
82
  self.module = os.path.relpath(path, git_root)
53
83
  except Exception:
54
- # The git repo must be invalid for the following reasons:
55
- # * git binary or GitPython not installed
56
- # * No .git folder
57
- # * Corrupted .git folder
58
- # * Path is invalid
84
+ _LOGGER.debug(
85
+ "Did not find a git repo at %s. This is expected if this isn't a git repo, but could "
86
+ "also fail for other reasons: "
87
+ "1) git binary or GitPython not installed "
88
+ "2) No .git folder "
89
+ "3) Corrupted .git folder "
90
+ "4) Path is invalid.",
91
+ path,
92
+ exc_info=True,
93
+ )
59
94
  self.repo = None
60
95
 
61
96
  def __repr__(self) -> str:
62
97
  return util.repr_(self)
63
98
 
64
99
  def is_valid(self) -> bool:
65
- """True if there's a git repo here, and git.version >= MIN_GIT_VERSION."""
100
+ """True if there's a git repo here, and git.version >= _MIN_GIT_VERSION."""
66
101
  return (
67
102
  self.repo is not None
68
103
  and self.git_version is not None
69
- and self.git_version >= MIN_GIT_VERSION
104
+ and self.git_version >= _MIN_GIT_VERSION
70
105
  )
71
106
 
72
107
  @property
@@ -129,50 +164,37 @@ class GitRepo:
129
164
  remote_name, *branch = tracking_branch.name.split("/")
130
165
  branch_name = "/".join(branch)
131
166
 
132
- return self.repo.remote(remote_name), branch_name
133
-
134
- def is_github_repo(self) -> bool:
135
- if not self.is_valid():
136
- return False
137
-
138
- remote_info = self.get_tracking_branch_remote()
139
- if remote_info is None:
140
- return False
141
-
142
- remote, _branch = remote_info
143
-
144
- for url in remote.urls:
145
- if (
146
- re.match(GITHUB_HTTP_URL, url) is not None
147
- or re.match(GITHUB_SSH_URL, url) is not None
148
- ):
149
- return True
150
-
151
- return False
167
+ try:
168
+ return self.repo.remote(remote_name), branch_name
169
+ except Exception:
170
+ _LOGGER.debug("Failed to resolve remote %s", remote_name, exc_info=True)
171
+ return None
152
172
 
153
173
  def get_repo_info(self) -> tuple[str, str, str] | None:
154
174
  if not self.is_valid():
175
+ _LOGGER.debug(
176
+ "No valid git information found. Git version: %s", self.git_version
177
+ )
155
178
  return None
156
179
 
157
180
  remote_info = self.get_tracking_branch_remote()
158
181
  if remote_info is None:
182
+ _LOGGER.debug("No tracking remote branch found for the git repo.")
159
183
  return None
160
184
 
161
185
  remote, branch = remote_info
162
-
186
+ remote_urls = list(remote.urls)
163
187
  repo = None
164
- for url in remote.urls:
165
- https_matches = re.match(GITHUB_HTTP_URL, url)
166
- ssh_matches = re.match(GITHUB_SSH_URL, url)
167
- if https_matches is not None:
168
- repo = f"{https_matches.group(2)}/{https_matches.group(3)}"
169
- break
170
-
171
- if ssh_matches is not None:
172
- repo = f"{ssh_matches.group(1)}/{ssh_matches.group(2)}"
188
+ for url in remote_urls:
189
+ repo = _extract_github_repo_from_url(url)
190
+ if repo is not None:
173
191
  break
174
192
 
175
193
  if repo is None:
194
+ _LOGGER.debug(
195
+ "Unable to determine repo name from configured remote URLs. URLs: %s",
196
+ remote_urls,
197
+ )
176
198
  return None
177
199
 
178
200
  return repo, branch, self.module