streamlit-nightly 1.38.1.dev20240909__py2.py3-none-any.whl → 1.38.1.dev20240911__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.
- streamlit/__init__.py +1 -1
- streamlit/cli_util.py +59 -0
- streamlit/commands/experimental_query_params.py +33 -10
- streamlit/commands/page_config.py +6 -3
- streamlit/components/v1/custom_component.py +3 -5
- streamlit/config_option.py +3 -3
- streamlit/delta_generator.py +1 -1
- streamlit/elements/arrow.py +1 -1
- streamlit/elements/form.py +1 -1
- streamlit/elements/lib/built_in_chart_utils.py +1 -2
- streamlit/{color_util.py → elements/lib/color_util.py} +8 -20
- streamlit/elements/lib/options_selector_utils.py +191 -4
- streamlit/elements/lib/policies.py +1 -6
- streamlit/elements/lib/utils.py +11 -168
- streamlit/elements/map.py +6 -1
- streamlit/elements/plotly_chart.py +1 -1
- streamlit/elements/vega_charts.py +2 -2
- streamlit/elements/widgets/button.py +7 -5
- streamlit/elements/widgets/button_group.py +8 -8
- streamlit/elements/widgets/camera_input.py +1 -1
- streamlit/elements/widgets/chat.py +7 -5
- streamlit/elements/widgets/checkbox.py +1 -1
- streamlit/elements/widgets/color_picker.py +1 -1
- streamlit/elements/widgets/data_editor.py +1 -1
- streamlit/elements/widgets/file_uploader.py +1 -1
- streamlit/elements/widgets/multiselect.py +3 -5
- streamlit/elements/widgets/number_input.py +2 -2
- streamlit/elements/widgets/radio.py +3 -6
- streamlit/elements/widgets/select_slider.py +7 -5
- streamlit/elements/widgets/selectbox.py +3 -6
- streamlit/elements/widgets/slider.py +2 -2
- streamlit/elements/widgets/text_widgets.py +1 -1
- streamlit/elements/widgets/time_widgets.py +1 -1
- streamlit/errors.py +22 -0
- streamlit/file_util.py +4 -4
- streamlit/net_util.py +4 -2
- streamlit/runtime/app_session.py +1 -1
- streamlit/runtime/caching/cache_utils.py +5 -1
- streamlit/runtime/caching/storage/local_disk_cache_storage.py +2 -2
- streamlit/runtime/state/__init__.py +1 -5
- streamlit/runtime/state/common.py +1 -14
- streamlit/runtime/state/query_params.py +9 -2
- streamlit/runtime/state/widgets.py +0 -9
- streamlit/static/asset-manifest.json +20 -20
- streamlit/static/index.html +1 -1
- streamlit/static/static/js/1260.4017a70f.chunk.js +5 -0
- streamlit/static/static/js/2266.f3886a78.chunk.js +2 -0
- streamlit/static/static/js/{245.532167ed.chunk.js → 245.68a062da.chunk.js} +1 -1
- streamlit/static/static/js/{3156.0542d233.chunk.js → 3156.002c6ee0.chunk.js} +1 -1
- streamlit/static/static/js/3560.ce031236.chunk.js +1 -0
- streamlit/static/static/js/{4103.2a961369.chunk.js → 4103.d863052a.chunk.js} +1 -1
- streamlit/static/static/js/{5180.5e064ef1.chunk.js → 5180.e826dd46.chunk.js} +1 -1
- streamlit/static/static/js/5618.08be9e66.chunk.js +5 -0
- streamlit/static/static/js/{5625.0394ecdc.chunk.js → 5625.3a8dc81f.chunk.js} +1 -1
- streamlit/static/static/js/{5711.28939a95.chunk.js → 5711.3d376a33.chunk.js} +1 -1
- streamlit/static/static/js/6088.c137d543.chunk.js +1 -0
- streamlit/static/static/js/{6360.17e58a87.chunk.js → 6360.6d7cfa35.chunk.js} +1 -1
- streamlit/static/static/js/{7193.bc9bdd04.chunk.js → 7193.2594a18c.chunk.js} +1 -1
- streamlit/static/static/js/8166.11abccb8.chunk.js +1 -0
- streamlit/static/static/js/{8237.ed5d881b.chunk.js → 8237.210a5ac4.chunk.js} +1 -1
- streamlit/static/static/js/8815.0284d089.chunk.js +1 -0
- streamlit/static/static/js/9114.1ee3d4dd.chunk.js +1 -0
- streamlit/static/static/js/954.3cc76210.chunk.js +5 -0
- streamlit/static/static/js/{main.5d1dd93c.js → main.e270cec5.js} +2 -2
- streamlit/string_util.py +13 -5
- streamlit/time_util.py +3 -14
- streamlit/util.py +1 -127
- streamlit/watcher/local_sources_watcher.py +1 -1
- streamlit/web/bootstrap.py +2 -2
- streamlit/web/cli.py +2 -2
- {streamlit_nightly-1.38.1.dev20240909.dist-info → streamlit_nightly-1.38.1.dev20240911.dist-info}/METADATA +1 -1
- {streamlit_nightly-1.38.1.dev20240909.dist-info → streamlit_nightly-1.38.1.dev20240911.dist-info}/RECORD +82 -85
- streamlit/case_converters.py +0 -91
- streamlit/code_util.py +0 -90
- streamlit/constants.py +0 -19
- streamlit/static/static/js/1260.5ebd5704.chunk.js +0 -5
- streamlit/static/static/js/2266.48d2ebd0.chunk.js +0 -2
- streamlit/static/static/js/3560.17463b1c.chunk.js +0 -1
- streamlit/static/static/js/5618.6d42e995.chunk.js +0 -5
- streamlit/static/static/js/6088.00849717.chunk.js +0 -1
- streamlit/static/static/js/8166.0d1971ea.chunk.js +0 -1
- streamlit/static/static/js/8815.0b7dc879.chunk.js +0 -1
- streamlit/static/static/js/9114.c676bef4.chunk.js +0 -1
- streamlit/static/static/js/954.bf90fe19.chunk.js +0 -5
- /streamlit/{echo.py → commands/echo.py} +0 -0
- /streamlit/elements/{form_utils.py → lib/form_utils.py} +0 -0
- /streamlit/{js_number.py → elements/lib/js_number.py} +0 -0
- /streamlit/static/static/js/{2266.48d2ebd0.chunk.js.LICENSE.txt → 2266.f3886a78.chunk.js.LICENSE.txt} +0 -0
- /streamlit/static/static/js/{main.5d1dd93c.js.LICENSE.txt → main.e270cec5.js.LICENSE.txt} +0 -0
- /streamlit/{folder_black_list.py → watcher/folder_black_list.py} +0 -0
- {streamlit_nightly-1.38.1.dev20240909.data → streamlit_nightly-1.38.1.dev20240911.data}/scripts/streamlit.cmd +0 -0
- {streamlit_nightly-1.38.1.dev20240909.dist-info → streamlit_nightly-1.38.1.dev20240911.dist-info}/WHEEL +0 -0
- {streamlit_nightly-1.38.1.dev20240909.dist-info → streamlit_nightly-1.38.1.dev20240911.dist-info}/entry_points.txt +0 -0
- {streamlit_nightly-1.38.1.dev20240909.dist-info → streamlit_nightly-1.38.1.dev20240911.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
|
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 =
|
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
|
-
|
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
|
-
|
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,
|
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
|
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:
|
streamlit/config_option.py
CHANGED
@@ -21,8 +21,8 @@ import re
|
|
21
21
|
import textwrap
|
22
22
|
from typing import Any, Callable
|
23
23
|
|
24
|
-
from streamlit import
|
25
|
-
from streamlit.
|
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
|
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.
|
streamlit/delta_generator.py
CHANGED
@@ -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
|
streamlit/elements/arrow.py
CHANGED
@@ -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
|
streamlit/elements/form.py
CHANGED
@@ -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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
18
|
-
|
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,
|
@@ -124,11 +124,6 @@ def check_cache_replay_rules() -> None:
|
|
124
124
|
exception(CachedWidgetWarning())
|
125
125
|
|
126
126
|
|
127
|
-
_fragment_writes_widget_to_outside_error = (
|
128
|
-
"Fragments cannot write to elements outside of their container."
|
129
|
-
)
|
130
|
-
|
131
|
-
|
132
127
|
def check_fragment_path_policy(dg: DeltaGenerator):
|
133
128
|
"""Ensures that the current widget is not written outside of the
|
134
129
|
fragment's delta path.
|