streamlit 1.53.0__py3-none-any.whl → 1.53.1__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 (101) hide show
  1. streamlit/connections/snowflake_connection.py +1 -0
  2. streamlit/elements/lib/options_selector_utils.py +63 -19
  3. streamlit/elements/widgets/multiselect.py +31 -7
  4. streamlit/elements/widgets/selectbox.py +38 -16
  5. streamlit/static/index.html +1 -1
  6. streamlit/static/manifest.json +291 -291
  7. streamlit/static/static/js/{ErrorOutline.esm.Cxoit62D.js → ErrorOutline.esm.CScZvf44.js} +1 -1
  8. streamlit/static/static/js/{FileDownload.esm.Cym2KVOR.js → FileDownload.esm.COCxTZxP.js} +1 -1
  9. streamlit/static/static/js/{FileHelper.C47VLeXF.js → FileHelper.Bhs-iVRI.js} +1 -1
  10. streamlit/static/static/js/{FormClearHelper.CUrwwEeX.js → FormClearHelper.CA_5b-Ut.js} +1 -1
  11. streamlit/static/static/js/{InputInstructions.DyVOE42q.js → InputInstructions.Bzb0MCfv.js} +1 -1
  12. streamlit/static/static/js/{Particles.D5ZUTvE6.js → Particles.ix5_l22I.js} +1 -1
  13. streamlit/static/static/js/{ProgressBar.qKdiDYyx.js → ProgressBar.DyQNhVsJ.js} +1 -1
  14. streamlit/static/static/js/{StreamlitSyntaxHighlighter.DUPp9dS3.js → StreamlitSyntaxHighlighter.BOkJThtV.js} +1 -1
  15. streamlit/static/static/js/{TableChart.esm.C_g2CvCE.js → TableChart.esm.a60nntBC.js} +1 -1
  16. streamlit/static/static/js/{Toolbar.BbO8bxwz.js → Toolbar.CxkcuBQ8.js} +1 -1
  17. streamlit/static/static/js/{WidgetLabelHelpIconInline.Dy4yV6I2.js → WidgetLabelHelpIconInline.BjIku2ic.js} +1 -1
  18. streamlit/static/static/js/{base-input.DQAb60v0.js → base-input.avGkArOc.js} +1 -1
  19. streamlit/static/static/js/{checkbox.C0HE0ojW.js → checkbox.Q8mCuqps.js} +1 -1
  20. streamlit/static/static/js/{createDownloadLinkElement.DBMfH8_e.js → createDownloadLinkElement.CfqHRpxo.js} +1 -1
  21. streamlit/static/static/js/{data-grid-overlay-editor.CSZWem5Q.js → data-grid-overlay-editor.PuoMl3yV.js} +1 -1
  22. streamlit/static/static/js/{downloader.Bp8c7mYD.js → downloader.CjG2csSm.js} +1 -1
  23. streamlit/static/static/js/{embed.DQBlGL9Q.js → embed.DZ-CLCPz.js} +1 -1
  24. streamlit/static/static/js/{es6.j7akTCaI.js → es6.CQD6uUK7.js} +2 -2
  25. streamlit/static/static/js/{formatNumber.CfuUiEpF.js → formatNumber.CtjUO-if.js} +1 -1
  26. streamlit/static/static/js/{iconPosition.BVSTKfGd.js → iconPosition.7Qt6oUiI.js} +1 -1
  27. streamlit/static/static/js/{iframeResizer.contentWindow.BZ3lugzo.js → iframeResizer.contentWindow._oj2Xh0v.js} +1 -1
  28. streamlit/static/static/js/{index.Dk0CU4R6.js → index.B-XrnnK6.js} +1 -1
  29. streamlit/static/static/js/{index.DBIRzFM7.js → index.B2fAYU1N.js} +1 -1
  30. streamlit/static/static/js/{index.BMFt07G_.js → index.B5wmZkRW.js} +1 -1
  31. streamlit/static/static/js/{index.5CsPRetw.js → index.B6ZAXv47.js} +1 -1
  32. streamlit/static/static/js/{index.dgs1TGpP.js → index.B8PovXCX.js} +1 -1
  33. streamlit/static/static/js/{index.CZzy-Gct.js → index.BDm-Ia27.js} +1 -1
  34. streamlit/static/static/js/{index.CTQ8QcOV.js → index.BOTEMJfV.js} +1 -1
  35. streamlit/static/static/js/{index.1PD6f3vh.js → index.BOafPwIE.js} +1 -1
  36. streamlit/static/static/js/{index.BiVJWMS-.js → index.BRegnbUa.js} +1 -1
  37. streamlit/static/static/js/{index.DysJZEAt.js → index.BVX_bqnf.js} +1 -1
  38. streamlit/static/static/js/{index.BXfSsjdq.js → index.BWCFtBS4.js} +1 -1
  39. streamlit/static/static/js/{index.DtZTtufl.js → index.B_ylV_tl.js} +1 -1
  40. streamlit/static/static/js/{index.By8GIgDH.js → index.BeCZLkzg.js} +1 -1
  41. streamlit/static/static/js/{index.DgJeIFb5.js → index.Bg-9YNUa.js} +1 -1
  42. streamlit/static/static/js/{index.DSaE74nc.js → index.BhJwyXH6.js} +1 -1
  43. streamlit/static/static/js/{index.nEa8y_He.js → index.BksGMsW0.js} +1 -1
  44. streamlit/static/static/js/{index.C9QftD-S.js → index.BlJhnb4M.js} +1 -1
  45. streamlit/static/static/js/{index.Bukztsaz.js → index.BmDXWfgx.js} +1 -1
  46. streamlit/static/static/js/{index.Z0mB4zBp.js → index.BrRuSP42.js} +1 -1
  47. streamlit/static/static/js/{index.D0tXFTaW.js → index.BuBkymZd.js} +1 -1
  48. streamlit/static/static/js/{index.CeFdbzfR.js → index.BuEBeckn.js} +1 -1
  49. streamlit/static/static/js/{index.NtSfVVJe.js → index.CBqST2Yj.js} +1 -1
  50. streamlit/static/static/js/{index.FfR9SXQv.js → index.CIizdLeb.js} +1 -1
  51. streamlit/static/static/js/{index.CkmNfvPD.js → index.CL2eCR01.js} +1 -1
  52. streamlit/static/static/js/{index.Cow0Hs9V.js → index.CMBgAPh6.js} +1 -1
  53. streamlit/static/static/js/{index.iboGgrkh.js → index.CVRgrLT-.js} +1 -1
  54. streamlit/static/static/js/{index.BGzJYcHz.js → index.CdLlbsiN.js} +1 -1
  55. streamlit/static/static/js/{index.CAMxgVFm.js → index.Cptu1tS-.js} +1 -1
  56. streamlit/static/static/js/index.CsoN0h7K.js +188 -0
  57. streamlit/static/static/js/{index.Bk5wGJXh.js → index.CwIIk90V.js} +1 -1
  58. streamlit/static/static/js/{index.DpU0Bc2F.js → index.D1bkwsLT.js} +1 -1
  59. streamlit/static/static/js/{index.BDA5l7b9.js → index.D8t7R4QQ.js} +1 -1
  60. streamlit/static/static/js/{index.svncz-Ad.js → index.DDk0U8rh.js} +1 -1
  61. streamlit/static/static/js/{index.DYkkO_of.js → index.DJsqD2Sc.js} +1 -1
  62. streamlit/static/static/js/{index.CsmTnJl4.js → index.DNB79dOd.js} +1 -1
  63. streamlit/static/static/js/{index.C8VoW8Ph.js → index.DNj5S4tY.js} +1 -1
  64. streamlit/static/static/js/{index.DZGCJu4I.js → index.DOY0ZriT.js} +1 -1
  65. streamlit/static/static/js/{index.DFT9nVK6.js → index.DSTThs-t.js} +5 -5
  66. streamlit/static/static/js/{index.CWAvu1Qu.js → index.DVtfSohT.js} +1 -1
  67. streamlit/static/static/js/{index.C0F0G-wg.js → index.DXQ_Fvpt.js} +1 -1
  68. streamlit/static/static/js/{index.BgCYNmov.js → index.DZE_91Ym.js} +1 -1
  69. streamlit/static/static/js/{index.95DldRtG.js → index.DxQuXlXH.js} +1 -1
  70. streamlit/static/static/js/{index.7S_sCSRx.js → index.Egabyb7u.js} +1 -1
  71. streamlit/static/static/js/{index.BU3d_gp1.js → index.Ft2Zxbhr.js} +1 -1
  72. streamlit/static/static/js/{index.gPUFpUqs.js → index.KuLql7H0.js} +1 -1
  73. streamlit/static/static/js/{index.B2fTHpId.js → index.XGft6-dq.js} +1 -1
  74. streamlit/static/static/js/{index.Tq2okoAU.js → index.euRMkmNi.js} +1 -1
  75. streamlit/static/static/js/{index.BNpEDrb2.js → index.r0gCrMFP.js} +1 -1
  76. streamlit/static/static/js/{input.Pz8Lwzsi.js → input.Cf97CQME.js} +1 -1
  77. streamlit/static/static/js/{main.BeiYkHRo.js → main.Ccuk53yQ.js} +1 -1
  78. streamlit/static/static/js/{memory.Dyx_JBbb.js → memory.Bng6Ij0g.js} +1 -1
  79. streamlit/static/static/js/{number-overlay-editor.NLIdF6b9.js → number-overlay-editor.CFLv-CWC.js} +1 -1
  80. streamlit/static/static/js/{pandasStylerUtils.DsNlDEqS.js → pandasStylerUtils.C2hcAKiv.js} +1 -1
  81. streamlit/static/static/js/{sandbox.bER7qtR1.js → sandbox.BXdeD-wA.js} +1 -1
  82. streamlit/static/static/js/{styled-components.DcoFBb7G.js → styled-components.Br04Ogac.js} +1 -1
  83. streamlit/static/static/js/{throttle.DOaQWO4U.js → throttle.mI9ItGre.js} +1 -1
  84. streamlit/static/static/js/{timepicker.RjHB2IT4.js → timepicker.poFdB0sd.js} +1 -1
  85. streamlit/static/static/js/{toConsumableArray.DFAIugL0.js → toConsumableArray.92-fANS-.js} +1 -1
  86. streamlit/static/static/js/uniqueId.BUj-C6GA.js +1 -0
  87. streamlit/static/static/js/{useBasicWidgetState.CTtyymrp.js → useBasicWidgetState.DzKGLAv_.js} +1 -1
  88. streamlit/static/static/js/{useIntlLocale.DG5haQGX.js → useIntlLocale.BMma2iiY.js} +1 -1
  89. streamlit/static/static/js/{useTextInputAutoExpand.Cnfcep1Z.js → useTextInputAutoExpand.DQbIhdma.js} +1 -1
  90. streamlit/static/static/js/{useUpdateUiValue.BWnXwmrp.js → useUpdateUiValue.Bk5OIXup.js} +1 -1
  91. streamlit/static/static/js/{useWaveformController.DozaayUB.js → useWaveformController.AH0ggRyc.js} +1 -1
  92. streamlit/static/static/js/{withCalculatedWidth.SNNFFxhJ.js → withCalculatedWidth.G5xJ-MbS.js} +1 -1
  93. streamlit/static/static/js/{withFullScreenWrapper.Dl2f8_gt.js → withFullScreenWrapper.rdRu6zZ4.js} +1 -1
  94. {streamlit-1.53.0.dist-info → streamlit-1.53.1.dist-info}/METADATA +1 -1
  95. {streamlit-1.53.0.dist-info → streamlit-1.53.1.dist-info}/RECORD +99 -99
  96. {streamlit-1.53.0.dist-info → streamlit-1.53.1.dist-info}/WHEEL +1 -1
  97. streamlit/static/static/js/index.BGgra9Bb.js +0 -188
  98. streamlit/static/static/js/uniqueId.DEvFPH9n.js +0 -1
  99. {streamlit-1.53.0.data → streamlit-1.53.1.data}/scripts/streamlit.cmd +0 -0
  100. {streamlit-1.53.0.dist-info → streamlit-1.53.1.dist-info}/entry_points.txt +0 -0
  101. {streamlit-1.53.0.dist-info → streamlit-1.53.1.dist-info}/top_level.txt +0 -0
@@ -349,6 +349,7 @@ class BaseSnowflakeConnection(BaseConnection["InternalSnowflakeConnection"]):
349
349
  """Closes the underlying Snowflake connection."""
350
350
  if self._raw_instance is not None:
351
351
  self._raw_instance.close()
352
+ self._raw_instance = None
352
353
 
353
354
 
354
355
  class SnowflakeConnection(BaseSnowflakeConnection):
@@ -290,6 +290,7 @@ def validate_and_sync_value_with_options(
290
290
  opt: Sequence[T],
291
291
  default_index: int | None,
292
292
  key: str | int | None,
293
+ format_func: Callable[[Any], str] = str,
293
294
  ) -> tuple[T | None, bool]:
294
295
  """Validate current value against options, resetting session state if invalid.
295
296
 
@@ -306,6 +307,11 @@ def validate_and_sync_value_with_options(
306
307
  The default index to reset to if value is invalid.
307
308
  key
308
309
  The widget key for session state updates.
310
+ format_func
311
+ Function to format options for comparison. Used to compare values by their
312
+ string representation instead of using == directly. This is necessary because
313
+ widget values are deepcopied, and for custom classes without __eq__, the
314
+ deepcopied instances would fail identity comparison.
309
315
 
310
316
  Returns
311
317
  -------
@@ -315,30 +321,50 @@ def validate_and_sync_value_with_options(
315
321
  if current_value is None:
316
322
  return current_value, False
317
323
 
318
- # Check if current value is still in the new options
319
- try:
320
- index_(opt, current_value)
321
- return current_value, False
322
- except ValueError:
323
- # Value not in options - reset to default
324
- if default_index is not None and len(opt) > 0:
325
- new_value: T | None = opt[default_index]
326
- else:
327
- new_value = None
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:
346
+ return current_value, False
347
+
348
+ # Value not in options - reset to default
349
+ if default_index is not None and len(opt) > 0:
350
+ new_value: T | None = opt[default_index]
351
+ else:
352
+ new_value = None
328
353
 
329
- if key is not None:
330
- # Update session_state so subsequent accesses in this run
331
- # return the corrected value. Use reset_state_value to avoid
332
- # the "cannot be modified after widget instantiated" error.
333
- get_session_state().reset_state_value(str(key), new_value)
354
+ if key is not None:
355
+ # Update session_state so subsequent accesses in this run
356
+ # return the corrected value. Use reset_state_value to avoid
357
+ # the "cannot be modified after widget instantiated" error.
358
+ get_session_state().reset_state_value(str(key), new_value)
334
359
 
335
- return new_value, True
360
+ return new_value, True
336
361
 
337
362
 
338
363
  def validate_and_sync_multiselect_value_with_options(
339
364
  current_values: list[T] | list[T | str],
340
365
  opt: Sequence[T],
341
366
  key: str | int | None,
367
+ format_func: Callable[[Any], str] = str,
342
368
  ) -> tuple[list[T] | list[T | str], bool]:
343
369
  """Validate multiselect values against options, syncing session state if needed.
344
370
 
@@ -356,6 +382,11 @@ def validate_and_sync_multiselect_value_with_options(
356
382
  The sequence of valid options.
357
383
  key
358
384
  The widget key for session state updates.
385
+ format_func
386
+ Function to format options for comparison. Used to compare values by their
387
+ string representation instead of using == directly. This is necessary because
388
+ widget values are deepcopied, and for custom classes without __eq__, the
389
+ deepcopied instances would fail identity comparison.
359
390
 
360
391
  Returns
361
392
  -------
@@ -365,13 +396,26 @@ def validate_and_sync_multiselect_value_with_options(
365
396
  if not current_values:
366
397
  return current_values, False
367
398
 
399
+ # Create a set of formatted options for O(1) lookup.
400
+ # We use format_func to compare values by their string representation
401
+ # instead of using == directly. This is necessary because widget values
402
+ # are deepcopied, and for custom classes without __eq__, the deepcopied
403
+ # instances would fail identity comparison.
404
+ formatted_options_set = {format_func(o) for o in opt}
405
+
368
406
  valid_values: list[T | str] = []
369
407
  for value in current_values:
370
408
  try:
371
- index_(opt, value)
409
+ formatted_value = format_func(value)
410
+ except Exception: # noqa: S112
411
+ # format_func failed on this value (e.g., a string value from a previous
412
+ # session when format_func expects an object with specific attributes).
413
+ # In this case, the value is definitely not valid since the current options
414
+ # can be formatted successfully.
415
+ continue
416
+
417
+ if formatted_value in formatted_options_set:
372
418
  valid_values.append(value)
373
- except ValueError: # noqa: PERF203
374
- pass
375
419
 
376
420
  if len(valid_values) == len(current_values):
377
421
  return current_values, False
@@ -82,6 +82,7 @@ class MultiSelectSerde(Generic[T]):
82
82
  formatted_options: list[str]
83
83
  formatted_option_to_option_index: dict[str, int]
84
84
  default_options_indices: list[int]
85
+ format_func: Callable[[Any], str]
85
86
 
86
87
  def __init__(
87
88
  self,
@@ -90,6 +91,7 @@ class MultiSelectSerde(Generic[T]):
90
91
  formatted_options: list[str],
91
92
  formatted_option_to_option_index: dict[str, int],
92
93
  default_options_indices: list[int] | None = None,
94
+ format_func: Callable[[Any], str] = str,
93
95
  ) -> None:
94
96
  """Initialize the MultiSelectSerde.
95
97
 
@@ -111,24 +113,45 @@ class MultiSelectSerde(Generic[T]):
111
113
  default_option_index : int or None, optional
112
114
  The index of the default option to use when no selection is made.
113
115
  If None, no default option is selected.
116
+ format_func : Callable[[Any], str], optional
117
+ Function to format options for comparison. Used to compare values by their
118
+ string representation instead of using == directly. This is necessary because
119
+ widget values are deepcopied, and for custom classes without __eq__, the
120
+ deepcopied instances would fail identity comparison.
114
121
  """
115
122
 
116
123
  self.options = options
117
124
  self.formatted_options = formatted_options
118
125
  self.formatted_option_to_option_index = formatted_option_to_option_index
119
126
  self.default_options_indices = default_options_indices or []
127
+ self.format_func = format_func
120
128
 
121
129
  def serialize(self, value: list[T | str] | list[T]) -> list[str]:
122
130
  converted_value = convert_anything_to_list(value)
123
131
  values: list[str] = []
124
132
  for v in converted_value:
133
+ # Use format_func to find the formatted option instead of using
134
+ # self.options.index(v) which relies on == comparison. This is necessary
135
+ # because widget values are deepcopied, and for custom classes without
136
+ # __eq__, the deepcopied instances would fail identity comparison.
125
137
  try:
126
- option_index = self.options.index(v)
127
- values.append(self.formatted_options[option_index])
128
- except ValueError: # noqa: PERF203
129
- # at this point we know that v is a string, otherwise
130
- # it would have been found in the options
131
- values.append(cast("str", v))
138
+ formatted_value = self.format_func(v)
139
+ except Exception:
140
+ # format_func failed (e.g., v is a string but format_func expects
141
+ # an object with specific attributes). Use str(v) to ensure we append
142
+ # a proper string, not the original object. This handles both cases:
143
+ # - v is already a string -> str(v) returns it unchanged
144
+ # - v is a custom object -> str(v) gives its string representation
145
+ values.append(str(v))
146
+ continue
147
+
148
+ if formatted_value in self.formatted_option_to_option_index:
149
+ values.append(formatted_value)
150
+ else:
151
+ # Value not found in options - it's likely a user-entered string
152
+ # (when accept_new_options=True) or an invalid value. Use the
153
+ # formatted string (not the original object) for type consistency.
154
+ values.append(formatted_value)
132
155
  return values
133
156
 
134
157
  def deserialize(self, ui_value: list[str] | None) -> list[T | str] | list[T]:
@@ -530,6 +553,7 @@ class MultiSelectMixin:
530
553
  formatted_options=formatted_options,
531
554
  formatted_option_to_option_index=formatted_option_to_option_index,
532
555
  default_options_indices=default_values,
556
+ format_func=format_func,
533
557
  )
534
558
 
535
559
  widget_state = register_widget(
@@ -560,7 +584,7 @@ class MultiSelectMixin:
560
584
  # previously selected values are no longer available.
561
585
  current_values, value_needs_reset = (
562
586
  validate_and_sync_multiselect_value_with_options(
563
- widget_state.value, indexable_options, key
587
+ widget_state.value, indexable_options, key, format_func
564
588
  )
565
589
  )
566
590
 
@@ -35,7 +35,6 @@ from streamlit.elements.lib.layout_utils import (
35
35
  )
36
36
  from streamlit.elements.lib.options_selector_utils import (
37
37
  create_mappings,
38
- index_,
39
38
  maybe_coerce_enum,
40
39
  validate_and_sync_value_with_options,
41
40
  )
@@ -79,6 +78,7 @@ class SelectboxSerde(Generic[T]):
79
78
  formatted_options: list[str]
80
79
  formatted_option_to_option_index: dict[str, int]
81
80
  default_option_index: int | None
81
+ format_func: Callable[[Any], str]
82
82
 
83
83
  def __init__(
84
84
  self,
@@ -87,6 +87,7 @@ class SelectboxSerde(Generic[T]):
87
87
  formatted_options: list[str],
88
88
  formatted_option_to_option_index: dict[str, int],
89
89
  default_option_index: int | None = None,
90
+ format_func: Callable[[Any], str] = str,
90
91
  ) -> None:
91
92
  """Initialize the SelectboxSerde.
92
93
 
@@ -108,33 +109,53 @@ class SelectboxSerde(Generic[T]):
108
109
  default_option_index : int or None, optional
109
110
  The index of the default option to use when no selection is made.
110
111
  If None, no default option is selected.
112
+ format_func : Callable[[Any], str], optional
113
+ Function to format options for comparison. Used to compare values by their
114
+ string representation instead of using == directly. This is necessary because
115
+ widget values are deepcopied, and for custom classes without __eq__, the
116
+ deepcopied instances would fail identity comparison.
111
117
  """
112
118
 
113
119
  self.options = options
114
120
  self.formatted_options = formatted_options
115
121
  self.formatted_option_to_option_index = formatted_option_to_option_index
116
122
  self.default_option_index = default_option_index
123
+ self.format_func = format_func
117
124
 
118
125
  def serialize(self, v: T | str | None) -> str | None:
119
126
  if v is None:
120
127
  return None
121
- if len(self.options) == 0:
122
- return ""
123
-
124
- # we don't check for isinstance(v, str) because this could lead to wrong
125
- # results if v is a string that is part of the options itself as it would
126
- # skip formatting in that case
128
+ # Note: We don't short-circuit for empty options here because
129
+ # accept_new_options=True allows user-entered values even with no options.
130
+ # The normal flow below handles this correctly.
131
+
132
+ # Use format_func to find the formatted option instead of using
133
+ # index_(self.options, v) which relies on == comparison. This is necessary
134
+ # because widget values are deepcopied, and for custom classes without
135
+ # __eq__, the deepcopied instances would fail identity comparison.
127
136
  try:
128
- option_index = index_(self.options, v)
129
- return self.formatted_options[option_index]
130
- except ValueError:
131
- # we know that v is a string, otherwise it would have been found in the
132
- # options
133
- return cast("str", v)
137
+ formatted_value = self.format_func(v)
138
+ except Exception:
139
+ # format_func failed (e.g., v is a string but format_func expects
140
+ # an object with specific attributes). Use str(v) to ensure we return
141
+ # a proper string, not the original object. This handles both cases:
142
+ # - v is already a string -> str(v) returns it unchanged
143
+ # - v is a custom object -> str(v) gives its string representation
144
+ return str(v)
145
+
146
+ if formatted_value in self.formatted_option_to_option_index:
147
+ return formatted_value
148
+ # Value not found in options - return the formatted string (not the original
149
+ # object) to maintain type consistency since serialize() must return str|None
150
+ return formatted_value
134
151
 
135
152
  def deserialize(self, ui_value: str | None) -> T | str | None:
136
- # check if the option is pointing to a generic option type T,
137
- # otherwise return the option itself
153
+ # Note: We don't short-circuit for empty options here because
154
+ # accept_new_options=True allows user-entered values even with no options.
155
+ # The normal flow below handles this: ui_value not in options -> return ui_value.
156
+
157
+ # Check if the option is pointing to a generic option type T,
158
+ # otherwise return the option itself.
138
159
  if ui_value is None:
139
160
  return (
140
161
  self.options[self.default_option_index]
@@ -583,6 +604,7 @@ class SelectboxMixin:
583
604
  formatted_options=formatted_options,
584
605
  formatted_option_to_option_index=formatted_option_to_option_index,
585
606
  default_option_index=index,
607
+ format_func=format_func,
586
608
  )
587
609
  widget_state = register_widget(
588
610
  selectbox_proto.id,
@@ -605,7 +627,7 @@ class SelectboxMixin:
605
627
  # This handles the case where options change dynamically and the
606
628
  # previously selected value is no longer available.
607
629
  current_value, value_needs_reset = validate_and_sync_value_with_options(
608
- widget_state.value, opt, index, key
630
+ widget_state.value, opt, index, key, format_func
609
631
  )
610
632
 
611
633
  if value_needs_reset or widget_state.value_changed:
@@ -37,7 +37,7 @@
37
37
  <script>
38
38
  window.prerenderReady = false
39
39
  </script>
40
- <script type="module" crossorigin src="./static/js/index.DFT9nVK6.js"></script>
40
+ <script type="module" crossorigin src="./static/js/index.DSTThs-t.js"></script>
41
41
  <link rel="stylesheet" crossorigin href="./static/css/index.BUP6fTcR.css">
42
42
  </head>
43
43
  <body>