streamlit-nightly 1.38.1.dev20240909__py2.py3-none-any.whl → 1.38.1.dev20240910__py2.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 (90) hide show
  1. streamlit/__init__.py +1 -1
  2. streamlit/cli_util.py +59 -0
  3. streamlit/commands/experimental_query_params.py +33 -10
  4. streamlit/commands/page_config.py +6 -3
  5. streamlit/components/v1/custom_component.py +3 -5
  6. streamlit/config_option.py +3 -3
  7. streamlit/delta_generator.py +1 -1
  8. streamlit/elements/arrow.py +1 -1
  9. streamlit/elements/form.py +1 -1
  10. streamlit/elements/lib/built_in_chart_utils.py +1 -2
  11. streamlit/{color_util.py → elements/lib/color_util.py} +8 -20
  12. streamlit/elements/lib/options_selector_utils.py +191 -4
  13. streamlit/elements/lib/policies.py +1 -1
  14. streamlit/elements/lib/utils.py +11 -168
  15. streamlit/elements/map.py +6 -1
  16. streamlit/elements/plotly_chart.py +1 -1
  17. streamlit/elements/vega_charts.py +2 -2
  18. streamlit/elements/widgets/button.py +7 -5
  19. streamlit/elements/widgets/button_group.py +8 -8
  20. streamlit/elements/widgets/camera_input.py +1 -1
  21. streamlit/elements/widgets/chat.py +7 -5
  22. streamlit/elements/widgets/checkbox.py +1 -1
  23. streamlit/elements/widgets/color_picker.py +1 -1
  24. streamlit/elements/widgets/data_editor.py +1 -1
  25. streamlit/elements/widgets/file_uploader.py +1 -1
  26. streamlit/elements/widgets/multiselect.py +3 -5
  27. streamlit/elements/widgets/number_input.py +2 -2
  28. streamlit/elements/widgets/radio.py +3 -6
  29. streamlit/elements/widgets/select_slider.py +7 -5
  30. streamlit/elements/widgets/selectbox.py +3 -6
  31. streamlit/elements/widgets/slider.py +2 -2
  32. streamlit/elements/widgets/text_widgets.py +1 -1
  33. streamlit/elements/widgets/time_widgets.py +1 -1
  34. streamlit/errors.py +22 -0
  35. streamlit/file_util.py +4 -4
  36. streamlit/net_util.py +4 -2
  37. streamlit/runtime/app_session.py +1 -1
  38. streamlit/runtime/caching/storage/local_disk_cache_storage.py +2 -2
  39. streamlit/runtime/state/__init__.py +1 -5
  40. streamlit/runtime/state/common.py +1 -14
  41. streamlit/runtime/state/query_params.py +9 -2
  42. streamlit/runtime/state/widgets.py +0 -9
  43. streamlit/static/asset-manifest.json +19 -19
  44. streamlit/static/index.html +1 -1
  45. streamlit/static/static/js/1260.4017a70f.chunk.js +5 -0
  46. streamlit/static/static/js/{245.532167ed.chunk.js → 245.68a062da.chunk.js} +1 -1
  47. streamlit/static/static/js/{3156.0542d233.chunk.js → 3156.002c6ee0.chunk.js} +1 -1
  48. streamlit/static/static/js/3560.ce031236.chunk.js +1 -0
  49. streamlit/static/static/js/{4103.2a961369.chunk.js → 4103.d863052a.chunk.js} +1 -1
  50. streamlit/static/static/js/{5180.5e064ef1.chunk.js → 5180.e826dd46.chunk.js} +1 -1
  51. streamlit/static/static/js/5618.08be9e66.chunk.js +5 -0
  52. streamlit/static/static/js/{5625.0394ecdc.chunk.js → 5625.3a8dc81f.chunk.js} +1 -1
  53. streamlit/static/static/js/{5711.28939a95.chunk.js → 5711.2f36e813.chunk.js} +1 -1
  54. streamlit/static/static/js/6088.c137d543.chunk.js +1 -0
  55. streamlit/static/static/js/{6360.17e58a87.chunk.js → 6360.6d7cfa35.chunk.js} +1 -1
  56. streamlit/static/static/js/{7193.bc9bdd04.chunk.js → 7193.2594a18c.chunk.js} +1 -1
  57. streamlit/static/static/js/8166.11abccb8.chunk.js +1 -0
  58. streamlit/static/static/js/{8237.ed5d881b.chunk.js → 8237.210a5ac4.chunk.js} +1 -1
  59. streamlit/static/static/js/8815.0284d089.chunk.js +1 -0
  60. streamlit/static/static/js/9114.1ee3d4dd.chunk.js +1 -0
  61. streamlit/static/static/js/954.3cc76210.chunk.js +5 -0
  62. streamlit/static/static/js/{main.5d1dd93c.js → main.7b7fe9ac.js} +2 -2
  63. streamlit/string_util.py +13 -5
  64. streamlit/time_util.py +3 -14
  65. streamlit/util.py +1 -127
  66. streamlit/watcher/local_sources_watcher.py +1 -1
  67. streamlit/web/bootstrap.py +2 -2
  68. streamlit/web/cli.py +2 -2
  69. {streamlit_nightly-1.38.1.dev20240909.dist-info → streamlit_nightly-1.38.1.dev20240910.dist-info}/METADATA +1 -1
  70. {streamlit_nightly-1.38.1.dev20240909.dist-info → streamlit_nightly-1.38.1.dev20240910.dist-info}/RECORD +79 -82
  71. streamlit/case_converters.py +0 -91
  72. streamlit/code_util.py +0 -90
  73. streamlit/constants.py +0 -19
  74. streamlit/static/static/js/1260.5ebd5704.chunk.js +0 -5
  75. streamlit/static/static/js/3560.17463b1c.chunk.js +0 -1
  76. streamlit/static/static/js/5618.6d42e995.chunk.js +0 -5
  77. streamlit/static/static/js/6088.00849717.chunk.js +0 -1
  78. streamlit/static/static/js/8166.0d1971ea.chunk.js +0 -1
  79. streamlit/static/static/js/8815.0b7dc879.chunk.js +0 -1
  80. streamlit/static/static/js/9114.c676bef4.chunk.js +0 -1
  81. streamlit/static/static/js/954.bf90fe19.chunk.js +0 -5
  82. /streamlit/{echo.py → commands/echo.py} +0 -0
  83. /streamlit/elements/{form_utils.py → lib/form_utils.py} +0 -0
  84. /streamlit/{js_number.py → elements/lib/js_number.py} +0 -0
  85. /streamlit/static/static/js/{main.5d1dd93c.js.LICENSE.txt → main.7b7fe9ac.js.LICENSE.txt} +0 -0
  86. /streamlit/{folder_black_list.py → watcher/folder_black_list.py} +0 -0
  87. {streamlit_nightly-1.38.1.dev20240909.data → streamlit_nightly-1.38.1.dev20240910.data}/scripts/streamlit.cmd +0 -0
  88. {streamlit_nightly-1.38.1.dev20240909.dist-info → streamlit_nightly-1.38.1.dev20240910.dist-info}/WHEEL +0 -0
  89. {streamlit_nightly-1.38.1.dev20240909.dist-info → streamlit_nightly-1.38.1.dev20240910.dist-info}/entry_points.txt +0 -0
  90. {streamlit_nightly-1.38.1.dev20240909.dist-info → streamlit_nightly-1.38.1.dev20240910.dist-info}/top_level.txt +0 -0
streamlit/__init__.py CHANGED
@@ -131,7 +131,7 @@ import streamlit.column_config as _column_config
131
131
  # disables implicit_reexport, where you use the respective command in the example_app.py
132
132
  # Streamlit app.
133
133
 
134
- from streamlit.echo import echo as echo
134
+ from streamlit.commands.echo import echo as echo
135
135
  from streamlit.commands.logo import logo as logo
136
136
  from streamlit.commands.navigation import navigation as navigation
137
137
  from streamlit.navigation.page import Page as Page
streamlit/cli_util.py CHANGED
@@ -16,6 +16,11 @@
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
+ import os
20
+ import subprocess
21
+
22
+ from streamlit import env_util, errors
23
+
19
24
 
20
25
  def print_to_cli(message: str, **kwargs) -> None:
21
26
  """Print a message to the terminal using click if available, else print
@@ -44,3 +49,57 @@ def style_for_cli(message: str, **kwargs) -> str:
44
49
  return click.style(message, **kwargs)
45
50
  except ImportError:
46
51
  return message
52
+
53
+
54
+ def _open_browser_with_webbrowser(url: str) -> None:
55
+ import webbrowser
56
+
57
+ webbrowser.open(url)
58
+
59
+
60
+ def _open_browser_with_command(command: str, url: str) -> None:
61
+ cmd_line = [command, url]
62
+ with open(os.devnull, "w") as devnull:
63
+ subprocess.Popen(cmd_line, stdout=devnull, stderr=subprocess.STDOUT)
64
+
65
+
66
+ def open_browser(url: str) -> None:
67
+ """Open a web browser pointing to a given URL.
68
+
69
+ We use this function instead of Python's `webbrowser` module because this
70
+ way we can capture stdout/stderr to avoid polluting the terminal with the
71
+ browser's messages. For example, Chrome always prints things like "Created
72
+ new window in existing browser session", and those get on the user's way.
73
+
74
+ url : str
75
+ The URL. Must include the protocol.
76
+
77
+ """
78
+ # Treat Windows separately because:
79
+ # 1. /dev/null doesn't exist.
80
+ # 2. subprocess.Popen(['start', url]) doesn't actually pop up the
81
+ # browser even though 'start url' works from the command prompt.
82
+ # Fun!
83
+ # Also, use webbrowser if we are on Linux and xdg-open is not installed.
84
+ #
85
+ # We don't use the webbrowser module on Linux and Mac because some browsers
86
+ # (ahem... Chrome) always print "Opening in existing browser session" to
87
+ # the terminal, which is spammy and annoying. So instead we start the
88
+ # browser ourselves and send all its output to /dev/null.
89
+
90
+ if env_util.IS_WINDOWS:
91
+ _open_browser_with_webbrowser(url)
92
+ return
93
+ if env_util.IS_LINUX_OR_BSD:
94
+ if env_util.is_executable_in_path("xdg-open"):
95
+ _open_browser_with_command("xdg-open", url)
96
+ return
97
+ _open_browser_with_webbrowser(url)
98
+ return
99
+ if env_util.IS_DARWIN:
100
+ _open_browser_with_command("open", url)
101
+ return
102
+
103
+ import platform
104
+
105
+ raise errors.Error(f'Cannot open browser in platform "{platform.system()}"')
@@ -17,16 +17,15 @@ from __future__ import annotations
17
17
  import urllib.parse as parse
18
18
  from typing import Any
19
19
 
20
- from streamlit import util
21
- from streamlit.constants import (
22
- EMBED_OPTIONS_QUERY_PARAM,
23
- EMBED_QUERY_PARAM,
24
- EMBED_QUERY_PARAMS_KEYS,
25
- )
26
20
  from streamlit.errors import StreamlitAPIException
27
21
  from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
28
22
  from streamlit.runtime.metrics_util import gather_metrics
29
23
  from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
24
+ from streamlit.runtime.state.query_params import (
25
+ EMBED_OPTIONS_QUERY_PARAM,
26
+ EMBED_QUERY_PARAM,
27
+ EMBED_QUERY_PARAMS_KEYS,
28
+ )
30
29
 
31
30
 
32
31
  @gather_metrics("experimental_get_query_params")
@@ -61,7 +60,7 @@ def get_query_params() -> dict[str, list[str]]:
61
60
  return {}
62
61
  ctx.mark_experimental_query_params_used()
63
62
  # Return new query params dict, but without embed, embed_options query params
64
- return util.exclude_keys_in_dict(
63
+ return _exclude_keys_in_dict(
65
64
  parse.parse_qs(ctx.query_string, keep_blank_values=True),
66
65
  keys_to_exclude=EMBED_QUERY_PARAMS_KEYS,
67
66
  )
@@ -107,6 +106,30 @@ def set_query_params(**query_params: Any) -> None:
107
106
  ctx.enqueue(msg)
108
107
 
109
108
 
109
+ def _exclude_keys_in_dict(
110
+ d: dict[str, Any], keys_to_exclude: list[str]
111
+ ) -> dict[str, Any]:
112
+ """Returns new object but without keys defined in keys_to_exclude"""
113
+ return {
114
+ key: value for key, value in d.items() if key.lower() not in keys_to_exclude
115
+ }
116
+
117
+
118
+ def _extract_key_query_params(
119
+ query_params: dict[str, list[str]], param_key: str
120
+ ) -> set[str]:
121
+ """Extracts key (case-insensitive) query params from Dict, and returns them as Set of str."""
122
+ return {
123
+ item.lower()
124
+ for sublist in [
125
+ [value.lower() for value in query_params[key]]
126
+ for key in query_params.keys()
127
+ if key.lower() == param_key and query_params.get(key)
128
+ ]
129
+ for item in sublist
130
+ }
131
+
132
+
110
133
  def _ensure_no_embed_params(
111
134
  query_params: dict[str, list[str] | str], query_string: str
112
135
  ) -> str:
@@ -114,7 +137,7 @@ def _ensure_no_embed_params(
114
137
  also makes sure old param values in query_string are preserved. Returns query_string : str.
115
138
  """
116
139
  # Get query params dict without embed, embed_options params
117
- query_params_without_embed = util.exclude_keys_in_dict(
140
+ query_params_without_embed = _exclude_keys_in_dict(
118
141
  query_params, keys_to_exclude=EMBED_QUERY_PARAMS_KEYS
119
142
  )
120
143
  if query_params != query_params_without_embed:
@@ -126,12 +149,12 @@ def _ensure_no_embed_params(
126
149
  current_embed_params = parse.urlencode(
127
150
  {
128
151
  EMBED_QUERY_PARAM: list(
129
- util.extract_key_query_params(
152
+ _extract_key_query_params(
130
153
  all_current_params, param_key=EMBED_QUERY_PARAM
131
154
  )
132
155
  ),
133
156
  EMBED_OPTIONS_QUERY_PARAM: list(
134
- util.extract_key_query_params(
157
+ _extract_key_query_params(
135
158
  all_current_params, param_key=EMBED_OPTIONS_QUERY_PARAM
136
159
  )
137
160
  ),
@@ -16,7 +16,7 @@ from __future__ import annotations
16
16
 
17
17
  import random
18
18
  from textwrap import dedent
19
- from typing import TYPE_CHECKING, Final, Literal, Mapping, Union, cast
19
+ from typing import TYPE_CHECKING, Any, Final, Literal, Mapping, Union, cast
20
20
 
21
21
  from typing_extensions import TypeAlias
22
22
 
@@ -33,7 +33,6 @@ from streamlit.runtime.metrics_util import gather_metrics
33
33
  from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
34
34
  from streamlit.string_util import is_emoji, validate_material_icon
35
35
  from streamlit.url_util import is_url
36
- from streamlit.util import lower_clean_dict_keys
37
36
 
38
37
  if TYPE_CHECKING:
39
38
  from typing_extensions import TypeGuard
@@ -80,6 +79,10 @@ ENG_EMOJIS: Final = [
80
79
  ]
81
80
 
82
81
 
82
+ def _lower_clean_dict_keys(dict: MenuItems) -> dict[str, Any]:
83
+ return {str(k).lower().strip(): v for k, v in dict.items()}
84
+
85
+
83
86
  def _get_favicon_string(page_icon: PageIcon) -> str:
84
87
  """Return the string to pass to the frontend to have it show
85
88
  the given PageIcon.
@@ -251,7 +254,7 @@ def set_page_config(
251
254
  msg.page_config_changed.initial_sidebar_state = pb_sidebar_state
252
255
 
253
256
  if menu_items is not None:
254
- lowercase_menu_items = cast(MenuItems, lower_clean_dict_keys(menu_items))
257
+ lowercase_menu_items = cast(MenuItems, _lower_clean_dict_keys(menu_items))
255
258
  validate_menu_items(lowercase_menu_items)
256
259
  menu_items_proto = msg.page_config_changed.menu_items
257
260
  set_menu_items_proto(lowercase_menu_items, menu_items_proto)
@@ -20,7 +20,7 @@ from typing import TYPE_CHECKING, Any
20
20
  from streamlit.components.types.base_custom_component import BaseCustomComponent
21
21
  from streamlit.dataframe_util import is_dataframe_like
22
22
  from streamlit.delta_generator_singletons import get_dg_singleton_instance
23
- from streamlit.elements.form_utils import current_form_id
23
+ from streamlit.elements.lib.form_utils import current_form_id
24
24
  from streamlit.elements.lib.policies import check_cache_replay_rules
25
25
  from streamlit.elements.lib.utils import compute_and_register_element_id
26
26
  from streamlit.errors import StreamlitAPIException
@@ -29,7 +29,7 @@ from streamlit.proto.Components_pb2 import SpecialArg
29
29
  from streamlit.proto.Element_pb2 import Element
30
30
  from streamlit.runtime.metrics_util import gather_metrics
31
31
  from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
32
- from streamlit.runtime.state import NoValue, register_widget
32
+ from streamlit.runtime.state import register_widget
33
33
  from streamlit.type_util import is_bytes_like, to_bytes
34
34
 
35
35
  if TYPE_CHECKING:
@@ -143,9 +143,7 @@ And if you're using Streamlit Cloud, add "pyarrow" to your requirements.txt."""
143
143
  "Could not convert component args to JSON", ex
144
144
  )
145
145
 
146
- def marshall_component(
147
- dg: DeltaGenerator, element: Element
148
- ) -> Any | type[NoValue]:
146
+ def marshall_component(dg: DeltaGenerator, element: Element) -> Any:
149
147
  element.component_instance.component_name = self.name
150
148
  element.component_instance.form_id = current_form_id(dg)
151
149
  if self.url is not None:
@@ -21,8 +21,8 @@ import re
21
21
  import textwrap
22
22
  from typing import Any, Callable
23
23
 
24
- from streamlit import util
25
- from streamlit.case_converters import to_snake_case
24
+ from streamlit.string_util import to_snake_case
25
+ from streamlit.util import repr_
26
26
 
27
27
 
28
28
  class ConfigOption:
@@ -194,7 +194,7 @@ class ConfigOption:
194
194
  self.set_value(default_val)
195
195
 
196
196
  def __repr__(self) -> str:
197
- return util.repr_(self)
197
+ return repr_(self)
198
198
 
199
199
  def __call__(self, get_val_func: Callable[[], Any]) -> ConfigOption:
200
200
  """Assign a function to compute the value for this option.
@@ -55,7 +55,6 @@ from streamlit.elements.doc_string import HelpMixin
55
55
  from streamlit.elements.empty import EmptyMixin
56
56
  from streamlit.elements.exception import ExceptionMixin
57
57
  from streamlit.elements.form import FormMixin
58
- from streamlit.elements.form_utils import FormData, current_form_id
59
58
  from streamlit.elements.graphviz_chart import GraphvizMixin
60
59
  from streamlit.elements.heading import HeadingMixin
61
60
  from streamlit.elements.html import HtmlMixin
@@ -63,6 +62,7 @@ from streamlit.elements.iframe import IframeMixin
63
62
  from streamlit.elements.image import ImageMixin
64
63
  from streamlit.elements.json import JsonMixin
65
64
  from streamlit.elements.layouts import LayoutsMixin
65
+ from streamlit.elements.lib.form_utils import FormData, current_form_id
66
66
  from streamlit.elements.map import MapMixin
67
67
  from streamlit.elements.markdown import MarkdownMixin
68
68
  from streamlit.elements.media import MediaMixin
@@ -31,7 +31,6 @@ from typing import (
31
31
  from typing_extensions import TypeAlias
32
32
 
33
33
  from streamlit import dataframe_util
34
- from streamlit.elements.form_utils import current_form_id
35
34
  from streamlit.elements.lib.column_config_utils import (
36
35
  INDEX_IDENTIFIER,
37
36
  ColumnConfigMappingInput,
@@ -41,6 +40,7 @@ from streamlit.elements.lib.column_config_utils import (
41
40
  update_column_config,
42
41
  )
43
42
  from streamlit.elements.lib.event_utils import AttributeDictionary
43
+ from streamlit.elements.lib.form_utils import current_form_id
44
44
  from streamlit.elements.lib.pandas_styler_utils import marshall_styler
45
45
  from streamlit.elements.lib.policies import check_widget_policies
46
46
  from streamlit.elements.lib.utils import Key, compute_and_register_element_id, to_key
@@ -16,7 +16,7 @@ from __future__ import annotations
16
16
  import textwrap
17
17
  from typing import TYPE_CHECKING, Literal, cast
18
18
 
19
- from streamlit.elements.form_utils import FormData, current_form_id, is_in_form
19
+ from streamlit.elements.lib.form_utils import FormData, current_form_id, is_in_form
20
20
  from streamlit.elements.lib.policies import (
21
21
  check_cache_replay_rules,
22
22
  check_session_state_rules,
@@ -34,7 +34,7 @@ from typing import (
34
34
  from typing_extensions import TypeAlias
35
35
 
36
36
  from streamlit import dataframe_util, type_util
37
- from streamlit.color_util import (
37
+ from streamlit.elements.lib.color_util import (
38
38
  Color,
39
39
  is_color_like,
40
40
  is_color_tuple_like,
@@ -89,7 +89,6 @@ class ChartType(Enum):
89
89
  # color legends in all instances, since the "size" circles vary in size based
90
90
  # on the data, and their container is top-aligned with the color container. But
91
91
  # through trial-and-error I found this value to be a good enough middle ground.
92
- # See e2e/scripts/st_arrow_scatter_chart.py for some alignment tests.
93
92
  #
94
93
  # NOTE #2: In theory, we could move COLOR_LEGEND_SETTINGS into
95
94
  # ArrowVegaLiteChart/CustomTheme.tsx, but this would impact existing behavior.
@@ -18,7 +18,7 @@ from typing import Any, Callable, Collection, Tuple, Union, cast
18
18
 
19
19
  from typing_extensions import TypeAlias
20
20
 
21
- from streamlit.errors import StreamlitAPIException
21
+ from streamlit.errors import StreamlitInvalidColorError
22
22
 
23
23
  # components go from 0.0 to 1.0
24
24
  # Supported by Pillow and pretty common.
@@ -86,7 +86,7 @@ def to_css_color(color: MaybeColor) -> Color:
86
86
  c4tuple = cast(MixedRGBAColorTuple, ctuple)
87
87
  return f"rgba({c4tuple[0]}, {c4tuple[1]}, {c4tuple[2]}, {c4tuple[3]})"
88
88
 
89
- raise InvalidColorException(color)
89
+ raise StreamlitInvalidColorError(color)
90
90
 
91
91
 
92
92
  def is_css_color_like(color: MaybeColor) -> bool:
@@ -192,18 +192,18 @@ def _to_color_tuple(
192
192
  b = color_hex[5:7]
193
193
  a = color_hex[7:9]
194
194
  else:
195
- raise InvalidColorException(color)
195
+ raise StreamlitInvalidColorError(color)
196
196
 
197
197
  try:
198
198
  color = int(r, 16), int(g, 16), int(b, 16), int(a, 16)
199
199
  except Exception as ex:
200
- raise InvalidColorException(color) from ex
200
+ raise StreamlitInvalidColorError(color) from ex
201
201
 
202
202
  if is_color_tuple_like(color):
203
203
  color_tuple = cast(ColorTuple, color)
204
204
  return _normalize_tuple(color_tuple, rgb_formatter, alpha_formatter)
205
205
 
206
- raise InvalidColorException(color)
206
+ raise StreamlitInvalidColorError(color)
207
207
 
208
208
 
209
209
  def _normalize_tuple(
@@ -233,7 +233,7 @@ def _normalize_tuple(
233
233
  alpha = alpha_formatter(color_4tuple[3], color_4tuple)
234
234
  return r, g, b, alpha
235
235
 
236
- raise InvalidColorException(color)
236
+ raise StreamlitInvalidColorError(color)
237
237
 
238
238
 
239
239
  def _int_formatter(component: float, color: MaybeColor) -> int:
@@ -247,7 +247,7 @@ def _int_formatter(component: float, color: MaybeColor) -> int:
247
247
  if isinstance(component, int):
248
248
  return min(255, max(component, 0))
249
249
 
250
- raise InvalidColorException(color)
250
+ raise StreamlitInvalidColorError(color)
251
251
 
252
252
 
253
253
  def _float_formatter(component: float, color: MaybeColor) -> float:
@@ -261,16 +261,4 @@ def _float_formatter(component: float, color: MaybeColor) -> float:
261
261
  if isinstance(component, float):
262
262
  return min(1.0, max(component, 0.0))
263
263
 
264
- raise InvalidColorException(color)
265
-
266
-
267
- class InvalidColorException(StreamlitAPIException):
268
- def __init__(self, color, *args):
269
- message = f"""This does not look like a valid color: {repr(color)}.
270
-
271
- Colors must be in one of the following formats:
272
-
273
- * Hex string with 3, 4, 6, or 8 digits. Example: `'#00ff00'`
274
- * List or tuple with 3 or 4 components. Example: `[1.0, 0.5, 0, 0.2]`
275
- """
276
- super().__init__(message, *args)
264
+ raise StreamlitInvalidColorError(color)
@@ -14,18 +14,48 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- from typing import (
18
- Any,
19
- Sequence,
20
- )
17
+ from enum import Enum, EnumMeta
18
+ from typing import Any, Final, Iterable, Sequence, TypeVar, overload
21
19
 
20
+ from streamlit import config, logger
22
21
  from streamlit.dataframe_util import OptionSequence, convert_anything_to_list
23
22
  from streamlit.errors import StreamlitAPIException
23
+ from streamlit.runtime.state.common import RegisterWidgetResult
24
24
  from streamlit.type_util import (
25
25
  T,
26
26
  check_python_comparable,
27
27
  )
28
28
 
29
+ _LOGGER: Final = logger.get_logger(__name__)
30
+
31
+ _FLOAT_EQUALITY_EPSILON: Final[float] = 0.000000000005
32
+ _Value = TypeVar("_Value")
33
+
34
+
35
+ def index_(iterable: Iterable[_Value], x: _Value) -> int:
36
+ """Return zero-based index of the first item whose value is equal to x.
37
+ Raises a ValueError if there is no such item.
38
+
39
+ We need a custom implementation instead of the built-in list .index() to
40
+ be compatible with NumPy array and Pandas Series.
41
+
42
+ Parameters
43
+ ----------
44
+ iterable : list, tuple, numpy.ndarray, pandas.Series
45
+ x : Any
46
+
47
+ Returns
48
+ -------
49
+ int
50
+ """
51
+ for i, value in enumerate(iterable):
52
+ if x == value:
53
+ return i
54
+ elif isinstance(value, float) and isinstance(x, float):
55
+ if abs(x - value) < _FLOAT_EQUALITY_EPSILON:
56
+ return i
57
+ raise ValueError(f"{str(x)} is not in iterable")
58
+
29
59
 
30
60
  def check_and_convert_to_indices(
31
61
  opt: Sequence[Any], default_values: Sequence[Any] | Any | None
@@ -58,3 +88,160 @@ def get_default_indices(
58
88
  default_indices = check_and_convert_to_indices(indexable_options, default)
59
89
  default_indices = default_indices if default_indices is not None else []
60
90
  return default_indices
91
+
92
+
93
+ E1 = TypeVar("E1", bound=Enum)
94
+ E2 = TypeVar("E2", bound=Enum)
95
+
96
+ _ALLOWED_ENUM_COERCION_CONFIG_SETTINGS = ("off", "nameOnly", "nameAndValue")
97
+
98
+
99
+ def _coerce_enum(from_enum_value: E1, to_enum_class: type[E2]) -> E1 | E2:
100
+ """Attempt to coerce an Enum value to another EnumMeta.
101
+
102
+ An Enum value of EnumMeta E1 is considered coercable to EnumType E2
103
+ if the EnumMeta __qualname__ match and the names of their members
104
+ match as well. (This is configurable in streamlist configs)
105
+ """
106
+ if not isinstance(from_enum_value, Enum):
107
+ raise ValueError(
108
+ f"Expected an Enum in the first argument. Got {type(from_enum_value)}"
109
+ )
110
+ if not isinstance(to_enum_class, EnumMeta):
111
+ raise ValueError(
112
+ f"Expected an EnumMeta/Type in the second argument. Got {type(to_enum_class)}"
113
+ )
114
+ if isinstance(from_enum_value, to_enum_class):
115
+ return from_enum_value # Enum is already a member, no coersion necessary
116
+
117
+ coercion_type = config.get_option("runner.enumCoercion")
118
+ if coercion_type not in _ALLOWED_ENUM_COERCION_CONFIG_SETTINGS:
119
+ raise StreamlitAPIException(
120
+ "Invalid value for config option runner.enumCoercion. "
121
+ f"Expected one of {_ALLOWED_ENUM_COERCION_CONFIG_SETTINGS}, "
122
+ f"but got '{coercion_type}'."
123
+ )
124
+ if coercion_type == "off":
125
+ return from_enum_value # do not attempt to coerce
126
+
127
+ # We now know this is an Enum AND the user has configured coercion enabled.
128
+ # Check if we do NOT meet the required conditions and log a failure message
129
+ # if that is the case.
130
+ from_enum_class = from_enum_value.__class__
131
+ if (
132
+ from_enum_class.__qualname__ != to_enum_class.__qualname__
133
+ or (
134
+ coercion_type == "nameOnly"
135
+ and set(to_enum_class._member_names_) != set(from_enum_class._member_names_)
136
+ )
137
+ or (
138
+ coercion_type == "nameAndValue"
139
+ and set(to_enum_class._value2member_map_)
140
+ != set(from_enum_class._value2member_map_)
141
+ )
142
+ ):
143
+ _LOGGER.debug("Failed to coerce %s to class %s", from_enum_value, to_enum_class)
144
+ return from_enum_value # do not attempt to coerce
145
+
146
+ # At this point we think the Enum is coercable, and we know
147
+ # E1 and E2 have the same member names. We convert from E1 to E2 using _name_
148
+ # (since user Enum subclasses can override the .name property in 3.11)
149
+ _LOGGER.debug("Coerced %s to class %s", from_enum_value, to_enum_class)
150
+ return to_enum_class[from_enum_value._name_]
151
+
152
+
153
+ def _extract_common_class_from_iter(iterable: Iterable[Any]) -> Any:
154
+ """Return the common class of all elements in a iterable if they share one.
155
+ Otherwise, return None."""
156
+ try:
157
+ inner_iter = iter(iterable)
158
+ first_class = type(next(inner_iter))
159
+ except StopIteration:
160
+ return None
161
+ if all(type(item) is first_class for item in inner_iter):
162
+ return first_class
163
+ return None
164
+
165
+
166
+ @overload
167
+ def maybe_coerce_enum(
168
+ register_widget_result: RegisterWidgetResult[Enum],
169
+ options: type[Enum],
170
+ opt_sequence: Sequence[Any],
171
+ ) -> RegisterWidgetResult[Enum]: ...
172
+
173
+
174
+ @overload
175
+ def maybe_coerce_enum(
176
+ register_widget_result: RegisterWidgetResult[T],
177
+ options: OptionSequence[T],
178
+ opt_sequence: Sequence[T],
179
+ ) -> RegisterWidgetResult[T]: ...
180
+
181
+
182
+ def maybe_coerce_enum(register_widget_result, options, opt_sequence):
183
+ """Maybe Coerce a RegisterWidgetResult with an Enum member value to
184
+ RegisterWidgetResult[option] if option is an EnumType, otherwise just return
185
+ the original RegisterWidgetResult."""
186
+
187
+ # If the value is not a Enum, return early
188
+ if not isinstance(register_widget_result.value, Enum):
189
+ return register_widget_result
190
+
191
+ coerce_class: EnumMeta | None
192
+ if isinstance(options, EnumMeta):
193
+ coerce_class = options
194
+ else:
195
+ coerce_class = _extract_common_class_from_iter(opt_sequence)
196
+ if coerce_class is None:
197
+ return register_widget_result
198
+
199
+ return RegisterWidgetResult(
200
+ _coerce_enum(register_widget_result.value, coerce_class),
201
+ register_widget_result.value_changed,
202
+ )
203
+
204
+
205
+ # slightly ugly typing because TypeVars with Generic Bounds are not supported
206
+ # (https://github.com/python/typing/issues/548)
207
+ @overload
208
+ def maybe_coerce_enum_sequence(
209
+ register_widget_result: RegisterWidgetResult[list[T]],
210
+ options: OptionSequence[T],
211
+ opt_sequence: Sequence[T],
212
+ ) -> RegisterWidgetResult[list[T]]: ...
213
+
214
+
215
+ @overload
216
+ def maybe_coerce_enum_sequence(
217
+ register_widget_result: RegisterWidgetResult[tuple[T, T]],
218
+ options: OptionSequence[T],
219
+ opt_sequence: Sequence[T],
220
+ ) -> RegisterWidgetResult[tuple[T, T]]: ...
221
+
222
+
223
+ def maybe_coerce_enum_sequence(register_widget_result, options, opt_sequence):
224
+ """Maybe Coerce a RegisterWidgetResult with a sequence of Enum members as value
225
+ to RegisterWidgetResult[Sequence[option]] if option is an EnumType, otherwise just return
226
+ the original RegisterWidgetResult."""
227
+
228
+ # If not all widget values are Enums, return early
229
+ if not all(isinstance(val, Enum) for val in register_widget_result.value):
230
+ return register_widget_result
231
+
232
+ # Extract the class to coerce
233
+ coerce_class: EnumMeta | None
234
+ if isinstance(options, EnumMeta):
235
+ coerce_class = options
236
+ else:
237
+ coerce_class = _extract_common_class_from_iter(opt_sequence)
238
+ if coerce_class is None:
239
+ return register_widget_result
240
+
241
+ # Return a new RegisterWidgetResult with the coerced enum values sequence
242
+ return RegisterWidgetResult(
243
+ type(register_widget_result.value)(
244
+ _coerce_enum(val, coerce_class) for val in register_widget_result.value
245
+ ),
246
+ register_widget_result.value_changed,
247
+ )
@@ -17,7 +17,7 @@ from __future__ import annotations
17
17
  from typing import TYPE_CHECKING, Any, Final, Sequence
18
18
 
19
19
  from streamlit import config, errors, logger, runtime
20
- from streamlit.elements.form_utils import is_in_form
20
+ from streamlit.elements.lib.form_utils import is_in_form
21
21
  from streamlit.errors import (
22
22
  StreamlitAPIWarning,
23
23
  StreamlitFragmentWidgetsNotAllowedOutsideError,