Mise à jour de Monitor.py et autres scripts

This commit is contained in:
Debian
2025-07-23 10:46:27 +02:00
parent 7081418ce0
commit 7de3e0fb50
8604 changed files with 2789953 additions and 295 deletions

View File

@@ -0,0 +1,13 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

View File

@@ -0,0 +1,311 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from dataclasses import dataclass
from textwrap import dedent
from typing import TYPE_CHECKING, Union, cast
from typing_extensions import TypeAlias
from streamlit.deprecation_util import (
make_deprecated_name_warning,
show_deprecation_warning,
)
from streamlit.elements.lib.file_uploader_utils import enforce_filename_restriction
from streamlit.elements.lib.form_utils import current_form_id
from streamlit.elements.lib.policies import (
check_widget_policies,
maybe_raise_label_warnings,
)
from streamlit.elements.lib.utils import (
Key,
LabelVisibility,
compute_and_register_element_id,
get_label_visibility_proto_value,
to_key,
)
from streamlit.elements.widgets.file_uploader import _get_upload_files
from streamlit.proto.AudioInput_pb2 import AudioInput as AudioInputProto
from streamlit.proto.Common_pb2 import FileUploaderState as FileUploaderStateProto
from streamlit.proto.Common_pb2 import UploadedFileInfo as UploadedFileInfoProto
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.runtime.state import (
WidgetArgs,
WidgetCallback,
WidgetKwargs,
register_widget,
)
from streamlit.runtime.uploaded_file_manager import DeletedFile, UploadedFile
if TYPE_CHECKING:
from streamlit.delta_generator import DeltaGenerator
SomeUploadedAudioFile: TypeAlias = Union[UploadedFile, DeletedFile, None]
@dataclass
class AudioInputSerde:
def serialize(
self,
audio_file: SomeUploadedAudioFile,
) -> FileUploaderStateProto:
state_proto = FileUploaderStateProto()
if audio_file is None or isinstance(audio_file, DeletedFile):
return state_proto
file_info: UploadedFileInfoProto = state_proto.uploaded_file_info.add()
file_info.file_id = audio_file.file_id
file_info.name = audio_file.name
file_info.size = audio_file.size
file_info.file_urls.CopyFrom(audio_file._file_urls)
return state_proto
def deserialize(
self, ui_value: FileUploaderStateProto | None, widget_id: str
) -> SomeUploadedAudioFile:
upload_files = _get_upload_files(ui_value)
if len(upload_files) == 0:
return_value = None
else:
return_value = upload_files[0]
if return_value is not None and not isinstance(return_value, DeletedFile):
enforce_filename_restriction(return_value.name, [".wav"])
return return_value
class AudioInputMixin:
@gather_metrics("audio_input")
def audio_input(
self,
label: str,
*,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> UploadedFile | None:
r"""Display a widget that returns an audio recording from the user's microphone.
Parameters
----------
label : str
A short label explaining to the user what this widget is used for.
The label can optionally contain GitHub-flavored Markdown of the
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
and Images. Images display like icons, with a max height equal to
the font height.
Unsupported Markdown elements are unwrapped so only their children
(text contents) render. Display unsupported elements as literal
characters by backslash-escaping them. E.g.,
``"1\. Not an ordered list"``.
See the ``body`` parameter of |st.markdown|_ for additional,
supported Markdown directives.
For accessibility reasons, you should never set an empty label, but
you can hide it with ``label_visibility`` if needed. In the future,
we may disallow empty labels by raising an exception.
.. |st.markdown| replace:: ``st.markdown``
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. No two widgets may have the same key.
help : str or None
A tooltip that gets displayed next to the widget label. Streamlit
only displays the tooltip when ``label_visibility="visible"``. If
this is ``None`` (default), no tooltip is displayed.
The tooltip can optionally contain GitHub-flavored Markdown,
including the Markdown directives described in the ``body``
parameter of ``st.markdown``.
on_change : callable
An optional callback invoked when this audio input's value
changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
disabled : bool
An optional boolean that disables the audio input if set to
``True``. Default is ``False``.
label_visibility : "visible", "hidden", or "collapsed"
The visibility of the label. The default is ``"visible"``. If this
is ``"hidden"``, Streamlit displays an empty spacer instead of the
label, which can help keep the widget alligned with other widgets.
If this is ``"collapsed"``, Streamlit displays no label or spacer.
Returns
-------
None or UploadedFile
The ``UploadedFile`` class is a subclass of ``BytesIO``, and
therefore is "file-like". This means you can pass an instance of it
anywhere a file is expected. The MIME type for the audio data is
``audio/wav``.
.. Note::
The resulting ``UploadedFile`` is subject to the size
limitation configured in ``server.maxUploadSize``. If you
expect large sound files, update the configuration option
appropriately.
Examples
--------
>>> import streamlit as st
>>>
>>> audio_value = st.audio_input("Record a voice message")
>>>
>>> if audio_value:
... st.audio(audio_value)
.. output::
https://doc-audio-input.streamlit.app/
height: 260px
"""
ctx = get_script_run_ctx()
return self._audio_input(
label=label,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
disabled=disabled,
label_visibility=label_visibility,
ctx=ctx,
)
@gather_metrics("experimental_audio_input")
def experimental_audio_input(
self,
label: str,
*,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> UploadedFile | None:
"""Deprecated alias for st.audio_input.
See the docstring for the widget's new name.
"""
show_deprecation_warning(
make_deprecated_name_warning(
"experimental_audio_input",
"audio_input",
"2025-01-01",
)
)
ctx = get_script_run_ctx()
return self._audio_input(
label=label,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
disabled=disabled,
label_visibility=label_visibility,
ctx=ctx,
)
def _audio_input(
self,
label: str,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
ctx: ScriptRunContext | None = None,
) -> UploadedFile | None:
key = to_key(key)
check_widget_policies(
self.dg,
key,
on_change,
default_value=None,
writes_allowed=False,
)
maybe_raise_label_warnings(label, label_visibility)
element_id = compute_and_register_element_id(
"audio_input",
user_key=key,
form_id=current_form_id(self.dg),
label=label,
help=help,
)
audio_input_proto = AudioInputProto()
audio_input_proto.id = element_id
audio_input_proto.label = label
audio_input_proto.form_id = current_form_id(self.dg)
audio_input_proto.disabled = disabled
audio_input_proto.label_visibility.value = get_label_visibility_proto_value(
label_visibility
)
if label and help is not None:
audio_input_proto.help = dedent(help)
serde = AudioInputSerde()
audio_input_state = register_widget(
audio_input_proto.id,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=serde.deserialize,
serializer=serde.serialize,
ctx=ctx,
value_type="file_uploader_state_value",
)
self.dg._enqueue("audio_input", audio_input_proto)
if isinstance(audio_input_state.value, DeletedFile):
return None
return audio_input_state.value
@property
def dg(self) -> DeltaGenerator:
"""Get our DeltaGenerator."""
return cast("DeltaGenerator", self)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,263 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from dataclasses import dataclass
from textwrap import dedent
from typing import TYPE_CHECKING, Union, cast
from typing_extensions import TypeAlias
from streamlit.elements.lib.file_uploader_utils import enforce_filename_restriction
from streamlit.elements.lib.form_utils import current_form_id
from streamlit.elements.lib.policies import (
check_widget_policies,
maybe_raise_label_warnings,
)
from streamlit.elements.lib.utils import (
Key,
LabelVisibility,
compute_and_register_element_id,
get_label_visibility_proto_value,
to_key,
)
from streamlit.elements.widgets.file_uploader import _get_upload_files
from streamlit.proto.CameraInput_pb2 import CameraInput as CameraInputProto
from streamlit.proto.Common_pb2 import FileUploaderState as FileUploaderStateProto
from streamlit.proto.Common_pb2 import UploadedFileInfo as UploadedFileInfoProto
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.runtime.state import (
WidgetArgs,
WidgetCallback,
WidgetKwargs,
register_widget,
)
from streamlit.runtime.uploaded_file_manager import DeletedFile, UploadedFile
if TYPE_CHECKING:
from streamlit.delta_generator import DeltaGenerator
SomeUploadedSnapshotFile: TypeAlias = Union[UploadedFile, DeletedFile, None]
@dataclass
class CameraInputSerde:
def serialize(
self,
snapshot: SomeUploadedSnapshotFile,
) -> FileUploaderStateProto:
state_proto = FileUploaderStateProto()
if snapshot is None or isinstance(snapshot, DeletedFile):
return state_proto
file_info: UploadedFileInfoProto = state_proto.uploaded_file_info.add()
file_info.file_id = snapshot.file_id
file_info.name = snapshot.name
file_info.size = snapshot.size
file_info.file_urls.CopyFrom(snapshot._file_urls)
return state_proto
def deserialize(
self, ui_value: FileUploaderStateProto | None, widget_id: str
) -> SomeUploadedSnapshotFile:
upload_files = _get_upload_files(ui_value)
if len(upload_files) == 0:
return_value = None
else:
return_value = upload_files[0]
if return_value is not None and not isinstance(return_value, DeletedFile):
enforce_filename_restriction(return_value.name, [".jpg"])
return return_value
class CameraInputMixin:
@gather_metrics("camera_input")
def camera_input(
self,
label: str,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> UploadedFile | None:
r"""Display a widget that returns pictures from the user's webcam.
Parameters
----------
label : str
A short label explaining to the user what this widget is used for.
The label can optionally contain GitHub-flavored Markdown of the
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
and Images. Images display like icons, with a max height equal to
the font height.
Unsupported Markdown elements are unwrapped so only their children
(text contents) render. Display unsupported elements as literal
characters by backslash-escaping them. E.g.,
``"1\. Not an ordered list"``.
See the ``body`` parameter of |st.markdown|_ for additional,
supported Markdown directives.
For accessibility reasons, you should never set an empty label, but
you can hide it with ``label_visibility`` if needed. In the future,
we may disallow empty labels by raising an exception.
.. |st.markdown| replace:: ``st.markdown``
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. No two widgets may have the same key.
help : str or None
A tooltip that gets displayed next to the widget label. Streamlit
only displays the tooltip when ``label_visibility="visible"``. If
this is ``None`` (default), no tooltip is displayed.
The tooltip can optionally contain GitHub-flavored Markdown,
including the Markdown directives described in the ``body``
parameter of ``st.markdown``.
on_change : callable
An optional callback invoked when this camera_input's value
changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
disabled : bool
An optional boolean that disables the camera input if set to
``True``. Default is ``False``.
label_visibility : "visible", "hidden", or "collapsed"
The visibility of the label. The default is ``"visible"``. If this
is ``"hidden"``, Streamlit displays an empty spacer instead of the
label, which can help keep the widget alligned with other widgets.
If this is ``"collapsed"``, Streamlit displays no label or spacer.
Returns
-------
None or UploadedFile
The UploadedFile class is a subclass of BytesIO, and therefore is
"file-like". This means you can pass an instance of it anywhere a
file is expected.
Examples
--------
>>> import streamlit as st
>>>
>>> enable = st.checkbox("Enable camera")
>>> picture = st.camera_input("Take a picture", disabled=not enable)
>>>
>>> if picture:
... st.image(picture)
.. output::
https://doc-camera-input.streamlit.app/
height: 600px
"""
ctx = get_script_run_ctx()
return self._camera_input(
label=label,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
disabled=disabled,
label_visibility=label_visibility,
ctx=ctx,
)
def _camera_input(
self,
label: str,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
ctx: ScriptRunContext | None = None,
) -> UploadedFile | None:
key = to_key(key)
check_widget_policies(
self.dg,
key,
on_change,
default_value=None,
writes_allowed=False,
)
maybe_raise_label_warnings(label, label_visibility)
element_id = compute_and_register_element_id(
"camera_input",
user_key=key,
form_id=current_form_id(self.dg),
label=label,
help=help,
)
camera_input_proto = CameraInputProto()
camera_input_proto.id = element_id
camera_input_proto.label = label
camera_input_proto.form_id = current_form_id(self.dg)
camera_input_proto.disabled = disabled
camera_input_proto.label_visibility.value = get_label_visibility_proto_value(
label_visibility
)
if help is not None:
camera_input_proto.help = dedent(help)
serde = CameraInputSerde()
camera_input_state = register_widget(
camera_input_proto.id,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=serde.deserialize,
serializer=serde.serialize,
ctx=ctx,
value_type="file_uploader_state_value",
)
self.dg._enqueue("camera_input", camera_input_proto)
if isinstance(camera_input_state.value, DeletedFile):
return None
return camera_input_state.value
@property
def dg(self) -> DeltaGenerator:
"""Get our DeltaGenerator."""
return cast("DeltaGenerator", self)

View File

@@ -0,0 +1,647 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from collections.abc import Iterator, MutableMapping, Sequence
from dataclasses import dataclass
from enum import Enum
from typing import (
TYPE_CHECKING,
Any,
Literal,
cast,
overload,
)
from streamlit import config, runtime
from streamlit.delta_generator_singletons import get_dg_singleton_instance
from streamlit.elements.lib.file_uploader_utils import (
enforce_filename_restriction,
normalize_upload_file_type,
)
from streamlit.elements.lib.form_utils import is_in_form
from streamlit.elements.lib.image_utils import AtomicImage, WidthBehavior, image_to_url
from streamlit.elements.lib.policies import check_widget_policies
from streamlit.elements.lib.utils import (
Key,
compute_and_register_element_id,
get_chat_input_accept_file_proto_value,
save_for_app_testing,
to_key,
)
from streamlit.errors import StreamlitAPIException
from streamlit.proto.Block_pb2 import Block as BlockProto
from streamlit.proto.ChatInput_pb2 import ChatInput as ChatInputProto
from streamlit.proto.Common_pb2 import ChatInputValue as ChatInputValueProto
from streamlit.proto.Common_pb2 import FileUploaderState as FileUploaderStateProto
from streamlit.proto.RootContainer_pb2 import RootContainer
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
from streamlit.runtime.state import (
WidgetArgs,
WidgetCallback,
WidgetKwargs,
register_widget,
)
from streamlit.runtime.uploaded_file_manager import DeletedFile, UploadedFile
from streamlit.string_util import is_emoji, validate_material_icon
if TYPE_CHECKING:
from streamlit.delta_generator import DeltaGenerator
@dataclass
class ChatInputValue(MutableMapping[str, Any]):
text: str
files: list[UploadedFile]
def __len__(self) -> int:
return len(vars(self))
def __iter__(self) -> Iterator[str]:
return iter(vars(self))
def __getitem__(self, item: str) -> str | list[UploadedFile]:
try:
return getattr(self, item) # type: ignore[no-any-return]
except AttributeError:
raise KeyError(f"Invalid key: {item}") from None
def __setitem__(self, key: str, value: Any) -> None:
setattr(self, key, value)
def __delitem__(self, key: str) -> None:
try:
delattr(self, key)
except AttributeError:
raise KeyError(f"Invalid key: {key}") from None
def to_dict(self) -> dict[str, str | list[UploadedFile]]:
return vars(self)
class PresetNames(str, Enum):
USER = "user"
ASSISTANT = "assistant"
AI = "ai" # Equivalent to assistant
HUMAN = "human" # Equivalent to user
def _process_avatar_input(
avatar: str | AtomicImage | None, delta_path: str
) -> tuple[BlockProto.ChatMessage.AvatarType.ValueType, str]:
"""Detects the avatar type and prepares the avatar data for the frontend.
Parameters
----------
avatar :
The avatar that was provided by the user.
delta_path : str
The delta path is used as media ID when a local image is served via the media
file manager.
Returns
-------
Tuple[AvatarType, str]
The detected avatar type and the prepared avatar data.
"""
AvatarType = BlockProto.ChatMessage.AvatarType
if avatar is None:
return AvatarType.ICON, ""
elif isinstance(avatar, str) and avatar in {item.value for item in PresetNames}:
# On the frontend, we only support "assistant" and "user" for the avatar.
return (
AvatarType.ICON,
(
"assistant"
if avatar in [PresetNames.AI, PresetNames.ASSISTANT]
else "user"
),
)
elif isinstance(avatar, str) and is_emoji(avatar):
return AvatarType.EMOJI, avatar
elif isinstance(avatar, str) and avatar.startswith(":material"):
return AvatarType.ICON, validate_material_icon(avatar)
else:
try:
return AvatarType.IMAGE, image_to_url(
avatar,
width=WidthBehavior.ORIGINAL,
clamp=False,
channels="RGB",
output_format="auto",
image_id=delta_path,
)
except Exception as ex:
raise StreamlitAPIException(
"Failed to load the provided avatar value as an image."
) from ex
def _pop_upload_files(
files_value: FileUploaderStateProto | None,
) -> list[UploadedFile]:
if files_value is None:
return []
ctx = get_script_run_ctx()
if ctx is None:
return []
uploaded_file_info = files_value.uploaded_file_info
if len(uploaded_file_info) == 0:
return []
file_recs_list = ctx.uploaded_file_mgr.get_files(
session_id=ctx.session_id,
file_ids=[f.file_id for f in uploaded_file_info],
)
file_recs = {f.file_id: f for f in file_recs_list}
collected_files: list[UploadedFile] = []
for f in uploaded_file_info:
maybe_file_rec = file_recs.get(f.file_id)
if maybe_file_rec is not None:
uploaded_file = UploadedFile(maybe_file_rec, f.file_urls)
collected_files.append(uploaded_file)
if hasattr(ctx.uploaded_file_mgr, "remove_file"):
ctx.uploaded_file_mgr.remove_file(
session_id=ctx.session_id,
file_id=f.file_id,
)
return collected_files
@dataclass
class ChatInputSerde:
accept_files: bool = False
allowed_types: Sequence[str] | None = None
def deserialize(
self,
ui_value: ChatInputValueProto | None,
widget_id: str = "",
) -> str | ChatInputValue | None:
if ui_value is None or not ui_value.HasField("data"):
return None
if not self.accept_files:
return ui_value.data
else:
uploaded_files = _pop_upload_files(ui_value.file_uploader_state)
for file in uploaded_files:
if self.allowed_types and not isinstance(file, DeletedFile):
enforce_filename_restriction(file.name, self.allowed_types)
return ChatInputValue(
text=ui_value.data,
files=uploaded_files,
)
def serialize(self, v: str | None) -> ChatInputValueProto:
return ChatInputValueProto(data=v)
class ChatMixin:
@gather_metrics("chat_message")
def chat_message(
self,
name: Literal["user", "assistant", "ai", "human"] | str,
*,
avatar: Literal["user", "assistant"] | str | AtomicImage | None = None,
) -> DeltaGenerator:
"""Insert a chat message container.
To add elements to the returned container, you can use ``with`` notation
(preferred) or just call methods directly on the returned object. See the
examples below.
Parameters
----------
name : "user", "assistant", "ai", "human", or str
The name of the message author. Can be "human"/"user" or
"ai"/"assistant" to enable preset styling and avatars.
Currently, the name is not shown in the UI but is only set as an
accessibility label. For accessibility reasons, you should not use
an empty string.
avatar : Anything supported by st.image (except list), str, or None
The avatar shown next to the message.
If ``avatar`` is ``None`` (default), the icon will be determined
from ``name`` as follows:
- If ``name`` is ``"user"`` or ``"human"``, the message will have a
default user icon.
- If ``name`` is ``"ai"`` or ``"assistant"``, the message will have
a default bot icon.
- For all other values of ``name``, the message will show the first
letter of the name.
In addition to the types supported by |st.image|_ (except list),
the following strings are valid:
- A single-character emoji. For example, you can set ``avatar="🧑‍💻"``
or ``avatar="🦖"``. Emoji short codes are not supported.
- An icon from the Material Symbols library (rounded style) in the
format ``":material/icon_name:"`` where "icon_name" is the name
of the icon in snake case.
For example, ``icon=":material/thumb_up:"`` will display the
Thumb Up icon. Find additional icons in the `Material Symbols \
<https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded>`_
font library.
.. |st.image| replace:: ``st.image``
.. _st.image: https://docs.streamlit.io/develop/api-reference/media/st.image
Returns
-------
Container
A single container that can hold multiple elements.
Examples
--------
You can use ``with`` notation to insert any element into an expander
>>> import streamlit as st
>>> import numpy as np
>>>
>>> with st.chat_message("user"):
... st.write("Hello 👋")
... st.line_chart(np.random.randn(30, 3))
.. output ::
https://doc-chat-message-user.streamlit.app/
height: 450px
Or you can just call methods directly in the returned objects:
>>> import streamlit as st
>>> import numpy as np
>>>
>>> message = st.chat_message("assistant")
>>> message.write("Hello human")
>>> message.bar_chart(np.random.randn(30, 3))
.. output ::
https://doc-chat-message-user1.streamlit.app/
height: 450px
"""
if name is None:
raise StreamlitAPIException(
"The author name is required for a chat message, please set it via the parameter `name`."
)
if avatar is None and (
name.lower() in {item.value for item in PresetNames} or is_emoji(name)
):
# For selected labels, we are mapping the label to an avatar
avatar = name.lower()
avatar_type, converted_avatar = _process_avatar_input(
avatar, self.dg._get_delta_path_str()
)
message_container_proto = BlockProto.ChatMessage()
message_container_proto.name = name
message_container_proto.avatar = converted_avatar
message_container_proto.avatar_type = avatar_type
block_proto = BlockProto()
block_proto.allow_empty = True
block_proto.chat_message.CopyFrom(message_container_proto)
return self.dg._block(block_proto=block_proto)
@overload
def chat_input(
self,
placeholder: str = "Your message",
*,
key: Key | None = None,
max_chars: int | None = None,
accept_file: Literal[False] = False,
file_type: str | Sequence[str] | None = None,
disabled: bool = False,
on_submit: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
) -> str | None: ...
@overload
def chat_input(
self,
placeholder: str = "Your message",
*,
key: Key | None = None,
max_chars: int | None = None,
accept_file: Literal[True, "multiple"],
file_type: str | Sequence[str] | None = None,
disabled: bool = False,
on_submit: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
) -> ChatInputValue | None: ...
@gather_metrics("chat_input")
def chat_input(
self,
placeholder: str = "Your message",
*,
key: Key | None = None,
max_chars: int | None = None,
accept_file: bool | Literal["multiple"] = False,
file_type: str | Sequence[str] | None = None,
disabled: bool = False,
on_submit: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
) -> str | ChatInputValue | None:
"""Display a chat input widget.
Parameters
----------
placeholder : str
A placeholder text shown when the chat input is empty. This
defaults to ``"Your message"``. For accessibility reasons, you
should not use an empty string.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget based on
its content. No two widgets may have the same key.
max_chars : int or None
The maximum number of characters that can be entered. If this is
``None`` (default), there will be no maximum.
accept_file : bool or str
Whether the chat input should accept files. This can be one of the
following values:
- ``False`` (default): No files are accepted and the user can only
submit a message.
- ``True``: The user can add a single file to their submission.
- ``"multiple"``: The user can add multiple files to their
submission.
When the widget is configured to accept files, the accepted file
types can be configured with the ``file_type`` parameter.
By default, uploaded files are limited to 200 MB each. You can
configure this using the ``server.maxUploadSize`` config option.
For more information on how to set config options, see
|config.toml|_.
.. |config.toml| replace:: ``config.toml``
.. _config.toml: https://docs.streamlit.io/develop/api-reference/configuration/config.toml
file_type : str, Sequence[str], or None
The allowed file extension(s) for uploaded files. This can be one
of the following types:
- ``None`` (default): All file extensions are allowed.
- A string: A single file extension is allowed. For example, to
only accept CSV files, use ``"csv"``.
- A sequence of strings: Multiple file extensions are allowed. For
example, to only accept JPG/JPEG and PNG files, use
``["jpg", "jpeg", "png"]``.
disabled : bool
Whether the chat input should be disabled. This defaults to
``False``.
on_submit : callable
An optional callback invoked when the chat input's value is submitted.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
Returns
-------
None, str, or dict-like
The user's submission. This is one of the following types:
- ``None``: If the user didn't submit a message or file in the last
rerun, the widget returns ``None``.
- A string: When the widget is not configured to accept files and
the user submitted a message in the last rerun, the widget
returns the user's message as a string.
- A dict-like object: When the widget is configured to accept files
and the user submitted a message and/or file(s) in the last
rerun, the widget returns a dict-like object with two attributes,
``text`` and ``files``.
When the widget is configured to accept files and the user submits
something in the last rerun, you can access the user's submission
with key or attribute notation from the dict-like object. This is
shown in Example 3 below.
The ``text`` attribute holds a string, which is the user's message.
This is an empty string if the user only submitted one or more
files.
The ``files`` attribute holds a list of UploadedFile objects.
The list is empty if the user only submitted a message. Unlike
``st.file_uploader``, this attribute always returns a list, even
when the widget is configured to accept only one file at a time.
The UploadedFile class is a subclass of BytesIO, and therefore is
"file-like". This means you can pass an instance of it anywhere a
file is expected.
Examples
--------
**Example 1: Pin the the chat input widget to the bottom of your app**
When ``st.chat_input`` is used in the main body of an app, it will be
pinned to the bottom of the page.
>>> import streamlit as st
>>>
>>> prompt = st.chat_input("Say something")
>>> if prompt:
... st.write(f"User has sent the following prompt: {prompt}")
.. output ::
https://doc-chat-input.streamlit.app/
height: 350px
**Example 2: Use the chat input widget inline**
The chat input can also be used inline by nesting it inside any layout
container (container, columns, tabs, sidebar, etc) or fragment. Create
chat interfaces embedded next to other content, or have multiple
chatbots!
>>> import streamlit as st
>>>
>>> with st.sidebar:
>>> messages = st.container(height=300)
>>> if prompt := st.chat_input("Say something"):
>>> messages.chat_message("user").write(prompt)
>>> messages.chat_message("assistant").write(f"Echo: {prompt}")
.. output ::
https://doc-chat-input-inline.streamlit.app/
height: 350px
**Example 3: Let users upload files**
When you configure your chat input widget to allow file attachments, it
will return a dict-like object when the user sends a submission. You
can access the user's message through the ``text`` attribute of this
dictionary. You can access a list of the user's submitted file(s)
through the ``files`` attribute. Similar to ``st.session_state``, you
can use key or attribute notation.
>>> import streamlit as st
>>>
>>> prompt = st.chat_input(
>>> "Say something and/or attach an image",
>>> accept_file=True,
>>> file_type=["jpg", "jpeg", "png"],
>>> )
>>> if prompt and prompt.text:
>>> st.markdown(prompt.text)
>>> if prompt and prompt["files"]:
>>> st.image(prompt["files"][0])
.. output ::
https://doc-chat-input-file-uploader.streamlit.app/
height: 350px
"""
# We default to an empty string here and disallow user choice intentionally
default = ""
key = to_key(key)
check_widget_policies(
self.dg,
key,
on_submit,
default_value=default,
writes_allowed=False,
)
if accept_file not in {True, False, "multiple"}:
raise StreamlitAPIException(
"The `accept_file` parameter must be a boolean or 'multiple'."
)
ctx = get_script_run_ctx()
element_id = compute_and_register_element_id(
"chat_input",
user_key=key,
# chat_input is not allowed to be used in a form.
form_id=None,
placeholder=placeholder,
max_chars=max_chars,
accept_file=accept_file,
file_type=file_type,
)
if file_type:
file_type = normalize_upload_file_type(file_type)
# It doesn't make sense to create a chat input inside a form.
# We throw an error to warn the user about this.
# We omit this check for scripts running outside streamlit, because
# they will have no script_run_ctx.
if runtime.exists():
if is_in_form(self.dg):
raise StreamlitAPIException(
"`st.chat_input()` can't be used in a `st.form()`."
)
# Determine the position of the chat input:
# Use bottom position if chat input is within the main container
# either directly or within a vertical container. If it has any
# other container types as parents, we use inline position.
ancestor_block_types = set(self.dg._active_dg._ancestor_block_types)
if (
self.dg._active_dg._root_container == RootContainer.MAIN
and not ancestor_block_types
):
position = "bottom"
else:
position = "inline"
chat_input_proto = ChatInputProto()
chat_input_proto.id = element_id
chat_input_proto.placeholder = str(placeholder)
if max_chars is not None:
chat_input_proto.max_chars = max_chars
chat_input_proto.default = default
chat_input_proto.accept_file = get_chat_input_accept_file_proto_value(
accept_file
)
chat_input_proto.file_type[:] = file_type if file_type is not None else []
chat_input_proto.max_upload_size_mb = config.get_option("server.maxUploadSize")
serde = ChatInputSerde(
accept_files=bool(accept_file),
allowed_types=file_type,
)
widget_state = register_widget( # type: ignore[misc]
chat_input_proto.id,
on_change_handler=on_submit,
args=args,
kwargs=kwargs,
deserializer=serde.deserialize,
serializer=serde.serialize,
ctx=ctx,
value_type="chat_input_value",
)
chat_input_proto.disabled = disabled
if widget_state.value_changed and widget_state.value is not None:
chat_input_proto.value = widget_state.value
chat_input_proto.set_value = True
if ctx:
save_for_app_testing(ctx, element_id, widget_state.value)
if position == "bottom":
# We need to enqueue the chat input into the bottom container
# instead of the currently active dg.
get_dg_singleton_instance().bottom_dg._enqueue(
"chat_input", chat_input_proto
)
else:
self.dg._enqueue("chat_input", chat_input_proto)
return widget_state.value if not widget_state.value_changed else None
@property
def dg(self) -> DeltaGenerator:
"""Get our DeltaGenerator."""
return cast("DeltaGenerator", self)

View File

@@ -0,0 +1,352 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from dataclasses import dataclass
from textwrap import dedent
from typing import TYPE_CHECKING, cast
from streamlit.elements.lib.form_utils import current_form_id
from streamlit.elements.lib.policies import (
check_widget_policies,
maybe_raise_label_warnings,
)
from streamlit.elements.lib.utils import (
Key,
LabelVisibility,
compute_and_register_element_id,
get_label_visibility_proto_value,
to_key,
)
from streamlit.proto.Checkbox_pb2 import Checkbox as CheckboxProto
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.runtime.state import (
WidgetArgs,
WidgetCallback,
WidgetKwargs,
register_widget,
)
if TYPE_CHECKING:
from streamlit.delta_generator import DeltaGenerator
@dataclass
class CheckboxSerde:
value: bool
def serialize(self, v: bool) -> bool:
return bool(v)
def deserialize(self, ui_value: bool | None, widget_id: str = "") -> bool:
return bool(ui_value if ui_value is not None else self.value)
class CheckboxMixin:
@gather_metrics("checkbox")
def checkbox(
self,
label: str,
value: bool = False,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> bool:
r"""Display a checkbox widget.
Parameters
----------
label : str
A short label explaining to the user what this checkbox is for.
The label can optionally contain GitHub-flavored Markdown of the
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
and Images. Images display like icons, with a max height equal to
the font height.
Unsupported Markdown elements are unwrapped so only their children
(text contents) render. Display unsupported elements as literal
characters by backslash-escaping them. E.g.,
``"1\. Not an ordered list"``.
See the ``body`` parameter of |st.markdown|_ for additional,
supported Markdown directives.
For accessibility reasons, you should never set an empty label, but
you can hide it with ``label_visibility`` if needed. In the future,
we may disallow empty labels by raising an exception.
.. |st.markdown| replace:: ``st.markdown``
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
value : bool
Preselect the checkbox when it first renders. This will be
cast to bool internally.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. No two widgets may have the same key.
help : str or None
A tooltip that gets displayed next to the widget label. Streamlit
only displays the tooltip when ``label_visibility="visible"``. If
this is ``None`` (default), no tooltip is displayed.
The tooltip can optionally contain GitHub-flavored Markdown,
including the Markdown directives described in the ``body``
parameter of ``st.markdown``.
on_change : callable
An optional callback invoked when this checkbox's value changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
disabled : bool
An optional boolean that disables the checkbox if set to ``True``.
The default is ``False``.
label_visibility : "visible", "hidden", or "collapsed"
The visibility of the label. The default is ``"visible"``. If this
is ``"hidden"``, Streamlit displays an empty spacer instead of the
label, which can help keep the widget alligned with other widgets.
If this is ``"collapsed"``, Streamlit displays no label or spacer.
Returns
-------
bool
Whether or not the checkbox is checked.
Example
-------
>>> import streamlit as st
>>>
>>> agree = st.checkbox("I agree")
>>>
>>> if agree:
... st.write("Great!")
.. output::
https://doc-checkbox.streamlit.app/
height: 220px
"""
ctx = get_script_run_ctx()
return self._checkbox(
label=label,
value=value,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
disabled=disabled,
label_visibility=label_visibility,
type=CheckboxProto.StyleType.DEFAULT,
ctx=ctx,
)
@gather_metrics("toggle")
def toggle(
self,
label: str,
value: bool = False,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> bool:
r"""Display a toggle widget.
Parameters
----------
label : str
A short label explaining to the user what this toggle is for.
The label can optionally contain GitHub-flavored Markdown of the
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
and Images. Images display like icons, with a max height equal to
the font height.
Unsupported Markdown elements are unwrapped so only their children
(text contents) render. Display unsupported elements as literal
characters by backslash-escaping them. E.g.,
``"1\. Not an ordered list"``.
See the ``body`` parameter of |st.markdown|_ for additional,
supported Markdown directives.
For accessibility reasons, you should never set an empty label, but
you can hide it with ``label_visibility`` if needed. In the future,
we may disallow empty labels by raising an exception.
.. |st.markdown| replace:: ``st.markdown``
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
value : bool
Preselect the toggle when it first renders. This will be
cast to bool internally.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. No two widgets may have the same key.
help : str or None
A tooltip that gets displayed next to the widget label. Streamlit
only displays the tooltip when ``label_visibility="visible"``. If
this is ``None`` (default), no tooltip is displayed.
The tooltip can optionally contain GitHub-flavored Markdown,
including the Markdown directives described in the ``body``
parameter of ``st.markdown``.
on_change : callable
An optional callback invoked when this toggle's value changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
disabled : bool
An optional boolean that disables the toggle if set to ``True``.
The default is ``False``.
label_visibility : "visible", "hidden", or "collapsed"
The visibility of the label. The default is ``"visible"``. If this
is ``"hidden"``, Streamlit displays an empty spacer instead of the
label, which can help keep the widget alligned with other widgets.
If this is ``"collapsed"``, Streamlit displays no label or spacer.
Returns
-------
bool
Whether or not the toggle is checked.
Example
-------
>>> import streamlit as st
>>>
>>> on = st.toggle("Activate feature")
>>>
>>> if on:
... st.write("Feature activated!")
.. output::
https://doc-toggle.streamlit.app/
height: 220px
"""
ctx = get_script_run_ctx()
return self._checkbox(
label=label,
value=value,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
disabled=disabled,
label_visibility=label_visibility,
type=CheckboxProto.StyleType.TOGGLE,
ctx=ctx,
)
def _checkbox(
self,
label: str,
value: bool = False,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
type: CheckboxProto.StyleType.ValueType = CheckboxProto.StyleType.DEFAULT,
ctx: ScriptRunContext | None = None,
) -> bool:
key = to_key(key)
check_widget_policies(
self.dg,
key,
on_change,
default_value=None if value is False else value,
)
maybe_raise_label_warnings(label, label_visibility)
element_id = compute_and_register_element_id(
"toggle" if type == CheckboxProto.StyleType.TOGGLE else "checkbox",
user_key=key,
form_id=current_form_id(self.dg),
label=label,
value=bool(value),
help=help,
)
checkbox_proto = CheckboxProto()
checkbox_proto.id = element_id
checkbox_proto.label = label
checkbox_proto.default = bool(value)
checkbox_proto.type = type
checkbox_proto.form_id = current_form_id(self.dg)
checkbox_proto.disabled = disabled
checkbox_proto.label_visibility.value = get_label_visibility_proto_value(
label_visibility
)
if help is not None:
checkbox_proto.help = dedent(help)
serde = CheckboxSerde(value)
checkbox_state = register_widget(
checkbox_proto.id,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=serde.deserialize,
serializer=serde.serialize,
ctx=ctx,
value_type="bool_value",
)
if checkbox_state.value_changed:
checkbox_proto.value = checkbox_state.value
checkbox_proto.set_value = True
self.dg._enqueue("checkbox", checkbox_proto)
return checkbox_state.value
@property
def dg(self) -> DeltaGenerator:
"""Get our DeltaGenerator."""
return cast("DeltaGenerator", self)

View File

@@ -0,0 +1,265 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import re
from dataclasses import dataclass
from textwrap import dedent
from typing import TYPE_CHECKING, cast
from streamlit.elements.lib.form_utils import current_form_id
from streamlit.elements.lib.policies import (
check_widget_policies,
maybe_raise_label_warnings,
)
from streamlit.elements.lib.utils import (
Key,
LabelVisibility,
compute_and_register_element_id,
get_label_visibility_proto_value,
to_key,
)
from streamlit.errors import StreamlitAPIException
from streamlit.proto.ColorPicker_pb2 import ColorPicker as ColorPickerProto
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.runtime.state import (
WidgetArgs,
WidgetCallback,
WidgetKwargs,
register_widget,
)
if TYPE_CHECKING:
from streamlit.delta_generator import DeltaGenerator
@dataclass
class ColorPickerSerde:
value: str
def serialize(self, v: str) -> str:
return str(v)
def deserialize(self, ui_value: str | None, widget_id: str = "") -> str:
return str(ui_value if ui_value is not None else self.value)
class ColorPickerMixin:
@gather_metrics("color_picker")
def color_picker(
self,
label: str,
value: str | None = None,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> str:
r"""Display a color picker widget.
Parameters
----------
label : str
A short label explaining to the user what this input is for.
The label can optionally contain GitHub-flavored Markdown of the
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
and Images. Images display like icons, with a max height equal to
the font height.
Unsupported Markdown elements are unwrapped so only their children
(text contents) render. Display unsupported elements as literal
characters by backslash-escaping them. E.g.,
``"1\. Not an ordered list"``.
See the ``body`` parameter of |st.markdown|_ for additional,
supported Markdown directives.
For accessibility reasons, you should never set an empty label, but
you can hide it with ``label_visibility`` if needed. In the future,
we may disallow empty labels by raising an exception.
.. |st.markdown| replace:: ``st.markdown``
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
value : str
The hex value of this widget when it first renders. If None,
defaults to black.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. No two widgets may have the same key.
help : str or None
A tooltip that gets displayed next to the widget label. Streamlit
only displays the tooltip when ``label_visibility="visible"``. If
this is ``None`` (default), no tooltip is displayed.
The tooltip can optionally contain GitHub-flavored Markdown,
including the Markdown directives described in the ``body``
parameter of ``st.markdown``.
on_change : callable
An optional callback invoked when this color_picker's value
changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
disabled : bool
An optional boolean that disables the color picker if set to
``True``. The default is ``False``.
label_visibility : "visible", "hidden", or "collapsed"
The visibility of the label. The default is ``"visible"``. If this
is ``"hidden"``, Streamlit displays an empty spacer instead of the
label, which can help keep the widget alligned with other widgets.
If this is ``"collapsed"``, Streamlit displays no label or spacer.
Returns
-------
str
The selected color as a hex string.
Example
-------
>>> import streamlit as st
>>>
>>> color = st.color_picker("Pick A Color", "#00f900")
>>> st.write("The current color is", color)
.. output::
https://doc-color-picker.streamlit.app/
height: 335px
"""
ctx = get_script_run_ctx()
return self._color_picker(
label=label,
value=value,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
disabled=disabled,
label_visibility=label_visibility,
ctx=ctx,
)
def _color_picker(
self,
label: str,
value: str | None = None,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
ctx: ScriptRunContext | None = None,
) -> str:
key = to_key(key)
check_widget_policies(
self.dg,
key,
on_change,
default_value=value,
)
maybe_raise_label_warnings(label, label_visibility)
element_id = compute_and_register_element_id(
"color_picker",
user_key=key,
form_id=current_form_id(self.dg),
label=label,
value=str(value),
help=help,
)
# set value default
if value is None:
value = "#000000"
# make sure the value is a string
if not isinstance(value, str):
raise StreamlitAPIException(
"""
Color Picker Value has invalid type: %s. Expects a hex string
like '#00FFAA' or '#000'.
"""
% type(value).__name__
)
# validate the value and expects a hex string
match = re.match(r"^#(?:[0-9a-fA-F]{3}){1,2}$", value)
if not match:
raise StreamlitAPIException(
"""
'%s' is not a valid hex code for colors. Valid ones are like
'#00FFAA' or '#000'.
"""
% value
)
color_picker_proto = ColorPickerProto()
color_picker_proto.id = element_id
color_picker_proto.label = label
color_picker_proto.default = str(value)
color_picker_proto.form_id = current_form_id(self.dg)
color_picker_proto.disabled = disabled
color_picker_proto.label_visibility.value = get_label_visibility_proto_value(
label_visibility
)
if help is not None:
color_picker_proto.help = dedent(help)
serde = ColorPickerSerde(value)
widget_state = register_widget(
color_picker_proto.id,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=serde.deserialize,
serializer=serde.serialize,
ctx=ctx,
value_type="string_value",
)
if widget_state.value_changed:
color_picker_proto.value = widget_state.value
color_picker_proto.set_value = True
self.dg._enqueue("color_picker", color_picker_proto)
return widget_state.value
@property
def dg(self) -> DeltaGenerator:
"""Get our DeltaGenerator."""
return cast("DeltaGenerator", self)

View File

@@ -0,0 +1,982 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import json
from dataclasses import dataclass
from decimal import Decimal
from typing import (
TYPE_CHECKING,
Any,
Final,
Literal,
TypedDict,
TypeVar,
Union,
cast,
overload,
)
from typing_extensions import TypeAlias
from streamlit import dataframe_util
from streamlit import logger as _logger
from streamlit.elements.lib.column_config_utils import (
INDEX_IDENTIFIER,
ColumnConfigMapping,
ColumnConfigMappingInput,
ColumnDataKind,
DataframeSchema,
apply_data_specific_configs,
determine_dataframe_schema,
is_type_compatible,
marshall_column_config,
process_config_mapping,
update_column_config,
)
from streamlit.elements.lib.form_utils import current_form_id
from streamlit.elements.lib.pandas_styler_utils import marshall_styler
from streamlit.elements.lib.policies import check_widget_policies
from streamlit.elements.lib.utils import Key, compute_and_register_element_id, to_key
from streamlit.errors import StreamlitAPIException
from streamlit.proto.Arrow_pb2 import Arrow as ArrowProto
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
from streamlit.runtime.state import (
WidgetArgs,
WidgetCallback,
WidgetKwargs,
register_widget,
)
from streamlit.type_util import is_type
from streamlit.util import calc_md5
if TYPE_CHECKING:
from collections.abc import Iterable, Mapping
import numpy as np
import pandas as pd
import pyarrow as pa
from pandas.io.formats.style import Styler
from streamlit.delta_generator import DeltaGenerator
_LOGGER: Final = _logger.get_logger(__name__)
# All formats that support direct editing, meaning that these
# formats will be returned with the same type when used with data_editor.
EditableData = TypeVar(
"EditableData",
bound=Union[
dataframe_util.DataFrameGenericAlias[Any], # covers DataFrame and Series
tuple[Any],
list[Any],
set[Any],
dict[str, Any],
# TODO(lukasmasuch): Add support for np.ndarray
# but it is not possible with np.ndarray.
# NDArray[Any] works, but is only available in numpy>1.20.
# TODO(lukasmasuch): Add support for pa.Table typing
# pa.Table does not work since it is a C-based class resulting in Any
],
)
# All data types supported by the data editor.
DataTypes: TypeAlias = Union[
"pd.DataFrame",
"pd.Series",
"pd.Index",
"Styler",
"pa.Table",
"np.ndarray[Any, np.dtype[np.float64]]",
tuple[Any],
list[Any],
set[Any],
dict[str, Any],
]
class EditingState(TypedDict, total=False):
"""
A dictionary representing the current state of the data editor.
Attributes
----------
edited_rows : Dict[int, Dict[str, str | int | float | bool | None]]
An hierarchical mapping of edited cells based on:
row position -> column name -> value.
added_rows : List[Dict[str, str | int | float | bool | None]]
A list of added rows, where each row is a mapping from column name to
the cell value.
deleted_rows : List[int]
A list of deleted rows, where each row is the numerical position of
the deleted row.
"""
edited_rows: dict[int, dict[str, str | int | float | bool | None]]
added_rows: list[dict[str, str | int | float | bool | None]]
deleted_rows: list[int]
@dataclass
class DataEditorSerde:
"""DataEditorSerde is used to serialize and deserialize the data editor state."""
def deserialize(self, ui_value: str | None, widget_id: str = "") -> EditingState:
data_editor_state: EditingState = (
{
"edited_rows": {},
"added_rows": [],
"deleted_rows": [],
}
if ui_value is None
else json.loads(ui_value)
)
# Make sure that all editing state keys are present:
if "edited_rows" not in data_editor_state:
data_editor_state["edited_rows"] = {}
if "deleted_rows" not in data_editor_state:
data_editor_state["deleted_rows"] = []
if "added_rows" not in data_editor_state:
data_editor_state["added_rows"] = []
# Convert the keys (numerical row positions) to integers.
# The keys are strings because they are serialized to JSON.
data_editor_state["edited_rows"] = {
int(k): v for k, v in data_editor_state["edited_rows"].items()
}
return data_editor_state
def serialize(self, editing_state: EditingState) -> str:
return json.dumps(editing_state, default=str)
def _parse_value(
value: str | int | float | bool | None,
column_data_kind: ColumnDataKind,
) -> Any:
"""Convert a value to the correct type.
Parameters
----------
value : str | int | float | bool | None
The value to convert.
column_data_kind : ColumnDataKind
The determined data kind of the column. The column data kind refers to the
shared data type of the values in the column (e.g. int, float, str).
Returns
-------
The converted value.
"""
if value is None:
return None
import pandas as pd
try:
if column_data_kind == ColumnDataKind.STRING:
return str(value)
if column_data_kind == ColumnDataKind.INTEGER:
return int(value)
if column_data_kind == ColumnDataKind.FLOAT:
return float(value)
if column_data_kind == ColumnDataKind.BOOLEAN:
return bool(value)
if column_data_kind == ColumnDataKind.DECIMAL:
# Decimal theoretically can also be initialized via number values.
# However, using number values here seems to cause issues with Arrow
# serialization, once you try to render the returned dataframe.
return Decimal(str(value))
if column_data_kind == ColumnDataKind.TIMEDELTA:
return pd.Timedelta(value)
if column_data_kind in [
ColumnDataKind.DATETIME,
ColumnDataKind.DATE,
ColumnDataKind.TIME,
]:
datetime_value = pd.Timestamp(value)
if datetime_value is pd.NaT:
return None
if column_data_kind == ColumnDataKind.DATETIME:
return datetime_value
if column_data_kind == ColumnDataKind.DATE:
return datetime_value.date()
if column_data_kind == ColumnDataKind.TIME:
return datetime_value.time()
except (ValueError, pd.errors.ParserError) as ex:
_LOGGER.warning(
"Failed to parse value %s as %s.",
value,
column_data_kind,
exc_info=ex,
)
return None
return value
def _apply_cell_edits(
df: pd.DataFrame,
edited_rows: Mapping[int, Mapping[str, str | int | float | bool | None]],
dataframe_schema: DataframeSchema,
) -> None:
"""Apply cell edits to the provided dataframe (inplace).
Parameters
----------
df : pd.DataFrame
The dataframe to apply the cell edits to.
edited_rows : Mapping[int, Mapping[str, str | int | float | bool | None]]
A hierarchical mapping based on row position -> column name -> value
dataframe_schema: DataframeSchema
The schema of the dataframe.
"""
for row_id, row_changes in edited_rows.items():
row_pos = int(row_id)
for col_name, value in row_changes.items():
if col_name == INDEX_IDENTIFIER:
# The edited cell is part of the index
# TODO(lukasmasuch): To support multi-index in the future:
# use a tuple of values here instead of a single value
df.index.to_numpy()[row_pos] = _parse_value(
value, dataframe_schema[INDEX_IDENTIFIER]
)
else:
col_pos = df.columns.get_loc(col_name)
df.iloc[row_pos, col_pos] = _parse_value(
value, dataframe_schema[col_name]
)
def _apply_row_additions(
df: pd.DataFrame,
added_rows: list[dict[str, Any]],
dataframe_schema: DataframeSchema,
) -> None:
"""Apply row additions to the provided dataframe (inplace).
Parameters
----------
df : pd.DataFrame
The dataframe to apply the row additions to.
added_rows : List[Dict[str, Any]]
A list of row additions. Each row addition is a dictionary with the
column position as key and the new cell value as value.
dataframe_schema: DataframeSchema
The schema of the dataframe.
"""
if not added_rows:
return
import pandas as pd
# This is only used if the dataframe has a range index:
# There seems to be a bug in older pandas versions with RangeIndex in
# combination with loc. As a workaround, we manually track the values here:
range_index_stop = None
range_index_step = None
if isinstance(df.index, pd.RangeIndex):
range_index_stop = df.index.stop
range_index_step = df.index.step
for added_row in added_rows:
index_value = None
new_row: list[Any] = [None for _ in range(df.shape[1])]
for col_name in added_row.keys():
value = added_row[col_name]
if col_name == INDEX_IDENTIFIER:
# TODO(lukasmasuch): To support multi-index in the future:
# use a tuple of values here instead of a single value
index_value = _parse_value(value, dataframe_schema[INDEX_IDENTIFIER])
else:
col_pos = df.columns.get_loc(col_name)
new_row[col_pos] = _parse_value(value, dataframe_schema[col_name])
# Append the new row to the dataframe
if range_index_stop is not None:
df.loc[range_index_stop, :] = new_row
# Increment to the next range index value
range_index_stop += range_index_step
elif index_value is not None:
# TODO(lukasmasuch): we are only adding rows that have a non-None index
# value to prevent issues in the frontend component. Also, it just overwrites
# the row in case the index value already exists in the dataframe.
# In the future, it would be better to require users to provide unique
# non-None values for the index with some kind of visual indications.
df.loc[index_value, :] = new_row
def _apply_row_deletions(df: pd.DataFrame, deleted_rows: list[int]) -> None:
"""Apply row deletions to the provided dataframe (inplace).
Parameters
----------
df : pd.DataFrame
The dataframe to apply the row deletions to.
deleted_rows : List[int]
A list of row numbers to delete.
"""
# Drop rows based in numeric row positions
df.drop(df.index[deleted_rows], inplace=True) # noqa: PD002
def _apply_dataframe_edits(
df: pd.DataFrame,
data_editor_state: EditingState,
dataframe_schema: DataframeSchema,
) -> None:
"""Apply edits to the provided dataframe (inplace).
This includes cell edits, row additions and row deletions.
Parameters
----------
df : pd.DataFrame
The dataframe to apply the edits to.
data_editor_state : EditingState
The editing state of the data editor component.
dataframe_schema: DataframeSchema
The schema of the dataframe.
"""
if data_editor_state.get("edited_rows"):
_apply_cell_edits(df, data_editor_state["edited_rows"], dataframe_schema)
if data_editor_state.get("deleted_rows"):
_apply_row_deletions(df, data_editor_state["deleted_rows"])
if data_editor_state.get("added_rows"):
# The addition of new rows needs to happen after the deletion to not have
# unexpected side-effects, like https://github.com/streamlit/streamlit/issues/8854
_apply_row_additions(df, data_editor_state["added_rows"], dataframe_schema)
def _is_supported_index(df_index: pd.Index) -> bool:
"""Check if the index is supported by the data editor component.
Parameters
----------
df_index : pd.Index
The index to check.
Returns
-------
bool
True if the index is supported, False otherwise.
"""
import pandas as pd
return (
type(df_index)
in [
pd.RangeIndex,
pd.Index,
pd.DatetimeIndex,
pd.CategoricalIndex,
# Interval type isn't editable currently:
# pd.IntervalIndex,
# Period type isn't editable currently:
# pd.PeriodIndex,
]
# We need to check these index types without importing, since they are
# deprecated and planned to be removed soon.
or is_type(df_index, "pandas.core.indexes.numeric.Int64Index")
or is_type(df_index, "pandas.core.indexes.numeric.Float64Index")
or is_type(df_index, "pandas.core.indexes.numeric.UInt64Index")
)
def _fix_column_headers(data_df: pd.DataFrame) -> None:
"""Fix the column headers of the provided dataframe inplace to work
correctly for data editing.
"""
import pandas as pd
if isinstance(data_df.columns, pd.MultiIndex):
# Flatten hierarchical column headers to a single level:
data_df.columns = [
"_".join(map(str, header)) for header in data_df.columns.to_flat_index()
]
elif pd.api.types.infer_dtype(data_df.columns) != "string":
# If the column names are not all strings, we need to convert them to strings
# to avoid issues with editing:
data_df.rename(
columns={column: str(column) for column in data_df.columns},
inplace=True, # noqa: PD002
)
def _check_column_names(data_df: pd.DataFrame):
"""Check if the column names in the provided dataframe are valid.
It's not allowed to have duplicate column names or column names that are
named ``_index``. If the column names are not valid, a ``StreamlitAPIException``
is raised.
"""
if data_df.columns.empty:
return
# Check if the column names are unique and raise an exception if not.
# Add the names of the duplicated columns to the exception message.
duplicated_columns = data_df.columns[data_df.columns.duplicated()]
if len(duplicated_columns) > 0:
raise StreamlitAPIException(
f"All column names are required to be unique for usage with data editor. "
f"The following column names are duplicated: {list(duplicated_columns)}. "
f"Please rename the duplicated columns in the provided data."
)
# Check if the column names are not named "_index" and raise an exception if so.
if INDEX_IDENTIFIER in data_df.columns:
raise StreamlitAPIException(
f"The column name '{INDEX_IDENTIFIER}' is reserved for the index column "
f"and can't be used for data columns. Please rename the column in the "
f"provided data."
)
def _check_type_compatibilities(
data_df: pd.DataFrame,
columns_config: ColumnConfigMapping,
dataframe_schema: DataframeSchema,
):
"""Check column type to data type compatibility.
Iterates the index and all columns of the dataframe to check if
the configured column types are compatible with the underlying data types.
Parameters
----------
data_df : pd.DataFrame
The dataframe to check the type compatibilities for.
columns_config : ColumnConfigMapping
A mapping of column to column configurations.
dataframe_schema : DataframeSchema
The schema of the dataframe.
Raises
------
StreamlitAPIException
If a configured column type is editable and not compatible with the
underlying data type.
"""
# TODO(lukasmasuch): Update this here to support multi-index in the future:
indices = [(INDEX_IDENTIFIER, data_df.index)]
for column in indices + list(data_df.items()):
column_name, _ = column
column_data_kind = dataframe_schema[column_name]
# TODO(lukasmasuch): support column config via numerical index here?
if column_name in columns_config:
column_config = columns_config[column_name]
if column_config.get("disabled") is True:
# Disabled columns are not checked for compatibility.
# This might change in the future.
continue
type_config = column_config.get("type_config")
if type_config is None:
continue
configured_column_type = type_config.get("type")
if configured_column_type is None:
continue
if is_type_compatible(configured_column_type, column_data_kind) is False:
raise StreamlitAPIException(
f"The configured column type `{configured_column_type}` for column "
f"`{column_name}` is not compatible for editing the underlying "
f"data type `{column_data_kind}`.\n\nYou have following options to "
f"fix this: 1) choose a compatible type 2) disable the column "
f"3) convert the column into a compatible data type."
)
class DataEditorMixin:
@overload
def data_editor(
self,
data: EditableData,
*,
width: int | None = None,
height: int | None = None,
use_container_width: bool | None = None,
hide_index: bool | None = None,
column_order: Iterable[str] | None = None,
column_config: ColumnConfigMappingInput | None = None,
num_rows: Literal["fixed", "dynamic"] = "fixed",
disabled: bool | Iterable[str] = False,
key: Key | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
row_height: int | None = None,
) -> EditableData:
pass
@overload
def data_editor(
self,
data: Any,
*,
width: int | None = None,
height: int | None = None,
use_container_width: bool | None = None,
hide_index: bool | None = None,
column_order: Iterable[str] | None = None,
column_config: ColumnConfigMappingInput | None = None,
num_rows: Literal["fixed", "dynamic"] = "fixed",
disabled: bool | Iterable[str] = False,
key: Key | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
row_height: int | None = None,
) -> pd.DataFrame:
pass
@gather_metrics("data_editor")
def data_editor(
self,
data: DataTypes,
*,
width: int | None = None,
height: int | None = None,
use_container_width: bool | None = None,
hide_index: bool | None = None,
column_order: Iterable[str] | None = None,
column_config: ColumnConfigMappingInput | None = None,
num_rows: Literal["fixed", "dynamic"] = "fixed",
disabled: bool | Iterable[str] = False,
key: Key | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
row_height: int | None = None,
) -> DataTypes:
"""Display a data editor widget.
The data editor widget allows you to edit dataframes and many other data structures in a table-like UI.
Parameters
----------
data : Anything supported by st.dataframe
The data to edit in the data editor.
.. note::
- Styles from ``pandas.Styler`` will only be applied to non-editable columns.
- Text and number formatting from ``column_config`` always takes
precedence over text and number formatting from ``pandas.Styler``.
- Mixing data types within a column can make the column uneditable.
- Additionally, the following data types are not yet supported for editing:
``complex``, ``list``, ``tuple``, ``bytes``, ``bytearray``,
``memoryview``, ``dict``, ``set``, ``frozenset``,
``fractions.Fraction``, ``pandas.Interval``, and
``pandas.Period``.
- To prevent overflow in JavaScript, columns containing
``datetime.timedelta`` and ``pandas.Timedelta`` values will
default to uneditable, but this can be changed through column
configuration.
width : int or None
Desired width of the data editor expressed in pixels. If ``width``
is ``None`` (default), Streamlit sets the data editor width to fit
its contents up to the width of the parent container. If ``width``
is greater than the width of the parent container, Streamlit sets
the data editor width to match the width of the parent container.
height : int or None
Desired height of the data editor expressed in pixels. If ``height``
is ``None`` (default), Streamlit sets the height to show at most
ten rows. Vertical scrolling within the data editor element is
enabled when the height does not accomodate all rows.
use_container_width : bool
Whether to override ``width`` with the width of the parent
container. If this is ``True`` (default), Streamlit sets the width
of the data editor to match the width of the parent container. If
this is ``False``, Streamlit sets the data editor's width according
to ``width``.
hide_index : bool or None
Whether to hide the index column(s). If ``hide_index`` is ``None``
(default), the visibility of index columns is automatically
determined based on the data.
column_order : Iterable of str or None
Specifies the display order of columns. This also affects which columns are
visible. For example, ``column_order=("col2", "col1")`` will display 'col2'
first, followed by 'col1', and will hide all other non-index columns. If
None (default), the order is inherited from the original data structure.
column_config : dict or None
Configures how columns are displayed, e.g. their title, visibility, type, or
format, as well as editing properties such as min/max value or step.
This needs to be a dictionary where each key is a column name and the value
is one of:
- ``None`` to hide the column.
- A string to set the display label of the column.
- One of the column types defined under ``st.column_config``, e.g.
``st.column_config.NumberColumn("Dollar values”, format=”$ %d")`` to show
a column as dollar amounts. See more info on the available column types
and config options `here <https://docs.streamlit.io/develop/api-reference/data/st.column_config>`_.
To configure the index column(s), use ``_index`` as the column name.
num_rows : "fixed" or "dynamic"
Specifies if the user can add and delete rows in the data editor.
If "fixed", the user cannot add or delete rows. If "dynamic", the user can
add and delete rows in the data editor, but column sorting is disabled.
Defaults to "fixed".
disabled : bool or Iterable of str
Controls the editing of columns. If True, editing is disabled for all columns.
If an Iterable of column names is provided (e.g., ``disabled=("col1", "col2"))``,
only the specified columns will be disabled for editing. If False (default),
all columns that support editing are editable.
key : str
An optional string to use as the unique key for this widget. If this
is omitted, a key will be generated for the widget based on its
content. No two widgets may have the same key.
on_change : callable
An optional callback invoked when this data_editor's value changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
row_height : int or None
The height of each row in the data editor in pixels. If ``row_height``
is ``None`` (default), Streamlit will use a default row height,
which fits one line of text.
Returns
-------
pandas.DataFrame, pandas.Series, pyarrow.Table, numpy.ndarray, list, set, tuple, or dict.
The edited data. The edited data is returned in its original data type if
it corresponds to any of the supported return types. All other data types
are returned as a ``pandas.DataFrame``.
Examples
--------
>>> import streamlit as st
>>> import pandas as pd
>>>
>>> df = pd.DataFrame(
>>> [
>>> {"command": "st.selectbox", "rating": 4, "is_widget": True},
>>> {"command": "st.balloons", "rating": 5, "is_widget": False},
>>> {"command": "st.time_input", "rating": 3, "is_widget": True},
>>> ]
>>> )
>>> edited_df = st.data_editor(df)
>>>
>>> favorite_command = edited_df.loc[edited_df["rating"].idxmax()]["command"]
>>> st.markdown(f"Your favorite command is **{favorite_command}** 🎈")
.. output::
https://doc-data-editor.streamlit.app/
height: 350px
You can also allow the user to add and delete rows by setting ``num_rows`` to "dynamic":
>>> import streamlit as st
>>> import pandas as pd
>>>
>>> df = pd.DataFrame(
>>> [
>>> {"command": "st.selectbox", "rating": 4, "is_widget": True},
>>> {"command": "st.balloons", "rating": 5, "is_widget": False},
>>> {"command": "st.time_input", "rating": 3, "is_widget": True},
>>> ]
>>> )
>>> edited_df = st.data_editor(df, num_rows="dynamic")
>>>
>>> favorite_command = edited_df.loc[edited_df["rating"].idxmax()]["command"]
>>> st.markdown(f"Your favorite command is **{favorite_command}** 🎈")
.. output::
https://doc-data-editor1.streamlit.app/
height: 450px
Or you can customize the data editor via ``column_config``, ``hide_index``,
``column_order``, or ``disabled``:
>>> import pandas as pd
>>> import streamlit as st
>>>
>>> df = pd.DataFrame(
>>> [
>>> {"command": "st.selectbox", "rating": 4, "is_widget": True},
>>> {"command": "st.balloons", "rating": 5, "is_widget": False},
>>> {"command": "st.time_input", "rating": 3, "is_widget": True},
>>> ]
>>> )
>>> edited_df = st.data_editor(
>>> df,
>>> column_config={
>>> "command": "Streamlit Command",
>>> "rating": st.column_config.NumberColumn(
>>> "Your rating",
>>> help="How much do you like this command (1-5)?",
>>> min_value=1,
>>> max_value=5,
>>> step=1,
>>> format="%d",
>>> ),
>>> "is_widget": "Widget ?",
>>> },
>>> disabled=["command", "is_widget"],
>>> hide_index=True,
>>> )
>>>
>>> favorite_command = edited_df.loc[edited_df["rating"].idxmax()]["command"]
>>> st.markdown(f"Your favorite command is **{favorite_command}** 🎈")
.. output::
https://doc-data-editor-config.streamlit.app/
height: 350px
"""
# Lazy-loaded import
import pandas as pd
import pyarrow as pa
key = to_key(key)
check_widget_policies(
self.dg,
key,
on_change,
default_value=None,
writes_allowed=False,
)
if column_order is not None:
column_order = list(column_order)
column_config_mapping: ColumnConfigMapping = {}
data_format = dataframe_util.determine_data_format(data)
if data_format == dataframe_util.DataFormat.UNKNOWN:
raise StreamlitAPIException(
f"The data type ({type(data).__name__}) or format is not supported by "
"the data editor. Please convert your data into a Pandas Dataframe or "
"another supported data format."
)
# The dataframe should always be a copy of the original data
# since we will apply edits directly to it.
data_df = dataframe_util.convert_anything_to_pandas_df(data, ensure_copy=True)
# Check if the index is supported.
if not _is_supported_index(data_df.index):
raise StreamlitAPIException(
f"The type of the dataframe index - {type(data_df.index).__name__} - is not "
"yet supported by the data editor."
)
# Check if the column names are valid and unique.
_check_column_names(data_df)
# Convert the user provided column config into the frontend compatible format:
column_config_mapping = process_config_mapping(column_config)
# Deactivate editing for columns that are not compatible with arrow
for column_name, column_data in data_df.items():
if dataframe_util.is_colum_type_arrow_incompatible(column_data):
update_column_config(
column_config_mapping, column_name, {"disabled": True}
)
# Convert incompatible type to string
data_df[column_name] = column_data.astype("string")
apply_data_specific_configs(column_config_mapping, data_format)
# Fix the column headers to work correctly for data editing:
_fix_column_headers(data_df)
has_range_index = isinstance(data_df.index, pd.RangeIndex)
if not has_range_index:
# If the index is not a range index, we will configure it as required
# since the user is required to provide a (unique) value for editing.
update_column_config(
column_config_mapping, INDEX_IDENTIFIER, {"required": True}
)
if hide_index is None and has_range_index and num_rows == "dynamic":
# Temporary workaround:
# We hide range indices if num_rows is dynamic.
# since the current way of handling this index during editing is a
# bit confusing. The user can still decide to show the index by
# setting hide_index explicitly to False.
hide_index = True
if hide_index is not None:
update_column_config(
column_config_mapping, INDEX_IDENTIFIER, {"hidden": hide_index}
)
# If disabled not a boolean, we assume it is a list of columns to disable.
# This gets translated into the columns configuration:
if not isinstance(disabled, bool):
for column in disabled:
update_column_config(column_config_mapping, column, {"disabled": True})
# Convert the dataframe to an arrow table which is used as the main
# serialization format for sending the data to the frontend.
# We also utilize the arrow schema to determine the data kinds of every column.
arrow_table = pa.Table.from_pandas(data_df)
# Determine the dataframe schema which is required for parsing edited values
# and for checking type compatibilities.
dataframe_schema = determine_dataframe_schema(data_df, arrow_table.schema)
# Check if all configured column types are compatible with the underlying data.
# Throws an exception if any of the configured types are incompatible.
_check_type_compatibilities(data_df, column_config_mapping, dataframe_schema)
arrow_bytes = dataframe_util.convert_arrow_table_to_arrow_bytes(arrow_table)
# We want to do this as early as possible to avoid introducing nondeterminism,
# but it isn't clear how much processing is needed to have the data in a
# format that will hash consistently, so we do it late here to have it
# as close as possible to how it used to be.
ctx = get_script_run_ctx()
element_id = compute_and_register_element_id(
"data_editor",
user_key=key,
form_id=current_form_id(self.dg),
data=arrow_bytes,
width=width,
height=height,
use_container_width=use_container_width,
column_order=column_order,
column_config_mapping=str(column_config_mapping),
num_rows=num_rows,
row_height=row_height,
)
proto = ArrowProto()
proto.id = element_id
if use_container_width is None:
# If use_container_width was not explicitly set by the user, we set
# it to True if width was not set explicitly, and False otherwise.
use_container_width = True if width is None else False
proto.use_container_width = use_container_width
if width:
proto.width = width
if height:
proto.height = height
if row_height:
proto.row_height = row_height
if column_order:
proto.column_order[:] = column_order
# Only set disabled to true if it is actually true
# It can also be a list of columns, which should result in false here.
proto.disabled = disabled is True
proto.editing_mode = (
ArrowProto.EditingMode.DYNAMIC
if num_rows == "dynamic"
else ArrowProto.EditingMode.FIXED
)
proto.form_id = current_form_id(self.dg)
if dataframe_util.is_pandas_styler(data):
# Pandas styler will only work for non-editable/disabled columns.
# Get first 10 chars of md5 hash of the key or delta path as styler uuid
# and set it as styler uuid.
# We are only using the first 10 chars to keep the uuid short since
# it will be used for all the cells in the dataframe. Therefore, this
# might have a significant impact on the message size. 10 chars
# should be good enough to avoid potential collisions in this case.
# Even on collisions, there should not be a big issue with the
# rendering in the data editor.
styler_uuid = calc_md5(key or self.dg._get_delta_path_str())[:10]
data.set_uuid(styler_uuid)
marshall_styler(proto, data, styler_uuid)
proto.data = arrow_bytes
marshall_column_config(proto, column_config_mapping)
serde = DataEditorSerde()
widget_state = register_widget(
proto.id,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=serde.deserialize,
serializer=serde.serialize,
ctx=ctx,
value_type="string_value",
)
_apply_dataframe_edits(data_df, widget_state.value, dataframe_schema)
self.dg._enqueue("arrow_data_frame", proto)
return dataframe_util.convert_pandas_df_to_data_format(data_df, data_format)
@property
def dg(self) -> DeltaGenerator:
"""Get our DeltaGenerator."""
return cast("DeltaGenerator", self)

View File

@@ -0,0 +1,486 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from dataclasses import dataclass
from textwrap import dedent
from typing import TYPE_CHECKING, Literal, Union, cast, overload
from typing_extensions import TypeAlias
from streamlit import config
from streamlit.elements.lib.file_uploader_utils import (
enforce_filename_restriction,
normalize_upload_file_type,
)
from streamlit.elements.lib.form_utils import current_form_id
from streamlit.elements.lib.policies import (
check_widget_policies,
maybe_raise_label_warnings,
)
from streamlit.elements.lib.utils import (
Key,
LabelVisibility,
compute_and_register_element_id,
get_label_visibility_proto_value,
to_key,
)
from streamlit.proto.Common_pb2 import FileUploaderState as FileUploaderStateProto
from streamlit.proto.Common_pb2 import UploadedFileInfo as UploadedFileInfoProto
from streamlit.proto.FileUploader_pb2 import FileUploader as FileUploaderProto
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.runtime.state import (
WidgetArgs,
WidgetCallback,
WidgetKwargs,
register_widget,
)
from streamlit.runtime.uploaded_file_manager import DeletedFile, UploadedFile
if TYPE_CHECKING:
from collections.abc import Sequence
from streamlit.delta_generator import DeltaGenerator
SomeUploadedFiles: TypeAlias = Union[
UploadedFile,
DeletedFile,
list[Union[UploadedFile, DeletedFile]],
None,
]
def _get_upload_files(
widget_value: FileUploaderStateProto | None,
) -> list[UploadedFile | DeletedFile]:
if widget_value is None:
return []
ctx = get_script_run_ctx()
if ctx is None:
return []
uploaded_file_info = widget_value.uploaded_file_info
if len(uploaded_file_info) == 0:
return []
file_recs_list = ctx.uploaded_file_mgr.get_files(
session_id=ctx.session_id,
file_ids=[f.file_id for f in uploaded_file_info],
)
file_recs = {f.file_id: f for f in file_recs_list}
collected_files: list[UploadedFile | DeletedFile] = []
for f in uploaded_file_info:
maybe_file_rec = file_recs.get(f.file_id)
if maybe_file_rec is not None:
uploaded_file = UploadedFile(maybe_file_rec, f.file_urls)
collected_files.append(uploaded_file)
else:
collected_files.append(DeletedFile(f.file_id))
return collected_files
@dataclass
class FileUploaderSerde:
accept_multiple_files: bool
allowed_types: Sequence[str] | None = None
def deserialize(
self, ui_value: FileUploaderStateProto | None, widget_id: str
) -> SomeUploadedFiles:
upload_files = _get_upload_files(ui_value)
for file in upload_files:
if isinstance(file, DeletedFile):
continue
if self.allowed_types:
enforce_filename_restriction(file.name, self.allowed_types)
if len(upload_files) == 0:
return_value: SomeUploadedFiles = [] if self.accept_multiple_files else None
else:
return_value = (
upload_files if self.accept_multiple_files else upload_files[0]
)
return return_value
def serialize(self, files: SomeUploadedFiles) -> FileUploaderStateProto:
state_proto = FileUploaderStateProto()
if not files:
return state_proto
elif not isinstance(files, list):
files = [files]
for f in files:
if isinstance(f, DeletedFile):
continue
file_info: UploadedFileInfoProto = state_proto.uploaded_file_info.add()
file_info.file_id = f.file_id
file_info.name = f.name
file_info.size = f.size
file_info.file_urls.CopyFrom(f._file_urls)
return state_proto
class FileUploaderMixin:
# Multiple overloads are defined on `file_uploader()` below to represent
# the different return types of `file_uploader()`.
# These return types differ according to the value of the `accept_multiple_files` argument.
# There are 2 associated variables, each with 2 options.
# 1. The `accept_multiple_files` argument is set as `True`,
# or it is set as `False` or omitted, in which case the default value `False`.
# 2. The `type` argument may or may not be provided as a keyword-only argument.
# There must be 2x2=4 overloads to cover all the possible arguments,
# as these overloads must be mutually exclusive for mypy.
# 1. type is given as not a keyword-only argument
# 2. accept_multiple_files = True
@overload
def file_uploader(
self,
label: str,
type: str | Sequence[str] | None,
accept_multiple_files: Literal[True],
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*,
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> list[UploadedFile] | None: ...
# 1. type is given as not a keyword-only argument
# 2. accept_multiple_files = False or omitted
@overload
def file_uploader(
self,
label: str,
type: str | Sequence[str] | None,
accept_multiple_files: Literal[False] = False,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*,
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> UploadedFile | None: ...
# The following 2 overloads represent the cases where
# the `type` argument is a keyword-only argument.
# See https://github.com/python/mypy/issues/4020#issuecomment-737600893
# for the related discussions and examples.
# 1. type is skipped or a keyword argument
# 2. accept_multiple_files = True
@overload
def file_uploader(
self,
label: str,
*,
accept_multiple_files: Literal[True],
type: str | Sequence[str] | None = None,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> list[UploadedFile] | None: ...
# 1. type is skipped or a keyword argument
# 2. accept_multiple_files = False or omitted
@overload
def file_uploader(
self,
label: str,
*,
accept_multiple_files: Literal[False] = False,
type: str | Sequence[str] | None = None,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> UploadedFile | None: ...
@gather_metrics("file_uploader")
def file_uploader(
self,
label: str,
type: str | Sequence[str] | None = None,
accept_multiple_files: bool = False,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> UploadedFile | list[UploadedFile] | None:
r"""Display a file uploader widget.
By default, uploaded files are limited to 200 MB each. You can
configure this using the ``server.maxUploadSize`` config option. For
more information on how to set config options, see |config.toml|_.
.. |config.toml| replace:: ``config.toml``
.. _config.toml: https://docs.streamlit.io/develop/api-reference/configuration/config.toml
Parameters
----------
label : str
A short label explaining to the user what this file uploader is for.
The label can optionally contain GitHub-flavored Markdown of the
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
and Images. Images display like icons, with a max height equal to
the font height.
Unsupported Markdown elements are unwrapped so only their children
(text contents) render. Display unsupported elements as literal
characters by backslash-escaping them. E.g.,
``"1\. Not an ordered list"``.
See the ``body`` parameter of |st.markdown|_ for additional,
supported Markdown directives.
For accessibility reasons, you should never set an empty label, but
you can hide it with ``label_visibility`` if needed. In the future,
we may disallow empty labels by raising an exception.
.. |st.markdown| replace:: ``st.markdown``
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
type : str or list of str or None
The allowed file extension(s) for uploaded files. This can be one
of the following types:
- ``None`` (default): All file extensions are allowed.
- A string: A single file extension is allowed. For example, to
only accept CSV files, use ``"csv"``.
- A sequence of strings: Multiple file extensions are allowed. For
example, to only accept JPG/JPEG and PNG files, use
``["jpg", "jpeg", "png"]``.
accept_multiple_files : bool
Whether to accept more than one file in a submission. If this is
``False`` (default), the user can only submit one file at a time.
If this is ``True``, the user can upload multiple files at the same
time, in which case the return value will be a list of files.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. No two widgets may have the same key.
help : str or None
A tooltip that gets displayed next to the widget label. Streamlit
only displays the tooltip when ``label_visibility="visible"``. If
this is ``None`` (default), no tooltip is displayed.
The tooltip can optionally contain GitHub-flavored Markdown,
including the Markdown directives described in the ``body``
parameter of ``st.markdown``.
on_change : callable
An optional callback invoked when this file_uploader's value
changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
disabled : bool
An optional boolean that disables the file uploader if set to
``True``. The default is ``False``.
label_visibility : "visible", "hidden", or "collapsed"
The visibility of the label. The default is ``"visible"``. If this
is ``"hidden"``, Streamlit displays an empty spacer instead of the
label, which can help keep the widget alligned with other widgets.
If this is ``"collapsed"``, Streamlit displays no label or spacer.
Returns
-------
None, UploadedFile, or list of UploadedFile
- If accept_multiple_files is False, returns either None or
an UploadedFile object.
- If accept_multiple_files is True, returns a list with the
uploaded files as UploadedFile objects. If no files were
uploaded, returns an empty list.
The UploadedFile class is a subclass of BytesIO, and therefore is
"file-like". This means you can pass an instance of it anywhere a
file is expected.
Examples
--------
Insert a file uploader that accepts a single file at a time:
>>> import streamlit as st
>>> import pandas as pd
>>> from io import StringIO
>>>
>>> uploaded_file = st.file_uploader("Choose a file")
>>> if uploaded_file is not None:
... # To read file as bytes:
... bytes_data = uploaded_file.getvalue()
... st.write(bytes_data)
>>>
... # To convert to a string based IO:
... stringio = StringIO(uploaded_file.getvalue().decode("utf-8"))
... st.write(stringio)
>>>
... # To read file as string:
... string_data = stringio.read()
... st.write(string_data)
>>>
... # Can be used wherever a "file-like" object is accepted:
... dataframe = pd.read_csv(uploaded_file)
... st.write(dataframe)
Insert a file uploader that accepts multiple files at a time:
>>> import streamlit as st
>>>
>>> uploaded_files = st.file_uploader(
... "Choose a CSV file", accept_multiple_files=True
... )
>>> for uploaded_file in uploaded_files:
... bytes_data = uploaded_file.read()
... st.write("filename:", uploaded_file.name)
... st.write(bytes_data)
.. output::
https://doc-file-uploader.streamlit.app/
height: 375px
"""
ctx = get_script_run_ctx()
return self._file_uploader(
label=label,
type=type,
accept_multiple_files=accept_multiple_files,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
disabled=disabled,
label_visibility=label_visibility,
ctx=ctx,
)
def _file_uploader(
self,
label: str,
type: str | Sequence[str] | None = None,
accept_multiple_files: bool = False,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
label_visibility: LabelVisibility = "visible",
disabled: bool = False,
ctx: ScriptRunContext | None = None,
) -> UploadedFile | list[UploadedFile] | None:
key = to_key(key)
check_widget_policies(
self.dg,
key,
on_change,
default_value=None,
writes_allowed=False,
)
maybe_raise_label_warnings(label, label_visibility)
element_id = compute_and_register_element_id(
"file_uploader",
user_key=key,
form_id=current_form_id(self.dg),
label=label,
type=type,
accept_multiple_files=accept_multiple_files,
help=help,
)
if type:
type = normalize_upload_file_type(type)
file_uploader_proto = FileUploaderProto()
file_uploader_proto.id = element_id
file_uploader_proto.label = label
file_uploader_proto.type[:] = type if type is not None else []
file_uploader_proto.max_upload_size_mb = config.get_option(
"server.maxUploadSize"
)
file_uploader_proto.multiple_files = accept_multiple_files
file_uploader_proto.form_id = current_form_id(self.dg)
file_uploader_proto.disabled = disabled
file_uploader_proto.label_visibility.value = get_label_visibility_proto_value(
label_visibility
)
if help is not None:
file_uploader_proto.help = dedent(help)
serde = FileUploaderSerde(accept_multiple_files, allowed_types=type)
# FileUploader's widget value is a list of file IDs
# representing the current set of files that this uploader should
# know about.
widget_state = register_widget(
file_uploader_proto.id,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=serde.deserialize,
serializer=serde.serialize,
ctx=ctx,
value_type="file_uploader_state_value",
)
self.dg._enqueue("file_uploader", file_uploader_proto)
if isinstance(widget_state.value, DeletedFile):
return None
elif isinstance(widget_state.value, list):
return [f for f in widget_state.value if not isinstance(f, DeletedFile)]
return widget_state.value
@property
def dg(self) -> DeltaGenerator:
"""Get our DeltaGenerator."""
return cast("DeltaGenerator", self)

View File

@@ -0,0 +1,339 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from dataclasses import dataclass, field
from textwrap import dedent
from typing import TYPE_CHECKING, Any, Callable, Generic, cast
from streamlit.dataframe_util import OptionSequence
from streamlit.elements.lib.form_utils import current_form_id
from streamlit.elements.lib.options_selector_utils import (
check_and_convert_to_indices,
convert_to_sequence_and_check_comparable,
get_default_indices,
maybe_coerce_enum_sequence,
)
from streamlit.elements.lib.policies import (
check_widget_policies,
maybe_raise_label_warnings,
)
from streamlit.elements.lib.utils import (
Key,
LabelVisibility,
compute_and_register_element_id,
get_label_visibility_proto_value,
save_for_app_testing,
to_key,
)
from streamlit.errors import (
StreamlitSelectionCountExceedsMaxError,
)
from streamlit.proto.MultiSelect_pb2 import MultiSelect as MultiSelectProto
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.runtime.state import register_widget
from streamlit.type_util import (
T,
is_iterable,
)
if TYPE_CHECKING:
from collections.abc import Sequence
from streamlit.dataframe_util import OptionSequence
from streamlit.delta_generator import DeltaGenerator
from streamlit.runtime.state import (
WidgetArgs,
WidgetCallback,
WidgetKwargs,
)
@dataclass
class MultiSelectSerde(Generic[T]):
options: Sequence[T]
default_value: list[int] = field(default_factory=list)
def serialize(self, value: list[T]) -> list[int]:
indices = check_and_convert_to_indices(self.options, value)
return indices if indices is not None else []
def deserialize(
self,
ui_value: list[int] | None,
widget_id: str = "",
) -> list[T]:
current_value: list[int] = (
ui_value if ui_value is not None else self.default_value
)
return [self.options[i] for i in current_value]
def _get_default_count(default: Sequence[Any] | Any | None) -> int:
if default is None:
return 0
if not is_iterable(default):
return 1
return len(cast("Sequence[Any]", default))
def _check_max_selections(
selections: Sequence[Any] | Any | None, max_selections: int | None
):
if max_selections is None:
return
default_count = _get_default_count(selections)
if default_count > max_selections:
raise StreamlitSelectionCountExceedsMaxError(
current_selections_count=default_count, max_selections_count=max_selections
)
class MultiSelectMixin:
@gather_metrics("multiselect")
def multiselect(
self,
label: str,
options: OptionSequence[T],
default: Any | None = None,
format_func: Callable[[Any], Any] = str,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
max_selections: int | None = None,
placeholder: str = "Choose an option",
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> list[T]:
r"""Display a multiselect widget.
The multiselect widget starts as empty.
Parameters
----------
label: str
A short label explaining to the user what this select widget is for.
The label can optionally contain GitHub-flavored Markdown of the
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
and Images. Images display like icons, with a max height equal to
the font height.
Unsupported Markdown elements are unwrapped so only their children
(text contents) render. Display unsupported elements as literal
characters by backslash-escaping them. E.g.,
``"1\. Not an ordered list"``.
See the ``body`` parameter of |st.markdown|_ for additional,
supported Markdown directives.
For accessibility reasons, you should never set an empty label, but
you can hide it with ``label_visibility`` if needed. In the future,
we may disallow empty labels by raising an exception.
.. |st.markdown| replace:: ``st.markdown``
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
options: Iterable
Labels for the select options in an ``Iterable``. This can be a
``list``, ``set``, or anything supported by ``st.dataframe``. If
``options`` is dataframe-like, the first column will be used. Each
label will be cast to ``str`` internally by default.
default: Iterable of V, V, or None
List of default values. Can also be a single value.
format_func: function
Function to modify the display of the options. It receives
the raw option as an argument and should output the label to be
shown for that option. This has no impact on the return value of
the command.
key: str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. No two widgets may have the same key.
help: str or None
A tooltip that gets displayed next to the widget label. Streamlit
only displays the tooltip when ``label_visibility="visible"``. If
this is ``None`` (default), no tooltip is displayed.
The tooltip can optionally contain GitHub-flavored Markdown,
including the Markdown directives described in the ``body``
parameter of ``st.markdown``.
on_change: callable
An optional callback invoked when this widget's value changes.
args: tuple
An optional tuple of args to pass to the callback.
kwargs: dict
An optional dict of kwargs to pass to the callback.
max_selections: int
The max selections that can be selected at a time.
placeholder: str
A string to display when no options are selected.
Defaults to "Choose an option."
disabled: bool
An optional boolean that disables the multiselect widget if set
to ``True``. The default is ``False``.
label_visibility: "visible", "hidden", or "collapsed"
The visibility of the label. The default is ``"visible"``. If this
is ``"hidden"``, Streamlit displays an empty spacer instead of the
label, which can help keep the widget alligned with other widgets.
If this is ``"collapsed"``, Streamlit displays no label or spacer.
Returns
-------
list
A list with the selected options
Example
-------
>>> import streamlit as st
>>>
>>> options = st.multiselect(
... "What are your favorite colors",
... ["Green", "Yellow", "Red", "Blue"],
... ["Yellow", "Red"],
... )
>>>
>>> st.write("You selected:", options)
.. output::
https://doc-multiselect.streamlit.app/
height: 420px
"""
ctx = get_script_run_ctx()
return self._multiselect(
label=label,
options=options,
default=default,
format_func=format_func,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
max_selections=max_selections,
placeholder=placeholder,
disabled=disabled,
label_visibility=label_visibility,
ctx=ctx,
)
def _multiselect(
self,
label: str,
options: OptionSequence[T],
default: Sequence[Any] | Any | None = None,
format_func: Callable[[Any], Any] = str,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
max_selections: int | None = None,
placeholder: str = "Choose an option",
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
ctx: ScriptRunContext | None = None,
) -> list[T]:
key = to_key(key)
widget_name = "multiselect"
check_widget_policies(
self.dg,
key,
on_change,
default_value=default,
)
maybe_raise_label_warnings(label, label_visibility)
indexable_options = convert_to_sequence_and_check_comparable(options)
formatted_options = [format_func(option) for option in indexable_options]
default_values = get_default_indices(indexable_options, default)
form_id = current_form_id(self.dg)
element_id = compute_and_register_element_id(
widget_name,
user_key=key,
form_id=form_id,
label=label,
options=formatted_options,
default=default_values,
help=help,
max_selections=max_selections,
placeholder=placeholder,
)
proto = MultiSelectProto()
proto.id = element_id
proto.default[:] = default_values
proto.form_id = form_id
proto.disabled = disabled
proto.label = label
proto.max_selections = max_selections or 0
proto.placeholder = placeholder
proto.label_visibility.value = get_label_visibility_proto_value(
label_visibility
)
proto.options[:] = formatted_options
if help is not None:
proto.help = dedent(help)
serde = MultiSelectSerde(indexable_options, default_values)
widget_state = register_widget(
proto.id,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=serde.deserialize,
serializer=serde.serialize,
ctx=ctx,
value_type="int_array_value",
)
_check_max_selections(widget_state.value, max_selections)
widget_state = maybe_coerce_enum_sequence(
widget_state, options, indexable_options
)
if widget_state.value_changed:
proto.value[:] = serde.serialize(widget_state.value)
proto.set_value = True
if ctx:
save_for_app_testing(ctx, element_id, format_func)
self.dg._enqueue(widget_name, proto)
return widget_state.value
@property
def dg(self) -> DeltaGenerator:
"""Get our DeltaGenerator."""
return cast("DeltaGenerator", self)

View File

@@ -0,0 +1,562 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import numbers
from dataclasses import dataclass
from textwrap import dedent
from typing import TYPE_CHECKING, Literal, TypeVar, Union, cast, overload
from typing_extensions import TypeAlias
from streamlit.elements.lib.form_utils import current_form_id
from streamlit.elements.lib.js_number import JSNumber, JSNumberBoundsException
from streamlit.elements.lib.policies import (
check_widget_policies,
maybe_raise_label_warnings,
)
from streamlit.elements.lib.utils import (
Key,
LabelVisibility,
compute_and_register_element_id,
get_label_visibility_proto_value,
to_key,
)
from streamlit.errors import (
StreamlitInvalidNumberFormatError,
StreamlitJSNumberBoundsError,
StreamlitMixedNumericTypesError,
StreamlitValueAboveMaxError,
StreamlitValueBelowMinError,
)
from streamlit.proto.NumberInput_pb2 import NumberInput as NumberInputProto
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.runtime.state import (
WidgetArgs,
WidgetCallback,
WidgetKwargs,
get_session_state,
register_widget,
)
if TYPE_CHECKING:
from streamlit.delta_generator import DeltaGenerator
Number: TypeAlias = Union[int, float]
IntOrNone = TypeVar("IntOrNone", int, None)
FloatOrNone = TypeVar("FloatOrNone", float, None)
@dataclass
class NumberInputSerde:
value: Number | None
data_type: int
def serialize(self, v: Number | None) -> Number | None:
return v
def deserialize(
self, ui_value: Number | None, widget_id: str = ""
) -> Number | None:
val: Number | None = ui_value if ui_value is not None else self.value
if val is not None and self.data_type == NumberInputProto.INT:
val = int(val)
return val
class NumberInputMixin:
# For easier readability, all the arguments with un-changing types across these overload signatures have been
# collapsed onto a single line.
# fmt: off
# If "min_value: int" is given and all other numerical inputs are
# "int"s or not provided (value optionally being "min"), return "int"
# If "min_value: int, value: None" is given and all other numerical inputs
# are "int"s or not provided, return "int | None"
@overload
def number_input(
self,
label: str,
min_value: int,
max_value: int | None = None,
value: IntOrNone | Literal["min"] = "min",
step: int | None = None,
format: str | None = None, key: Key | None = None, help: str | None = None, on_change: WidgetCallback | None = None, args: WidgetArgs | None = None, kwargs: WidgetKwargs | None = None, *, placeholder: str | None = None, disabled: bool = False, label_visibility: LabelVisibility = "visible"
) -> int | IntOrNone:
...
# If "max_value: int" is given and all other numerical inputs are
# "int"s or not provided (value optionally being "min"), return "int"
# If "max_value: int, value=None" is given and all other numerical inputs
# are "int"s or not provided, return "int | None"
@overload
def number_input(
self,
label: str,
min_value: int | None = None,
*,
max_value: int,
value: IntOrNone | Literal["min"] = "min",
step: int | None = None,
format: str | None = None, key: Key | None = None, help: str | None = None, on_change: WidgetCallback | None = None, args: WidgetArgs | None = None, kwargs: WidgetKwargs | None = None, placeholder: str | None = None, disabled: bool = False, label_visibility: LabelVisibility = "visible"
) -> int | IntOrNone:
...
# If "value=int" is given and all other numerical inputs are "int"s
# or not provided, return "int"
@overload
def number_input(
self,
label: str,
min_value: int | None = None,
max_value: int | None = None,
*,
value: int,
step: int | None = None,
format: str | None = None, key: Key | None = None, help: str | None = None, on_change: WidgetCallback | None = None, args: WidgetArgs | None = None, kwargs: WidgetKwargs | None = None, placeholder: str | None = None, disabled: bool = False, label_visibility: LabelVisibility = "visible"
) -> int:
...
# If "step=int" is given and all other numerical inputs are "int"s
# or not provided (value optionally being "min"), return "int"
# If "step=int, value=None" is given and all other numerical inputs
# are "int"s or not provided, return "int | None"
@overload
def number_input(
self,
label: str,
min_value: int | None = None,
max_value: int | None = None,
value: IntOrNone | Literal["min"] = "min",
*,
step: int,
format: str | None = None, key: Key | None = None, help: str | None = None, on_change: WidgetCallback | None = None, args: WidgetArgs | None = None, kwargs: WidgetKwargs | None = None, placeholder: str | None = None, disabled: bool = False, label_visibility: LabelVisibility = "visible"
) -> int | IntOrNone:
...
# If all numerical inputs are floats (with value optionally being "min")
# or are not provided, return "float"
# If only "value=None" is given and none of the other numerical inputs
# are "int"s, return "float | None"
@overload
def number_input(
self,
label: str,
min_value: float | None = None,
max_value: float | None = None,
value: FloatOrNone | Literal["min"] = "min",
step: float | None = None,
format: str | None = None, key: Key | None = None, help: str | None = None, on_change: WidgetCallback | None = None, args: WidgetArgs | None = None, kwargs: WidgetKwargs | None = None, *, placeholder: str | None = None, disabled: bool = False, label_visibility: LabelVisibility = "visible"
) -> float | FloatOrNone:
...
# # fmt: on
@gather_metrics("number_input")
def number_input(
self,
label: str,
min_value: Number | None = None,
max_value: Number | None = None,
value: Number | Literal["min"] | None = "min",
step: Number | None = None,
format: str | None = None,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
placeholder: str | None = None,
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> Number | None:
r"""Display a numeric input widget.
.. note::
Integer values exceeding +/- ``(1<<53) - 1`` cannot be accurately
stored or returned by the widget due to serialization contstraints
between the Python server and JavaScript client. You must handle
such numbers as floats, leading to a loss in precision.
Parameters
----------
label : str
A short label explaining to the user what this input is for.
The label can optionally contain GitHub-flavored Markdown of the
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
and Images. Images display like icons, with a max height equal to
the font height.
Unsupported Markdown elements are unwrapped so only their children
(text contents) render. Display unsupported elements as literal
characters by backslash-escaping them. E.g.,
``"1\. Not an ordered list"``.
See the ``body`` parameter of |st.markdown|_ for additional,
supported Markdown directives.
For accessibility reasons, you should never set an empty label, but
you can hide it with ``label_visibility`` if needed. In the future,
we may disallow empty labels by raising an exception.
.. |st.markdown| replace:: ``st.markdown``
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
min_value : int, float, or None
The minimum permitted value.
If this is ``None`` (default), there will be no minimum for float
values and a minimum of ``- (1<<53) + 1`` for integer values.
max_value : int, float, or None
The maximum permitted value.
If this is ``None`` (default), there will be no maximum for float
values and a maximum of ``(1<<53) - 1`` for integer values.
value : int, float, "min" or None
The value of this widget when it first renders. If this is
``"min"`` (default), the initial value is ``min_value`` unless
``min_value`` is ``None``. If ``min_value`` is ``None``, the widget
initializes with a value of ``0.0`` or ``0``.
If ``value`` is ``None``, the widget will initialize with no value
and return ``None`` until the user provides input.
step : int, float, or None
The stepping interval.
Defaults to 1 if the value is an int, 0.01 otherwise.
If the value is not specified, the format parameter will be used.
format : str or None
A printf-style format string controlling how the interface should
display numbers. The output must be purely numeric. This does not
impact the return value of the widget. For more information about
the formatting specification, see `sprintf.js
<https://github.com/alexei/sprintf.js?tab=readme-ov-file#format-specification>`_.
For example, ``format="%0.1f"`` adjusts the displayed decimal
precision to only show one digit after the decimal.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. No two widgets may have the same key.
help : str or None
A tooltip that gets displayed next to the widget label. Streamlit
only displays the tooltip when ``label_visibility="visible"``. If
this is ``None`` (default), no tooltip is displayed.
The tooltip can optionally contain GitHub-flavored Markdown,
including the Markdown directives described in the ``body``
parameter of ``st.markdown``.
on_change : callable
An optional callback invoked when this number_input's value changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
placeholder : str or None
An optional string displayed when the number input is empty.
If None, no placeholder is displayed.
disabled : bool
An optional boolean that disables the number input if set to
``True``. The default is ``False``.
label_visibility : "visible", "hidden", or "collapsed"
The visibility of the label. The default is ``"visible"``. If this
is ``"hidden"``, Streamlit displays an empty spacer instead of the
label, which can help keep the widget alligned with other widgets.
If this is ``"collapsed"``, Streamlit displays no label or spacer.
Returns
-------
int or float or None
The current value of the numeric input widget or ``None`` if the widget
is empty. The return type will match the data type of the value parameter.
Example
-------
>>> import streamlit as st
>>>
>>> number = st.number_input("Insert a number")
>>> st.write("The current number is ", number)
.. output::
https://doc-number-input.streamlit.app/
height: 260px
To initialize an empty number input, use ``None`` as the value:
>>> import streamlit as st
>>>
>>> number = st.number_input(
... "Insert a number", value=None, placeholder="Type a number..."
... )
>>> st.write("The current number is ", number)
.. output::
https://doc-number-input-empty.streamlit.app/
height: 260px
"""
ctx = get_script_run_ctx()
return self._number_input(
label=label,
min_value=min_value,
max_value=max_value,
value=value,
step=step,
format=format,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
placeholder=placeholder,
disabled=disabled,
label_visibility=label_visibility,
ctx=ctx,
)
def _number_input(
self,
label: str,
min_value: Number | None = None,
max_value: Number | None = None,
value: Number | Literal["min"] | None = "min",
step: Number | None = None,
format: str | None = None,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
placeholder: str | None = None,
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
ctx: ScriptRunContext | None = None,
) -> Number | None:
key = to_key(key)
check_widget_policies(
self.dg,
key,
on_change,
default_value=value if value != "min" else None,
)
maybe_raise_label_warnings(label, label_visibility)
element_id = compute_and_register_element_id(
"number_input",
user_key=key,
form_id=current_form_id(self.dg),
label=label,
min_value=min_value,
max_value=max_value,
value=value,
step=step,
format=format,
help=help,
placeholder=None if placeholder is None else str(placeholder),
)
# Ensure that all arguments are of the same type.
number_input_args = [min_value, max_value, value, step]
all_int_args = all(
isinstance(a, (numbers.Integral, type(None), str))
for a in number_input_args
)
all_float_args = all(
isinstance(a, (float, type(None), str)) for a in number_input_args
)
if not all_int_args and not all_float_args:
raise StreamlitMixedNumericTypesError(value=value, min_value=min_value, max_value=max_value, step=step)
session_state = get_session_state().filtered_state
if key is not None and key in session_state and session_state[key] is None:
value = None
if value == "min":
if min_value is not None:
value = min_value
elif all_int_args and all_float_args:
value = 0.0 # if no values are provided, defaults to float
elif all_int_args:
value = 0
else:
value = 0.0
int_value = isinstance(value, numbers.Integral)
float_value = isinstance(value, float)
if value is None:
if all_int_args and not all_float_args:
# Select int type if all relevant args are ints:
int_value = True
else:
# Otherwise, defaults to float:
float_value = True
if format is None:
format = "%d" if int_value else "%0.2f"
# Warn user if they format an int type as a float or vice versa.
if format in ["%d", "%u", "%i"] and float_value:
import streamlit as st
st.warning(
"Warning: NumberInput value below has type float,"
f" but format {format} displays as integer."
)
elif format[-1] == "f" and int_value:
import streamlit as st
st.warning(
"Warning: NumberInput value below has type int so is"
f" displayed as int despite format string {format}."
)
if step is None:
step = 1 if int_value else 0.01
try:
float(format % 2)
except (TypeError, ValueError):
raise StreamlitInvalidNumberFormatError(format)
# Ensure that the value matches arguments' types.
all_ints = int_value and all_int_args
if min_value is not None and value is not None and min_value > value:
raise StreamlitValueBelowMinError(value=value, min_value=min_value)
if max_value is not None and value is not None and max_value < value:
raise StreamlitValueAboveMaxError(value=value, max_value=max_value)
# Bounds checks. JSNumber produces human-readable exceptions that
# we simply re-package as StreamlitAPIExceptions.
try:
if all_ints:
if min_value is not None:
JSNumber.validate_int_bounds(int(min_value), "`min_value`")
else:
# Issue 6740: If min_value not provided, set default to minimum safe integer
# to avoid JS issues from smaller numbers entered via UI
min_value = JSNumber.MIN_SAFE_INTEGER
if max_value is not None:
JSNumber.validate_int_bounds(int(max_value), "`max_value`")
else:
# See note above - set default to max safe integer
max_value = JSNumber.MAX_SAFE_INTEGER
if step is not None:
JSNumber.validate_int_bounds(int(step), "`step`")
if value is not None:
JSNumber.validate_int_bounds(int(value), "`value`")
else:
if min_value is not None:
JSNumber.validate_float_bounds(min_value, "`min_value`")
else:
# See note above
min_value = JSNumber.MIN_NEGATIVE_VALUE
if max_value is not None:
JSNumber.validate_float_bounds(max_value, "`max_value`")
else:
# See note above
max_value = JSNumber.MAX_VALUE
if step is not None:
JSNumber.validate_float_bounds(step, "`step`")
if value is not None:
JSNumber.validate_float_bounds(value, "`value`")
except JSNumberBoundsException as e:
raise StreamlitJSNumberBoundsError(str(e))
data_type = NumberInputProto.INT if all_ints else NumberInputProto.FLOAT
number_input_proto = NumberInputProto()
number_input_proto.id = element_id
number_input_proto.data_type = data_type
number_input_proto.label = label
if value is not None:
number_input_proto.default = value
if placeholder is not None:
number_input_proto.placeholder = str(placeholder)
number_input_proto.form_id = current_form_id(self.dg)
number_input_proto.disabled = disabled
number_input_proto.label_visibility.value = get_label_visibility_proto_value(
label_visibility
)
if help is not None:
number_input_proto.help = dedent(help)
if min_value is not None:
number_input_proto.min = min_value
number_input_proto.has_min = True
if max_value is not None:
number_input_proto.max = max_value
number_input_proto.has_max = True
if step is not None:
number_input_proto.step = step
if format is not None:
number_input_proto.format = format
serde = NumberInputSerde(value, data_type)
widget_state = register_widget(
number_input_proto.id,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=serde.deserialize,
serializer=serde.serialize,
ctx=ctx,
value_type="double_value"
)
if widget_state.value_changed:
if widget_state.value is not None:
# Min/Max bounds checks when the value is updated.
if number_input_proto.has_min and widget_state.value < number_input_proto.min:
raise StreamlitValueBelowMinError(value=widget_state.value, min_value=number_input_proto.min)
if number_input_proto.has_max and widget_state.value > number_input_proto.max:
raise StreamlitValueAboveMaxError(value=widget_state.value, max_value=number_input_proto.max)
number_input_proto.value = widget_state.value
number_input_proto.set_value = True
self.dg._enqueue("number_input", number_input_proto)
return widget_state.value
@property
def dg(self) -> DeltaGenerator:
"""Get our DeltaGenerator."""
return cast("DeltaGenerator", self)

View File

@@ -0,0 +1,407 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from dataclasses import dataclass
from textwrap import dedent
from typing import TYPE_CHECKING, Any, Callable, Generic, cast, overload
from streamlit.dataframe_util import OptionSequence, convert_anything_to_list
from streamlit.elements.lib.form_utils import current_form_id
from streamlit.elements.lib.options_selector_utils import index_, maybe_coerce_enum
from streamlit.elements.lib.policies import (
check_widget_policies,
maybe_raise_label_warnings,
)
from streamlit.elements.lib.utils import (
Key,
LabelVisibility,
compute_and_register_element_id,
get_label_visibility_proto_value,
save_for_app_testing,
to_key,
)
from streamlit.errors import StreamlitAPIException
from streamlit.proto.Radio_pb2 import Radio as RadioProto
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.runtime.state import (
WidgetArgs,
WidgetCallback,
WidgetKwargs,
get_session_state,
register_widget,
)
from streamlit.type_util import (
T,
check_python_comparable,
)
if TYPE_CHECKING:
from collections.abc import Sequence
from streamlit.delta_generator import DeltaGenerator
@dataclass
class RadioSerde(Generic[T]):
options: Sequence[T]
index: int | None
def serialize(self, v: object) -> int | None:
if v is None:
return None
return 0 if len(self.options) == 0 else index_(self.options, v)
def deserialize(
self,
ui_value: int | None,
widget_id: str = "",
) -> T | None:
idx = ui_value if ui_value is not None else self.index
return (
self.options[idx]
if idx is not None
and len(self.options) > 0
and self.options[idx] is not None
else None
)
class RadioMixin:
@overload
def radio(
self,
label: str,
options: OptionSequence[T],
index: int = 0,
format_func: Callable[[Any], Any] = str,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only args:
disabled: bool = False,
horizontal: bool = False,
captions: Sequence[str] | None = None,
label_visibility: LabelVisibility = "visible",
) -> T: ...
@overload
def radio(
self,
label: str,
options: OptionSequence[T],
index: None,
format_func: Callable[[Any], Any] = str,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only args:
disabled: bool = False,
horizontal: bool = False,
captions: Sequence[str] | None = None,
label_visibility: LabelVisibility = "visible",
) -> T | None: ...
@gather_metrics("radio")
def radio(
self,
label: str,
options: OptionSequence[T],
index: int | None = 0,
format_func: Callable[[Any], Any] = str,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only args:
disabled: bool = False,
horizontal: bool = False,
captions: Sequence[str] | None = None,
label_visibility: LabelVisibility = "visible",
) -> T | None:
r"""Display a radio button widget.
Parameters
----------
label : str
A short label explaining to the user what this radio group is for.
The label can optionally contain GitHub-flavored Markdown of the
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
and Images. Images display like icons, with a max height equal to
the font height.
Unsupported Markdown elements are unwrapped so only their children
(text contents) render. Display unsupported elements as literal
characters by backslash-escaping them. E.g.,
``"1\. Not an ordered list"``.
See the ``body`` parameter of |st.markdown|_ for additional,
supported Markdown directives.
For accessibility reasons, you should never set an empty label, but
you can hide it with ``label_visibility`` if needed. In the future,
we may disallow empty labels by raising an exception.
.. |st.markdown| replace:: ``st.markdown``
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
options : Iterable
Labels for the select options in an ``Iterable``. This can be a
``list``, ``set``, or anything supported by ``st.dataframe``. If
``options`` is dataframe-like, the first column will be used. Each
label will be cast to ``str`` internally by default.
Labels can include markdown as described in the ``label`` parameter
and will be cast to str internally by default.
index : int or None
The index of the preselected option on first render. If ``None``,
will initialize empty and return ``None`` until the user selects an option.
Defaults to 0 (the first option).
format_func : function
Function to modify the display of radio options. It receives
the raw option as an argument and should output the label to be
shown for that option. This has no impact on the return value of
the radio.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. No two widgets may have the same key.
help : str or None
A tooltip that gets displayed next to the widget label. Streamlit
only displays the tooltip when ``label_visibility="visible"``. If
this is ``None`` (default), no tooltip is displayed.
The tooltip can optionally contain GitHub-flavored Markdown,
including the Markdown directives described in the ``body``
parameter of ``st.markdown``.
on_change : callable
An optional callback invoked when this radio's value changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
disabled : bool
An optional boolean that disables the radio button if set to
``True``. The default is ``False``.
horizontal : bool
An optional boolean, which orients the radio group horizontally.
The default is false (vertical buttons).
captions : iterable of str or None
A list of captions to show below each radio button. If None (default),
no captions are shown.
label_visibility : "visible", "hidden", or "collapsed"
The visibility of the label. The default is ``"visible"``. If this
is ``"hidden"``, Streamlit displays an empty spacer instead of the
label, which can help keep the widget alligned with other widgets.
If this is ``"collapsed"``, Streamlit displays no label or spacer.
Returns
-------
any
The selected option or ``None`` if no option is selected.
Example
-------
>>> import streamlit as st
>>>
>>> genre = st.radio(
... "What's your favorite movie genre",
... [":rainbow[Comedy]", "***Drama***", "Documentary :movie_camera:"],
... captions=[
... "Laugh out loud.",
... "Get the popcorn.",
... "Never stop learning.",
... ],
... )
>>>
>>> if genre == ":rainbow[Comedy]":
... st.write("You selected comedy.")
... else:
... st.write("You didn't select comedy.")
.. output::
https://doc-radio.streamlit.app/
height: 300px
To initialize an empty radio widget, use ``None`` as the index value:
>>> import streamlit as st
>>>
>>> genre = st.radio(
... "What's your favorite movie genre",
... [":rainbow[Comedy]", "***Drama***", "Documentary :movie_camera:"],
... index=None,
... )
>>>
>>> st.write("You selected:", genre)
.. output::
https://doc-radio-empty.streamlit.app/
height: 300px
"""
ctx = get_script_run_ctx()
return self._radio(
label=label,
options=options,
index=index,
format_func=format_func,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
disabled=disabled,
horizontal=horizontal,
captions=captions,
label_visibility=label_visibility,
ctx=ctx,
)
def _radio(
self,
label: str,
options: OptionSequence[T],
index: int | None = 0,
format_func: Callable[[Any], Any] = str,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only args:
disabled: bool = False,
horizontal: bool = False,
label_visibility: LabelVisibility = "visible",
captions: Sequence[str] | None = None,
ctx: ScriptRunContext | None,
) -> T | None:
key = to_key(key)
check_widget_policies(
self.dg,
key,
on_change,
default_value=None if index == 0 else index,
)
maybe_raise_label_warnings(label, label_visibility)
opt = convert_anything_to_list(options)
check_python_comparable(opt)
element_id = compute_and_register_element_id(
"radio",
user_key=key,
form_id=current_form_id(self.dg),
label=label,
options=[str(format_func(option)) for option in opt],
index=index,
help=help,
horizontal=horizontal,
captions=captions,
)
if not isinstance(index, int) and index is not None:
raise StreamlitAPIException(
"Radio Value has invalid type: %s" % type(index).__name__
)
if index is not None and len(opt) > 0 and not 0 <= index < len(opt):
raise StreamlitAPIException(
"Radio index must be between 0 and length of options"
)
def handle_captions(caption: str | None) -> str:
if caption is None:
return ""
elif isinstance(caption, str):
return caption
else:
raise StreamlitAPIException(
f"Radio captions must be strings. Passed type: {type(caption).__name__}"
)
session_state = get_session_state().filtered_state
if key is not None and key in session_state and session_state[key] is None:
index = None
radio_proto = RadioProto()
radio_proto.id = element_id
radio_proto.label = label
if index is not None:
radio_proto.default = index
radio_proto.options[:] = [str(format_func(option)) for option in opt]
radio_proto.form_id = current_form_id(self.dg)
radio_proto.horizontal = horizontal
radio_proto.disabled = disabled
radio_proto.label_visibility.value = get_label_visibility_proto_value(
label_visibility
)
if captions is not None:
radio_proto.captions[:] = map(handle_captions, captions)
if help is not None:
radio_proto.help = dedent(help)
serde = RadioSerde(opt, index)
widget_state = register_widget(
radio_proto.id,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=serde.deserialize,
serializer=serde.serialize,
ctx=ctx,
value_type="int_value",
)
widget_state = maybe_coerce_enum(widget_state, options, opt)
if widget_state.value_changed:
if widget_state.value is not None:
serialized_value = serde.serialize(widget_state.value)
if serialized_value is not None:
radio_proto.value = serialized_value
radio_proto.set_value = True
if ctx:
save_for_app_testing(ctx, element_id, format_func)
self.dg._enqueue("radio", radio_proto)
return widget_state.value
@property
def dg(self) -> DeltaGenerator:
"""Get our DeltaGenerator."""
return cast("DeltaGenerator", self)

View File

@@ -0,0 +1,435 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from dataclasses import dataclass
from textwrap import dedent
from typing import (
TYPE_CHECKING,
Any,
Callable,
Generic,
cast,
overload,
)
from typing_extensions import TypeGuard
from streamlit.dataframe_util import OptionSequence, convert_anything_to_list
from streamlit.elements.lib.form_utils import current_form_id
from streamlit.elements.lib.options_selector_utils import (
index_,
maybe_coerce_enum,
maybe_coerce_enum_sequence,
)
from streamlit.elements.lib.policies import (
check_widget_policies,
maybe_raise_label_warnings,
)
from streamlit.elements.lib.utils import (
Key,
LabelVisibility,
compute_and_register_element_id,
get_label_visibility_proto_value,
save_for_app_testing,
to_key,
)
from streamlit.errors import StreamlitAPIException
from streamlit.proto.Slider_pb2 import Slider as SliderProto
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.runtime.state import (
WidgetArgs,
WidgetCallback,
WidgetKwargs,
register_widget,
)
from streamlit.type_util import T, check_python_comparable
if TYPE_CHECKING:
from collections.abc import Sequence
from streamlit.delta_generator import DeltaGenerator
from streamlit.runtime.state.common import RegisterWidgetResult
def _is_range_value(value: T | Sequence[T]) -> TypeGuard[Sequence[T]]:
return isinstance(value, (list, tuple))
@dataclass
class SelectSliderSerde(Generic[T]):
options: Sequence[T]
value: list[int]
is_range_value: bool
def serialize(self, v: object) -> list[int]:
return self._as_index_list(v)
def deserialize(
self,
ui_value: list[int] | None,
widget_id: str = "",
) -> T | tuple[T, T]:
if not ui_value:
# Widget has not been used; fallback to the original value,
ui_value = self.value
# The widget always returns floats, so convert to ints before indexing
return_value: tuple[T, T] = cast(
"tuple[T, T]",
tuple(self.options[int(x)] for x in ui_value),
)
# If the original value was a list/tuple, so will be the output (and vice versa)
return return_value if self.is_range_value else return_value[0]
def _as_index_list(self, v: object) -> list[int]:
if _is_range_value(v):
slider_value = [index_(self.options, val) for val in v]
start, end = slider_value
if start > end:
slider_value = [end, start]
return slider_value
else:
return [index_(self.options, v)]
class SelectSliderMixin:
@overload
def select_slider( # type: ignore[overload-overlap]
self,
label: str,
options: OptionSequence[T],
value: tuple[T, T] | list[T],
format_func: Callable[[Any], Any] = str,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> tuple[T, T]: ...
# The overload-overlap error given by mypy here stems from
# the fact that
#
# opt:List[object] = [1, 2, "3"]
# select_slider("foo", options=opt, value=[1, 2])
#
# matches both overloads; "opt" matches
# OptionsSequence[T] in each case, binding T to object.
# However, the list[int] type of "value" can be interpreted
# as subtype of object, or as a subtype of List[object],
# meaning it matches both signatures.
@overload
def select_slider(
self,
label: str,
options: OptionSequence[T] = (),
value: T | None = None,
format_func: Callable[[Any], Any] = str,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> T: ...
@gather_metrics("select_slider")
def select_slider(
self,
label: str,
options: OptionSequence[T] = (),
value: T | Sequence[T] | None = None,
format_func: Callable[[Any], Any] = str,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> T | tuple[T, T]:
r"""
Display a slider widget to select items from a list.
This also allows you to render a range slider by passing a two-element
tuple or list as the ``value``.
The difference between ``st.select_slider`` and ``st.slider`` is that
``select_slider`` accepts any datatype and takes an iterable set of
options, while ``st.slider`` only accepts numerical or date/time data and
takes a range as input.
Parameters
----------
label : str
A short label explaining to the user what this slider is for.
The label can optionally contain GitHub-flavored Markdown of the
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
and Images. Images display like icons, with a max height equal to
the font height.
Unsupported Markdown elements are unwrapped so only their children
(text contents) render. Display unsupported elements as literal
characters by backslash-escaping them. E.g.,
``"1\. Not an ordered list"``.
See the ``body`` parameter of |st.markdown|_ for additional,
supported Markdown directives.
For accessibility reasons, you should never set an empty label, but
you can hide it with ``label_visibility`` if needed. In the future,
we may disallow empty labels by raising an exception.
.. |st.markdown| replace:: ``st.markdown``
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
options : Iterable
Labels for the select options in an ``Iterable``. This can be a
``list``, ``set``, or anything supported by ``st.dataframe``. If
``options`` is dataframe-like, the first column will be used. Each
label will be cast to ``str`` internally by default.
value : a supported type or a tuple/list of supported types or None
The value of the slider when it first renders. If a tuple/list
of two values is passed here, then a range slider with those lower
and upper bounds is rendered. For example, if set to `(1, 10)` the
slider will have a selectable range between 1 and 10.
Defaults to first option.
format_func : function
Function to modify the display of the labels from the options.
argument. It receives the option as an argument and its output
will be cast to str.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. No two widgets may have the same key.
help : str or None
A tooltip that gets displayed next to the widget label. Streamlit
only displays the tooltip when ``label_visibility="visible"``. If
this is ``None`` (default), no tooltip is displayed.
The tooltip can optionally contain GitHub-flavored Markdown,
including the Markdown directives described in the ``body``
parameter of ``st.markdown``.
on_change : callable
An optional callback invoked when this select_slider's value changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
disabled : bool
An optional boolean that disables the select slider if set to
``True``. The default is ``False``.
label_visibility : "visible", "hidden", or "collapsed"
The visibility of the label. The default is ``"visible"``. If this
is ``"hidden"``, Streamlit displays an empty spacer instead of the
label, which can help keep the widget alligned with other widgets.
If this is ``"collapsed"``, Streamlit displays no label or spacer.
Returns
-------
any value or tuple of any value
The current value of the slider widget. The return type will match
the data type of the value parameter.
Examples
--------
>>> import streamlit as st
>>>
>>> color = st.select_slider(
... "Select a color of the rainbow",
... options=[
... "red",
... "orange",
... "yellow",
... "green",
... "blue",
... "indigo",
... "violet",
... ],
... )
>>> st.write("My favorite color is", color)
And here's an example of a range select slider:
>>> import streamlit as st
>>>
>>> start_color, end_color = st.select_slider(
... "Select a range of color wavelength",
... options=[
... "red",
... "orange",
... "yellow",
... "green",
... "blue",
... "indigo",
... "violet",
... ],
... value=("red", "blue"),
... )
>>> st.write("You selected wavelengths between", start_color, "and", end_color)
.. output::
https://doc-select-slider.streamlit.app/
height: 450px
"""
ctx = get_script_run_ctx()
return self._select_slider(
label=label,
options=options,
value=value,
format_func=format_func,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
disabled=disabled,
label_visibility=label_visibility,
ctx=ctx,
)
def _select_slider(
self,
label: str,
options: OptionSequence[T] = (),
value: T | Sequence[T] | None = None,
format_func: Callable[[Any], Any] = str,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
ctx: ScriptRunContext | None = None,
) -> T | tuple[T, T]:
key = to_key(key)
check_widget_policies(
self.dg,
key,
on_change,
default_value=value,
)
maybe_raise_label_warnings(label, label_visibility)
opt = convert_anything_to_list(options)
check_python_comparable(opt)
if len(opt) == 0:
raise StreamlitAPIException("The `options` argument needs to be non-empty")
def as_index_list(v: object) -> list[int]:
if _is_range_value(v):
slider_value = [index_(opt, val) for val in v]
start, end = slider_value
if start > end:
slider_value = [end, start]
return slider_value
else:
# Simplify future logic by always making value a list
try:
return [index_(opt, v)]
except ValueError:
if value is not None:
raise
return [0]
# Convert element to index of the elements
slider_value = as_index_list(value)
element_id = compute_and_register_element_id(
"select_slider",
user_key=key,
form_id=current_form_id(self.dg),
label=label,
options=[str(format_func(option)) for option in opt],
value=slider_value,
help=help,
)
slider_proto = SliderProto()
slider_proto.id = element_id
slider_proto.type = SliderProto.Type.SELECT_SLIDER
slider_proto.label = label
slider_proto.format = "%s"
slider_proto.default[:] = slider_value
slider_proto.min = 0
slider_proto.max = len(opt) - 1
slider_proto.step = 1 # default for index changes
slider_proto.data_type = SliderProto.INT
slider_proto.options[:] = [str(format_func(option)) for option in opt]
slider_proto.form_id = current_form_id(self.dg)
slider_proto.disabled = disabled
slider_proto.label_visibility.value = get_label_visibility_proto_value(
label_visibility
)
if help is not None:
slider_proto.help = dedent(help)
serde = SelectSliderSerde(opt, slider_value, _is_range_value(value))
widget_state = register_widget(
slider_proto.id,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=serde.deserialize,
serializer=serde.serialize,
ctx=ctx,
value_type="double_array_value",
)
if isinstance(widget_state.value, tuple):
widget_state = maybe_coerce_enum_sequence(
cast("RegisterWidgetResult[tuple[T, T]]", widget_state), options, opt
)
else:
widget_state = maybe_coerce_enum(widget_state, options, opt)
if widget_state.value_changed:
slider_proto.value[:] = serde.serialize(widget_state.value)
slider_proto.set_value = True
if ctx:
save_for_app_testing(ctx, element_id, format_func)
self.dg._enqueue("slider", slider_proto)
return widget_state.value
@property
def dg(self) -> DeltaGenerator:
"""Get our DeltaGenerator."""
return cast("DeltaGenerator", self)

View File

@@ -0,0 +1,366 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from dataclasses import dataclass
from textwrap import dedent
from typing import TYPE_CHECKING, Any, Callable, Generic, cast, overload
from streamlit.dataframe_util import OptionSequence, convert_anything_to_list
from streamlit.elements.lib.form_utils import current_form_id
from streamlit.elements.lib.options_selector_utils import index_, maybe_coerce_enum
from streamlit.elements.lib.policies import (
check_widget_policies,
maybe_raise_label_warnings,
)
from streamlit.elements.lib.utils import (
Key,
LabelVisibility,
compute_and_register_element_id,
get_label_visibility_proto_value,
save_for_app_testing,
to_key,
)
from streamlit.errors import StreamlitAPIException
from streamlit.proto.Selectbox_pb2 import Selectbox as SelectboxProto
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.runtime.state import (
WidgetArgs,
WidgetCallback,
WidgetKwargs,
get_session_state,
register_widget,
)
from streamlit.type_util import (
T,
check_python_comparable,
)
if TYPE_CHECKING:
from collections.abc import Sequence
from streamlit.delta_generator import DeltaGenerator
@dataclass
class SelectboxSerde(Generic[T]):
options: Sequence[T]
index: int | None
def serialize(self, v: object) -> int | None:
if v is None:
return None
if len(self.options) == 0:
return 0
return index_(self.options, v)
def deserialize(
self,
ui_value: int | None,
widget_id: str = "",
) -> T | None:
idx = ui_value if ui_value is not None else self.index
return self.options[idx] if idx is not None and len(self.options) > 0 else None
class SelectboxMixin:
@overload
def selectbox(
self,
label: str,
options: OptionSequence[T],
index: int = 0,
format_func: Callable[[Any], Any] = str,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
placeholder: str = "Choose an option",
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> T: ...
@overload
def selectbox(
self,
label: str,
options: OptionSequence[T],
index: None,
format_func: Callable[[Any], Any] = str,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
placeholder: str = "Choose an option",
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> T | None: ...
@gather_metrics("selectbox")
def selectbox(
self,
label: str,
options: OptionSequence[T],
index: int | None = 0,
format_func: Callable[[Any], Any] = str,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
placeholder: str = "Choose an option",
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> T | None:
r"""Display a select widget.
Parameters
----------
label : str
A short label explaining to the user what this select widget is for.
The label can optionally contain GitHub-flavored Markdown of the
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
and Images. Images display like icons, with a max height equal to
the font height.
Unsupported Markdown elements are unwrapped so only their children
(text contents) render. Display unsupported elements as literal
characters by backslash-escaping them. E.g.,
``"1\. Not an ordered list"``.
See the ``body`` parameter of |st.markdown|_ for additional,
supported Markdown directives.
For accessibility reasons, you should never set an empty label, but
you can hide it with ``label_visibility`` if needed. In the future,
we may disallow empty labels by raising an exception.
.. |st.markdown| replace:: ``st.markdown``
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
options : Iterable
Labels for the select options in an ``Iterable``. This can be a
``list``, ``set``, or anything supported by ``st.dataframe``. If
``options`` is dataframe-like, the first column will be used. Each
label will be cast to ``str`` internally by default.
index : int
The index of the preselected option on first render. If ``None``,
will initialize empty and return ``None`` until the user selects an option.
Defaults to 0 (the first option).
format_func : function
Function to modify the display of the options. It receives
the raw option as an argument and should output the label to be
shown for that option. This has no impact on the return value of
the command.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. No two widgets may have the same key.
help : str or None
A tooltip that gets displayed next to the widget label. Streamlit
only displays the tooltip when ``label_visibility="visible"``. If
this is ``None`` (default), no tooltip is displayed.
The tooltip can optionally contain GitHub-flavored Markdown,
including the Markdown directives described in the ``body``
parameter of ``st.markdown``.
on_change : callable
An optional callback invoked when this selectbox's value changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
placeholder : str
A string to display when no options are selected.
Defaults to "Choose an option".
disabled : bool
An optional boolean that disables the selectbox if set to ``True``.
The default is ``False``.
label_visibility : "visible", "hidden", or "collapsed"
The visibility of the label. The default is ``"visible"``. If this
is ``"hidden"``, Streamlit displays an empty spacer instead of the
label, which can help keep the widget alligned with other widgets.
If this is ``"collapsed"``, Streamlit displays no label or spacer.
Returns
-------
any
The selected option or ``None`` if no option is selected.
Example
-------
>>> import streamlit as st
>>>
>>> option = st.selectbox(
... "How would you like to be contacted?",
... ("Email", "Home phone", "Mobile phone"),
... )
>>>
>>> st.write("You selected:", option)
.. output::
https://doc-selectbox.streamlit.app/
height: 320px
To initialize an empty selectbox, use ``None`` as the index value:
>>> import streamlit as st
>>>
>>> option = st.selectbox(
... "How would you like to be contacted?",
... ("Email", "Home phone", "Mobile phone"),
... index=None,
... placeholder="Select contact method...",
... )
>>>
>>> st.write("You selected:", option)
.. output::
https://doc-selectbox-empty.streamlit.app/
height: 320px
"""
ctx = get_script_run_ctx()
return self._selectbox(
label=label,
options=options,
index=index,
format_func=format_func,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
placeholder=placeholder,
disabled=disabled,
label_visibility=label_visibility,
ctx=ctx,
)
def _selectbox(
self,
label: str,
options: OptionSequence[T],
index: int | None = 0,
format_func: Callable[[Any], Any] = str,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
placeholder: str = "Choose an option",
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
ctx: ScriptRunContext | None = None,
) -> T | None:
key = to_key(key)
check_widget_policies(
self.dg,
key,
on_change,
default_value=None if index == 0 else index,
)
maybe_raise_label_warnings(label, label_visibility)
opt = convert_anything_to_list(options)
check_python_comparable(opt)
element_id = compute_and_register_element_id(
"selectbox",
user_key=key,
form_id=current_form_id(self.dg),
label=label,
options=[str(format_func(option)) for option in opt],
index=index,
help=help,
placeholder=placeholder,
)
if not isinstance(index, int) and index is not None:
raise StreamlitAPIException(
"Selectbox Value has invalid type: %s" % type(index).__name__
)
if index is not None and len(opt) > 0 and not 0 <= index < len(opt):
raise StreamlitAPIException(
"Selectbox index must be greater than or equal to 0 and less than the length of options."
)
session_state = get_session_state().filtered_state
if key is not None and key in session_state and session_state[key] is None:
index = None
selectbox_proto = SelectboxProto()
selectbox_proto.id = element_id
selectbox_proto.label = label
if index is not None:
selectbox_proto.default = index
selectbox_proto.options[:] = [str(format_func(option)) for option in opt]
selectbox_proto.form_id = current_form_id(self.dg)
selectbox_proto.placeholder = placeholder
selectbox_proto.disabled = disabled
selectbox_proto.label_visibility.value = get_label_visibility_proto_value(
label_visibility
)
if help is not None:
selectbox_proto.help = dedent(help)
serde = SelectboxSerde(opt, index)
widget_state = register_widget(
selectbox_proto.id,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=serde.deserialize,
serializer=serde.serialize,
ctx=ctx,
value_type="int_value",
)
widget_state = maybe_coerce_enum(widget_state, options, opt)
if widget_state.value_changed:
serialized_value = serde.serialize(widget_state.value)
if serialized_value is not None:
selectbox_proto.value = serialized_value
selectbox_proto.set_value = True
if ctx:
save_for_app_testing(ctx, element_id, format_func)
self.dg._enqueue("selectbox", selectbox_proto)
return widget_state.value
@property
def dg(self) -> DeltaGenerator:
"""Get our DeltaGenerator."""
return cast("DeltaGenerator", self)

View File

@@ -0,0 +1,892 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass
from datetime import date, datetime, time, timedelta, timezone, tzinfo
from numbers import Integral, Real
from textwrap import dedent
from typing import (
TYPE_CHECKING,
Any,
Final,
TypeVar,
Union,
cast,
overload,
)
from typing_extensions import TypeAlias
from streamlit.elements.lib.form_utils import current_form_id
from streamlit.elements.lib.js_number import JSNumber, JSNumberBoundsException
from streamlit.elements.lib.policies import (
check_widget_policies,
maybe_raise_label_warnings,
)
from streamlit.elements.lib.utils import (
Key,
LabelVisibility,
compute_and_register_element_id,
get_label_visibility_proto_value,
to_key,
)
from streamlit.errors import (
StreamlitAPIException,
StreamlitValueAboveMaxError,
StreamlitValueBelowMinError,
)
from streamlit.proto.Slider_pb2 import Slider as SliderProto
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.runtime.state import (
WidgetArgs,
WidgetCallback,
WidgetKwargs,
get_session_state,
register_widget,
)
if TYPE_CHECKING:
from streamlit.delta_generator import DeltaGenerator
SliderNumericT = TypeVar("SliderNumericT", int, float)
SliderDatelikeT = TypeVar("SliderDatelikeT", date, time, datetime)
SliderNumericSpanT: TypeAlias = Union[
list[SliderNumericT],
tuple[()],
tuple[SliderNumericT],
tuple[SliderNumericT, SliderNumericT],
]
SliderDatelikeSpanT: TypeAlias = Union[
list[SliderDatelikeT],
tuple[()],
tuple[SliderDatelikeT],
tuple[SliderDatelikeT, SliderDatelikeT],
]
StepNumericT: TypeAlias = SliderNumericT
StepDatelikeT: TypeAlias = timedelta
SliderStep = Union[int, float, timedelta]
SliderScalar = Union[int, float, date, time, datetime]
SliderValueT = TypeVar("SliderValueT", int, float, date, time, datetime)
SliderValueGeneric: TypeAlias = Union[
SliderValueT,
Sequence[SliderValueT],
]
SliderValue: TypeAlias = Union[
SliderValueGeneric[int],
SliderValueGeneric[float],
SliderValueGeneric[date],
SliderValueGeneric[time],
SliderValueGeneric[datetime],
]
SliderReturnGeneric: TypeAlias = Union[
SliderValueT,
tuple[SliderValueT],
tuple[SliderValueT, SliderValueT],
]
SliderReturn: TypeAlias = Union[
SliderReturnGeneric[int],
SliderReturnGeneric[float],
SliderReturnGeneric[date],
SliderReturnGeneric[time],
SliderReturnGeneric[datetime],
]
SECONDS_TO_MICROS: Final = 1000 * 1000
DAYS_TO_MICROS: Final = 24 * 60 * 60 * SECONDS_TO_MICROS
UTC_EPOCH: Final = datetime(1970, 1, 1, tzinfo=timezone.utc)
def _time_to_datetime(time_: time) -> datetime:
# Note, here we pick an arbitrary date well after Unix epoch.
# This prevents pre-epoch timezone issues (https://bugs.python.org/issue36759)
# We're dropping the date from datetime later, anyway.
return datetime.combine(date(2000, 1, 1), time_)
def _date_to_datetime(date_: date) -> datetime:
return datetime.combine(date_, time())
def _delta_to_micros(delta: timedelta) -> int:
return (
delta.microseconds
+ delta.seconds * SECONDS_TO_MICROS
+ delta.days * DAYS_TO_MICROS
)
def _datetime_to_micros(dt: datetime) -> int:
# The frontend is not aware of timezones and only expects a UTC-based
# timestamp (in microseconds). Since we want to show the date/time exactly
# as it is in the given datetime object, we just set the tzinfo to UTC and
# do not do any timezone conversions. Only the backend knows about
# original timezone and will replace the UTC timestamp in the deserialization.
utc_dt = dt.replace(tzinfo=timezone.utc)
return _delta_to_micros(utc_dt - UTC_EPOCH)
def _micros_to_datetime(micros: int, orig_tz: tzinfo | None) -> datetime:
"""Restore times/datetimes to original timezone (dates are always naive)."""
utc_dt = UTC_EPOCH + timedelta(microseconds=micros)
# Add the original timezone. No conversion is required here,
# since in the serialization, we also just replace the timestamp with UTC.
return utc_dt.replace(tzinfo=orig_tz)
@dataclass
class SliderSerde:
value: list[float]
data_type: int
single_value: bool
orig_tz: tzinfo | None
def deserialize_single_value(self, value: float):
if self.data_type == SliderProto.INT:
return int(value)
if self.data_type == SliderProto.DATETIME:
return _micros_to_datetime(int(value), self.orig_tz)
if self.data_type == SliderProto.DATE:
return _micros_to_datetime(int(value), self.orig_tz).date()
if self.data_type == SliderProto.TIME:
return (
_micros_to_datetime(int(value), self.orig_tz)
.time()
.replace(tzinfo=self.orig_tz)
)
return value
def deserialize(self, ui_value: list[float] | None, widget_id: str = ""):
if ui_value is not None:
val = ui_value
else:
# Widget has not been used; fallback to the original value,
val = self.value
# The widget always returns a float array, so fix the return type if necessary
val = [self.deserialize_single_value(v) for v in val]
return val[0] if self.single_value else tuple(val)
def serialize(self, v: Any) -> list[Any]:
range_value = isinstance(v, (list, tuple))
value = list(v) if range_value else [v]
if self.data_type == SliderProto.DATE:
value = [_datetime_to_micros(_date_to_datetime(v)) for v in value]
if self.data_type == SliderProto.TIME:
value = [_datetime_to_micros(_time_to_datetime(v)) for v in value]
if self.data_type == SliderProto.DATETIME:
value = [_datetime_to_micros(v) for v in value]
return value
class SliderMixin:
# For easier readability, all the arguments with un-changing types across these overload signatures have been
# collapsed onto a single line.
# fmt: off
# If min/max/value/step are not provided, then we return an int.
# if ONLY step is provided, then it must be an int and we return an int.
@overload
def slider(
self,
label: str,
min_value: None = None,
max_value: None = None,
value: None = None,
step: int | None = None,
format: str | None = None, key: Key | None = None, help: str | None = None, on_change: WidgetCallback | None = None, args: WidgetArgs | None = None, kwargs: WidgetKwargs | None = None, *, disabled: bool = False, label_visibility: LabelVisibility = "visible"
) -> int:
...
# If min-value or max_value is provided and a numeric type, and value (if provided)
# is a singular numeric, return the same numeric type.
@overload
def slider(
self,
label: str,
min_value: SliderNumericT | None = None,
max_value: SliderNumericT | None = None,
value: SliderNumericT | None = None,
step: StepNumericT[SliderNumericT] | None = None,
format: str | None = None, key: Key | None = None, help: str | None = None, on_change: WidgetCallback | None = None, args: WidgetArgs | None = None, kwargs: WidgetKwargs | None = None, *, disabled: bool = False, label_visibility: LabelVisibility = "visible"
) -> SliderNumericT:
...
# If value is provided and a sequence of numeric type,
# return a tuple of the same numeric type.
@overload
def slider(
self,
label: str,
min_value: SliderNumericT | None = None,
max_value: SliderNumericT | None = None,
*,
value: SliderNumericSpanT[SliderNumericT],
step: StepNumericT[SliderNumericT] | None = None,
format: str | None = None, key: Key | None = None, help: str | None = None, on_change: WidgetCallback | None = None, args: WidgetArgs | None = None, kwargs: WidgetKwargs | None = None, disabled: bool = False, label_visibility: LabelVisibility = "visible"
) -> tuple[SliderNumericT, SliderNumericT]:
...
# If value is provided positionally and a sequence of numeric type,
# return a tuple of the same numeric type.
@overload
def slider(
self,
label: str,
min_value: SliderNumericT,
max_value: SliderNumericT,
value: SliderNumericSpanT[SliderNumericT],
/,
step: StepNumericT[SliderNumericT] | None = None,
format: str | None = None, key: Key | None = None, help: str | None = None, on_change: WidgetCallback | None = None, args: WidgetArgs | None = None, kwargs: WidgetKwargs | None = None, *, disabled: bool = False, label_visibility: LabelVisibility = "visible"
) -> tuple[SliderNumericT, SliderNumericT]:
...
# If min-value is provided and a datelike type, and value (if provided)
# is a singular datelike, return the same datelike type.
@overload
def slider(
self,
label: str,
min_value: SliderDatelikeT,
max_value: SliderDatelikeT | None = None,
value: SliderDatelikeT | None = None,
step: StepDatelikeT | None = None,
format: str | None = None, key: Key | None = None, help: str | None = None, on_change: WidgetCallback | None = None, args: WidgetArgs | None = None, kwargs: WidgetKwargs | None = None, *, disabled: bool = False, label_visibility: LabelVisibility = "visible"
) -> SliderDatelikeT:
...
# If max-value is provided and a datelike type, and value (if provided)
# is a singular datelike, return the same datelike type.
@overload
def slider(
self,
label: str,
min_value: SliderDatelikeT | None = None,
*,
max_value: SliderDatelikeT,
value: SliderDatelikeT | None = None,
step: StepDatelikeT | None = None,
format: str | None = None, key: Key | None = None, help: str | None = None, on_change: WidgetCallback | None = None, args: WidgetArgs | None = None, kwargs: WidgetKwargs | None = None, disabled: bool = False, label_visibility: LabelVisibility = "visible"
) -> SliderDatelikeT:
...
# If value is provided and a datelike type, return the same datelike type.
@overload
def slider(
self,
label: str,
min_value: SliderDatelikeT | None = None,
max_value: SliderDatelikeT | None = None,
*,
value: SliderDatelikeT,
step: StepDatelikeT | None = None,
format: str | None = None, key: Key | None = None, help: str | None = None, on_change: WidgetCallback | None = None, args: WidgetArgs | None = None, kwargs: WidgetKwargs | None = None, disabled: bool = False, label_visibility: LabelVisibility = "visible"
) -> SliderDatelikeT:
...
# If value is provided and a sequence of datelike type,
# return a tuple of the same datelike type.
@overload
def slider(
self,
label: str,
min_value: SliderDatelikeT | None = None,
max_value: SliderDatelikeT | None = None,
*,
value: SliderDatelikeSpanT[SliderDatelikeT],
step: StepDatelikeT | None = None,
format: str | None = None, key: Key | None = None, help: str | None = None, on_change: WidgetCallback | None = None, args: WidgetArgs | None = None, kwargs: WidgetKwargs | None = None, disabled: bool = False, label_visibility: LabelVisibility = "visible"
) -> tuple[SliderDatelikeT, SliderDatelikeT]:
...
# If value is provided positionally and a sequence of datelike type,
# return a tuple of the same datelike type.
@overload
def slider(
self,
label: str,
min_value: SliderDatelikeT,
max_value: SliderDatelikeT,
value: SliderDatelikeSpanT[SliderDatelikeT],
/,
step: StepDatelikeT | None = None,
format: str | None = None, key: Key | None = None, help: str | None = None, on_change: WidgetCallback | None = None, args: WidgetArgs | None = None, kwargs: WidgetKwargs | None = None, *, disabled: bool = False, label_visibility: LabelVisibility = "visible"
) -> tuple[SliderDatelikeT, SliderDatelikeT]:
...
# fmt: on
# https://github.com/python/mypy/issues/17614
@gather_metrics("slider") # type: ignore[misc]
def slider(
self,
label: str,
min_value: SliderScalar | None = None,
max_value: SliderScalar | None = None,
value: SliderValue | None = None,
step: SliderStep | None = None,
format: str | None = None,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> Any:
r"""Display a slider widget.
This supports int, float, date, time, and datetime types.
This also allows you to render a range slider by passing a two-element
tuple or list as the ``value``.
The difference between ``st.slider`` and ``st.select_slider`` is that
``slider`` only accepts numerical or date/time data and takes a range as
input, while ``select_slider`` accepts any datatype and takes an iterable
set of options.
.. note::
Integer values exceeding +/- ``(1<<53) - 1`` cannot be accurately
stored or returned by the widget due to serialization contstraints
between the Python server and JavaScript client. You must handle
such numbers as floats, leading to a loss in precision.
Parameters
----------
label : str
A short label explaining to the user what this slider is for.
The label can optionally contain GitHub-flavored Markdown of the
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
and Images. Images display like icons, with a max height equal to
the font height.
Unsupported Markdown elements are unwrapped so only their children
(text contents) render. Display unsupported elements as literal
characters by backslash-escaping them. E.g.,
``"1\. Not an ordered list"``.
See the ``body`` parameter of |st.markdown|_ for additional,
supported Markdown directives.
For accessibility reasons, you should never set an empty label, but
you can hide it with ``label_visibility`` if needed. In the future,
we may disallow empty labels by raising an exception.
.. |st.markdown| replace:: ``st.markdown``
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
min_value : a supported type or None
The minimum permitted value.
If this is ``None`` (default), the minimum value depends on the
type as follows:
- integer: ``0``
- float: ``0.0``
- date or datetime: ``value - timedelta(days=14)``
- time: ``time.min``
max_value : a supported type or None
The maximum permitted value.
If this is ``None`` (default), the maximum value depends on the
type as follows:
- integer: ``100``
- float: ``1.0``
- date or datetime: ``value + timedelta(days=14)``
- time: ``time.max``
value : a supported type or a tuple/list of supported types or None
The value of the slider when it first renders. If a tuple/list
of two values is passed here, then a range slider with those lower
and upper bounds is rendered. For example, if set to `(1, 10)` the
slider will have a selectable range between 1 and 10.
This defaults to ``min_value``. If the type is not otherwise
specified in any of the numeric parameters, the widget will have an
integer value.
step : int, float, timedelta, or None
The stepping interval.
Defaults to 1 if the value is an int, 0.01 if a float,
timedelta(days=1) if a date/datetime, timedelta(minutes=15) if a time
(or if max_value - min_value < 1 day)
format : str or None
A printf-style format string controlling how the interface should
display numbers. This does not impact the return value.
For information about formatting integers and floats, see
`sprintf.js
<https://github.com/alexei/sprintf.js?tab=readme-ov-file#format-specification>`_.
For example, ``format="%0.1f"`` adjusts the displayed decimal
precision to only show one digit after the decimal.
For information about formatting datetimes, dates, and times, see
`momentJS <https://momentjs.com/docs/#/displaying/format/>`_.
For example, ``format="ddd ha"`` adjusts the displayed datetime to
show the day of the week and the hour ("Tue 8pm").
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. No two widgets may have the same key.
help : str or None
A tooltip that gets displayed next to the widget label. Streamlit
only displays the tooltip when ``label_visibility="visible"``. If
this is ``None`` (default), no tooltip is displayed.
The tooltip can optionally contain GitHub-flavored Markdown,
including the Markdown directives described in the ``body``
parameter of ``st.markdown``.
on_change : callable
An optional callback invoked when this slider's value changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
disabled : bool
An optional boolean that disables the slider if set to ``True``.
The default is ``False``.
label_visibility : "visible", "hidden", or "collapsed"
The visibility of the label. The default is ``"visible"``. If this
is ``"hidden"``, Streamlit displays an empty spacer instead of the
label, which can help keep the widget alligned with other widgets.
If this is ``"collapsed"``, Streamlit displays no label or spacer.
Returns
-------
int/float/date/time/datetime or tuple of int/float/date/time/datetime
The current value of the slider widget. The return type will match
the data type of the value parameter.
Examples
--------
>>> import streamlit as st
>>>
>>> age = st.slider("How old are you?", 0, 130, 25)
>>> st.write("I'm ", age, "years old")
And here's an example of a range slider:
>>> import streamlit as st
>>>
>>> values = st.slider("Select a range of values", 0.0, 100.0, (25.0, 75.0))
>>> st.write("Values:", values)
This is a range time slider:
>>> import streamlit as st
>>> from datetime import time
>>>
>>> appointment = st.slider(
... "Schedule your appointment:", value=(time(11, 30), time(12, 45))
... )
>>> st.write("You're scheduled for:", appointment)
Finally, a datetime slider:
>>> import streamlit as st
>>> from datetime import datetime
>>>
>>> start_time = st.slider(
... "When do you start?",
... value=datetime(2020, 1, 1, 9, 30),
... format="MM/DD/YY - hh:mm",
... )
>>> st.write("Start time:", start_time)
.. output::
https://doc-slider.streamlit.app/
height: 300px
"""
ctx = get_script_run_ctx()
return self._slider(
label=label,
min_value=min_value,
max_value=max_value,
value=value,
step=step,
format=format,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
disabled=disabled,
label_visibility=label_visibility,
ctx=ctx,
)
def _slider(
self,
label: str,
min_value=None,
max_value=None,
value=None,
step=None,
format: str | None = None,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
ctx: ScriptRunContext | None = None,
) -> SliderReturn:
key = to_key(key)
check_widget_policies(
self.dg,
key,
on_change,
default_value=value,
)
maybe_raise_label_warnings(label, label_visibility)
element_id = compute_and_register_element_id(
"slider",
user_key=key,
form_id=current_form_id(self.dg),
label=label,
min_value=min_value,
max_value=max_value,
value=value,
step=step,
format=format,
help=help,
)
SUPPORTED_TYPES = {
Integral: SliderProto.INT,
Real: SliderProto.FLOAT,
datetime: SliderProto.DATETIME,
date: SliderProto.DATE,
time: SliderProto.TIME,
}
TIMELIKE_TYPES = (SliderProto.DATETIME, SliderProto.TIME, SliderProto.DATE)
if value is None:
# We need to know if this is a single or range slider, but don't have
# a default value, so we check if session_state can tell us.
# We already calcluated the id, so there is no risk of this causing
# the id to change.
single_value = True
session_state = get_session_state().filtered_state
if key is not None and key in session_state:
state_value = session_state[key]
single_value = isinstance(state_value, tuple(SUPPORTED_TYPES.keys()))
if single_value:
value = min_value if min_value is not None else 0
else:
mn = min_value if min_value is not None else 0
mx = max_value if max_value is not None else 100
value = [mn, mx]
# Ensure that the value is either a single value or a range of values.
single_value = isinstance(value, tuple(SUPPORTED_TYPES.keys()))
range_value = isinstance(value, (list, tuple)) and len(value) in (0, 1, 2)
if not single_value and not range_value:
raise StreamlitAPIException(
"Slider value should either be an int/float/datetime or a list/tuple of "
"0 to 2 ints/floats/datetimes"
)
# Simplify future logic by always making value a list
if single_value:
value = [value]
def value_to_generic_type(v):
if isinstance(v, Integral):
return SUPPORTED_TYPES[Integral]
elif isinstance(v, Real):
return SUPPORTED_TYPES[Real]
else:
return SUPPORTED_TYPES[type(v)]
def all_same_type(items):
return len(set(map(value_to_generic_type, items))) < 2
if not all_same_type(value):
raise StreamlitAPIException(
"Slider tuple/list components must be of the same type.\n"
f"But were: {list(map(type, value))}"
)
if len(value) == 0:
data_type = SliderProto.INT
else:
data_type = value_to_generic_type(value[0])
datetime_min = time.min
datetime_max = time.max
if data_type == SliderProto.TIME:
datetime_min = time.min.replace(tzinfo=value[0].tzinfo)
datetime_max = time.max.replace(tzinfo=value[0].tzinfo)
if data_type in (SliderProto.DATETIME, SliderProto.DATE):
datetime_min = value[0] - timedelta(days=14)
datetime_max = value[0] + timedelta(days=14)
DEFAULTS = {
SliderProto.INT: {
"min_value": 0,
"max_value": 100,
"step": 1,
"format": "%d",
},
SliderProto.FLOAT: {
"min_value": 0.0,
"max_value": 1.0,
"step": 0.01,
"format": "%0.2f",
},
SliderProto.DATETIME: {
"min_value": datetime_min,
"max_value": datetime_max,
"step": timedelta(days=1),
"format": "YYYY-MM-DD",
},
SliderProto.DATE: {
"min_value": datetime_min,
"max_value": datetime_max,
"step": timedelta(days=1),
"format": "YYYY-MM-DD",
},
SliderProto.TIME: {
"min_value": datetime_min,
"max_value": datetime_max,
"step": timedelta(minutes=15),
"format": "HH:mm",
},
}
if min_value is None:
min_value = DEFAULTS[data_type]["min_value"]
if max_value is None:
max_value = DEFAULTS[data_type]["max_value"]
if step is None:
step = DEFAULTS[data_type]["step"]
if data_type in (
SliderProto.DATETIME,
SliderProto.DATE,
) and max_value - min_value < timedelta(days=1):
step = timedelta(minutes=15)
if format is None:
format = cast("str", DEFAULTS[data_type]["format"])
if step == 0:
raise StreamlitAPIException(
"Slider components cannot be passed a `step` of 0."
)
# Ensure that all arguments are of the same type.
slider_args = [min_value, max_value, step]
int_args = all(isinstance(a, Integral) for a in slider_args)
float_args = all(
isinstance(a, Real) and not isinstance(a, Integral) for a in slider_args
)
# When min and max_value are the same timelike, step should be a timedelta
timelike_args = (
data_type in TIMELIKE_TYPES
and isinstance(step, timedelta)
and type(min_value) is type(max_value)
)
if not int_args and not float_args and not timelike_args:
raise StreamlitAPIException(
"Slider value arguments must be of matching types."
"\n`min_value` has %(min_type)s type."
"\n`max_value` has %(max_type)s type."
"\n`step` has %(step)s type."
% {
"min_type": type(min_value).__name__,
"max_type": type(max_value).__name__,
"step": type(step).__name__,
}
)
# Ensure that the value matches arguments' types.
all_ints = data_type == SliderProto.INT and int_args
all_floats = data_type == SliderProto.FLOAT and float_args
all_timelikes = data_type in TIMELIKE_TYPES and timelike_args
if not all_ints and not all_floats and not all_timelikes:
raise StreamlitAPIException(
"Both value and arguments must be of the same type."
"\n`value` has %(value_type)s type."
"\n`min_value` has %(min_type)s type."
"\n`max_value` has %(max_type)s type."
% {
"value_type": type(value).__name__,
"min_type": type(min_value).__name__,
"max_type": type(max_value).__name__,
}
)
# Ensure that min <= value(s) <= max, adjusting the bounds as necessary.
min_value = min(min_value, max_value)
max_value = max(min_value, max_value)
if len(value) == 1:
min_value = min(value[0], min_value)
max_value = max(value[0], max_value)
elif len(value) == 2:
start, end = value
if start > end:
# Swap start and end, since they seem reversed
start, end = end, start
value = start, end
min_value = min(start, min_value)
max_value = max(end, max_value)
else:
# Empty list, so let's just use the outer bounds
value = [min_value, max_value]
# Bounds checks. JSNumber produces human-readable exceptions that
# we simply re-package as StreamlitAPIExceptions.
# (We check `min_value` and `max_value` here; `value` and `step` are
# already known to be in the [min_value, max_value] range.)
try:
if all_ints:
JSNumber.validate_int_bounds(min_value, "`min_value`")
JSNumber.validate_int_bounds(max_value, "`max_value`")
elif all_floats:
JSNumber.validate_float_bounds(min_value, "`min_value`")
JSNumber.validate_float_bounds(max_value, "`max_value`")
elif all_timelikes:
# No validation yet. TODO: check between 0001-01-01 to 9999-12-31
pass
except JSNumberBoundsException as e:
raise StreamlitAPIException(str(e))
orig_tz = None
# Convert dates or times into datetimes
if data_type == SliderProto.TIME:
value = list(map(_time_to_datetime, value))
min_value = _time_to_datetime(min_value)
max_value = _time_to_datetime(max_value)
if data_type == SliderProto.DATE:
value = list(map(_date_to_datetime, value))
min_value = _date_to_datetime(min_value)
max_value = _date_to_datetime(max_value)
# The frontend will error if the values are equal, so checking here
# lets us produce a nicer python error message and stack trace.
if min_value == max_value:
raise StreamlitAPIException(
"Slider `min_value` must be less than the `max_value`."
f"\nThe values were {min_value} and {max_value}."
)
# Now, convert to microseconds (so we can serialize datetime to a long)
if data_type in TIMELIKE_TYPES:
# Restore times/datetimes to original timezone (dates are always naive)
orig_tz = (
value[0].tzinfo
if data_type in (SliderProto.TIME, SliderProto.DATETIME)
else None
)
value = list(map(_datetime_to_micros, value))
min_value = _datetime_to_micros(min_value)
max_value = _datetime_to_micros(max_value)
step = _delta_to_micros(cast("timedelta", step))
# It would be great if we could guess the number of decimal places from
# the `step` argument, but this would only be meaningful if step were a
# decimal. As a possible improvement we could make this function accept
# decimals and/or use some heuristics for floats.
slider_proto = SliderProto()
slider_proto.type = SliderProto.Type.SLIDER
slider_proto.id = element_id
slider_proto.label = label
slider_proto.format = format
slider_proto.default[:] = value
slider_proto.min = min_value
slider_proto.max = max_value
slider_proto.step = cast("float", step)
slider_proto.data_type = data_type
slider_proto.options[:] = []
slider_proto.form_id = current_form_id(self.dg)
slider_proto.disabled = disabled
slider_proto.label_visibility.value = get_label_visibility_proto_value(
label_visibility
)
if help is not None:
slider_proto.help = dedent(help)
serde = SliderSerde(value, data_type, single_value, orig_tz)
widget_state = register_widget(
slider_proto.id,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=serde.deserialize,
serializer=serde.serialize,
ctx=ctx,
value_type="double_array_value",
)
if widget_state.value_changed:
# Min/Max bounds checks when the value is updated.
serialized_values = serde.serialize(widget_state.value)
for value in serialized_values:
# Use the deserialized values for more readable error messages for dates/times
deserialized_value = serde.deserialize_single_value(value)
if value < slider_proto.min:
raise StreamlitValueBelowMinError(
value=deserialized_value,
min_value=serde.deserialize_single_value(slider_proto.min),
)
if value > slider_proto.max:
raise StreamlitValueAboveMaxError(
value=deserialized_value,
max_value=serde.deserialize_single_value(slider_proto.max),
)
slider_proto.value[:] = serialized_values
slider_proto.set_value = True
self.dg._enqueue("slider", slider_proto)
return cast("SliderReturn", widget_state.value)
@property
def dg(self) -> DeltaGenerator:
"""Get our DeltaGenerator."""
return cast("DeltaGenerator", self)

View File

@@ -0,0 +1,629 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
from dataclasses import dataclass
from textwrap import dedent
from typing import TYPE_CHECKING, Literal, cast, overload
from streamlit.elements.lib.form_utils import current_form_id
from streamlit.elements.lib.policies import (
check_widget_policies,
maybe_raise_label_warnings,
)
from streamlit.elements.lib.utils import (
Key,
LabelVisibility,
compute_and_register_element_id,
get_label_visibility_proto_value,
to_key,
)
from streamlit.errors import StreamlitAPIException
from streamlit.proto.TextArea_pb2 import TextArea as TextAreaProto
from streamlit.proto.TextInput_pb2 import TextInput as TextInputProto
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.runtime.state import (
WidgetArgs,
WidgetCallback,
WidgetKwargs,
get_session_state,
register_widget,
)
from streamlit.type_util import (
SupportsStr,
)
if TYPE_CHECKING:
from streamlit.delta_generator import DeltaGenerator
from streamlit.type_util import SupportsStr
@dataclass
class TextInputSerde:
value: str | None
def deserialize(self, ui_value: str | None, widget_id: str = "") -> str | None:
return ui_value if ui_value is not None else self.value
def serialize(self, v: str | None) -> str | None:
return v
@dataclass
class TextAreaSerde:
value: str | None
def deserialize(self, ui_value: str | None, widget_id: str = "") -> str | None:
return ui_value if ui_value is not None else self.value
def serialize(self, v: str | None) -> str | None:
return v
class TextWidgetsMixin:
@overload
def text_input(
self,
label: str,
value: str = "",
max_chars: int | None = None,
key: Key | None = None,
type: Literal["default", "password"] = "default",
help: str | None = None,
autocomplete: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
placeholder: str | None = None,
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> str:
pass
@overload
def text_input(
self,
label: str,
value: SupportsStr | None = None,
max_chars: int | None = None,
key: Key | None = None,
type: Literal["default", "password"] = "default",
help: str | None = None,
autocomplete: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
placeholder: str | None = None,
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> str | None:
pass
@gather_metrics("text_input")
def text_input(
self,
label: str,
value: str | SupportsStr | None = "",
max_chars: int | None = None,
key: Key | None = None,
type: Literal["default", "password"] = "default",
help: str | None = None,
autocomplete: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
placeholder: str | None = None,
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> str | None:
r"""Display a single-line text input widget.
Parameters
----------
label : str
A short label explaining to the user what this input is for.
The label can optionally contain GitHub-flavored Markdown of the
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
and Images. Images display like icons, with a max height equal to
the font height.
Unsupported Markdown elements are unwrapped so only their children
(text contents) render. Display unsupported elements as literal
characters by backslash-escaping them. E.g.,
``"1\. Not an ordered list"``.
See the ``body`` parameter of |st.markdown|_ for additional,
supported Markdown directives.
For accessibility reasons, you should never set an empty label, but
you can hide it with ``label_visibility`` if needed. In the future,
we may disallow empty labels by raising an exception.
.. |st.markdown| replace:: ``st.markdown``
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
value : object or None
The text value of this widget when it first renders. This will be
cast to str internally. If ``None``, will initialize empty and
return ``None`` until the user provides input. Defaults to empty string.
max_chars : int or None
Max number of characters allowed in text input.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. No two widgets may have the same key.
type : "default" or "password"
The type of the text input. This can be either "default" (for
a regular text input), or "password" (for a text input that
masks the user's typed value). Defaults to "default".
help : str or None
A tooltip that gets displayed next to the widget label. Streamlit
only displays the tooltip when ``label_visibility="visible"``. If
this is ``None`` (default), no tooltip is displayed.
The tooltip can optionally contain GitHub-flavored Markdown,
including the Markdown directives described in the ``body``
parameter of ``st.markdown``.
autocomplete : str
An optional value that will be passed to the <input> element's
autocomplete property. If unspecified, this value will be set to
"new-password" for "password" inputs, and the empty string for
"default" inputs. For more details, see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete
on_change : callable
An optional callback invoked when this text input's value changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
placeholder : str or None
An optional string displayed when the text input is empty. If None,
no text is displayed.
disabled : bool
An optional boolean that disables the text input if set to
``True``. The default is ``False``.
label_visibility : "visible", "hidden", or "collapsed"
The visibility of the label. The default is ``"visible"``. If this
is ``"hidden"``, Streamlit displays an empty spacer instead of the
label, which can help keep the widget alligned with other widgets.
If this is ``"collapsed"``, Streamlit displays no label or spacer.
Returns
-------
str or None
The current value of the text input widget or ``None`` if no value has been
provided by the user.
Example
-------
>>> import streamlit as st
>>>
>>> title = st.text_input("Movie title", "Life of Brian")
>>> st.write("The current movie title is", title)
.. output::
https://doc-text-input.streamlit.app/
height: 260px
"""
ctx = get_script_run_ctx()
return self._text_input(
label=label,
value=value,
max_chars=max_chars,
key=key,
type=type,
help=help,
autocomplete=autocomplete,
on_change=on_change,
args=args,
kwargs=kwargs,
placeholder=placeholder,
disabled=disabled,
label_visibility=label_visibility,
ctx=ctx,
)
def _text_input(
self,
label: str,
value: SupportsStr | None = "",
max_chars: int | None = None,
key: Key | None = None,
type: str = "default",
help: str | None = None,
autocomplete: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
placeholder: str | None = None,
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
ctx: ScriptRunContext | None = None,
) -> str | None:
key = to_key(key)
check_widget_policies(
self.dg,
key,
on_change,
default_value=None if value == "" else value,
)
maybe_raise_label_warnings(label, label_visibility)
# Make sure value is always string or None:
value = str(value) if value is not None else None
element_id = compute_and_register_element_id(
"text_input",
user_key=key,
form_id=current_form_id(self.dg),
label=label,
value=value,
max_chars=max_chars,
type=type,
help=help,
autocomplete=autocomplete,
placeholder=str(placeholder),
)
session_state = get_session_state().filtered_state
if key is not None and key in session_state and session_state[key] is None:
value = None
text_input_proto = TextInputProto()
text_input_proto.id = element_id
text_input_proto.label = label
if value is not None:
text_input_proto.default = value
text_input_proto.form_id = current_form_id(self.dg)
text_input_proto.disabled = disabled
text_input_proto.label_visibility.value = get_label_visibility_proto_value(
label_visibility
)
if help is not None:
text_input_proto.help = dedent(help)
if max_chars is not None:
text_input_proto.max_chars = max_chars
if placeholder is not None:
text_input_proto.placeholder = str(placeholder)
if type == "default":
text_input_proto.type = TextInputProto.DEFAULT
elif type == "password":
text_input_proto.type = TextInputProto.PASSWORD
else:
raise StreamlitAPIException(
"'%s' is not a valid text_input type. Valid types are 'default' and 'password'."
% type
)
# Marshall the autocomplete param. If unspecified, this will be
# set to "new-password" for password inputs.
if autocomplete is None:
autocomplete = "new-password" if type == "password" else ""
text_input_proto.autocomplete = autocomplete
serde = TextInputSerde(value)
widget_state = register_widget(
text_input_proto.id,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=serde.deserialize,
serializer=serde.serialize,
ctx=ctx,
value_type="string_value",
)
if widget_state.value_changed:
if widget_state.value is not None:
text_input_proto.value = widget_state.value
text_input_proto.set_value = True
self.dg._enqueue("text_input", text_input_proto)
return widget_state.value
@overload
def text_area(
self,
label: str,
value: str = "",
height: int | None = None,
max_chars: int | None = None,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
placeholder: str | None = None,
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> str:
pass
@overload
def text_area(
self,
label: str,
value: SupportsStr | None = None,
height: int | None = None,
max_chars: int | None = None,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
placeholder: str | None = None,
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> str | None:
pass
@gather_metrics("text_area")
def text_area(
self,
label: str,
value: str | SupportsStr | None = "",
height: int | None = None,
max_chars: int | None = None,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
placeholder: str | None = None,
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> str | None:
r"""Display a multi-line text input widget.
Parameters
----------
label : str
A short label explaining to the user what this input is for.
The label can optionally contain GitHub-flavored Markdown of the
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
and Images. Images display like icons, with a max height equal to
the font height.
Unsupported Markdown elements are unwrapped so only their children
(text contents) render. Display unsupported elements as literal
characters by backslash-escaping them. E.g.,
``"1\. Not an ordered list"``.
See the ``body`` parameter of |st.markdown|_ for additional,
supported Markdown directives.
For accessibility reasons, you should never set an empty label, but
you can hide it with ``label_visibility`` if needed. In the future,
we may disallow empty labels by raising an exception.
.. |st.markdown| replace:: ``st.markdown``
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
value : object or None
The text value of this widget when it first renders. This will be
cast to str internally. If ``None``, will initialize empty and
return ``None`` until the user provides input. Defaults to empty string.
height : int or None
Desired height of the UI element expressed in pixels. If this is
``None`` (default), the widget's initial height fits three lines.
The height must be at least 68 pixels, which fits two lines.
max_chars : int or None
Maximum number of characters allowed in text area.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. No two widgets may have the same key.
help : str or None
A tooltip that gets displayed next to the widget label. Streamlit
only displays the tooltip when ``label_visibility="visible"``. If
this is ``None`` (default), no tooltip is displayed.
The tooltip can optionally contain GitHub-flavored Markdown,
including the Markdown directives described in the ``body``
parameter of ``st.markdown``.
on_change : callable
An optional callback invoked when this text_area's value changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
placeholder : str or None
An optional string displayed when the text area is empty. If None,
no text is displayed.
disabled : bool
An optional boolean that disables the text area if set to ``True``.
The default is ``False``.
label_visibility : "visible", "hidden", or "collapsed"
The visibility of the label. The default is ``"visible"``. If this
is ``"hidden"``, Streamlit displays an empty spacer instead of the
label, which can help keep the widget alligned with other widgets.
If this is ``"collapsed"``, Streamlit displays no label or spacer.
Returns
-------
str or None
The current value of the text area widget or ``None`` if no value has been
provided by the user.
Example
-------
>>> import streamlit as st
>>>
>>> txt = st.text_area(
... "Text to analyze",
... "It was the best of times, it was the worst of times, it was the age of "
... "wisdom, it was the age of foolishness, it was the epoch of belief, it "
... "was the epoch of incredulity, it was the season of Light, it was the "
... "season of Darkness, it was the spring of hope, it was the winter of "
... "despair, (...)",
... )
>>>
>>> st.write(f"You wrote {len(txt)} characters.")
.. output::
https://doc-text-area.streamlit.app/
height: 300px
"""
# Specified height must be at least 68 pixels (3 lines of text).
if height is not None and height < 68:
raise StreamlitAPIException(
f"Invalid height {height}px for `st.text_area` - must be at least 68 pixels."
)
ctx = get_script_run_ctx()
return self._text_area(
label=label,
value=value,
height=height,
max_chars=max_chars,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
placeholder=placeholder,
disabled=disabled,
label_visibility=label_visibility,
ctx=ctx,
)
def _text_area(
self,
label: str,
value: SupportsStr | None = "",
height: int | None = None,
max_chars: int | None = None,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
placeholder: str | None = None,
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
ctx: ScriptRunContext | None = None,
) -> str | None:
key = to_key(key)
check_widget_policies(
self.dg,
key,
on_change,
default_value=None if value == "" else value,
)
maybe_raise_label_warnings(label, label_visibility)
value = str(value) if value is not None else None
element_id = compute_and_register_element_id(
"text_area",
user_key=key,
form_id=current_form_id(self.dg),
label=label,
value=value,
height=height,
max_chars=max_chars,
help=help,
placeholder=str(placeholder),
)
session_state = get_session_state().filtered_state
if key is not None and key in session_state and session_state[key] is None:
value = None
text_area_proto = TextAreaProto()
text_area_proto.id = element_id
text_area_proto.label = label
if value is not None:
text_area_proto.default = value
text_area_proto.form_id = current_form_id(self.dg)
text_area_proto.disabled = disabled
text_area_proto.label_visibility.value = get_label_visibility_proto_value(
label_visibility
)
if help is not None:
text_area_proto.help = dedent(help)
if height is not None:
text_area_proto.height = height
if max_chars is not None:
text_area_proto.max_chars = max_chars
if placeholder is not None:
text_area_proto.placeholder = str(placeholder)
serde = TextAreaSerde(value)
widget_state = register_widget(
text_area_proto.id,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=serde.deserialize,
serializer=serde.serialize,
ctx=ctx,
value_type="string_value",
)
if widget_state.value_changed:
if widget_state.value is not None:
text_area_proto.value = widget_state.value
text_area_proto.set_value = True
self.dg._enqueue("text_area", text_area_proto)
return widget_state.value
@property
def dg(self) -> DeltaGenerator:
"""Get our DeltaGenerator."""
return cast("DeltaGenerator", self)

View File

@@ -0,0 +1,970 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import re
from collections.abc import Sequence
from dataclasses import dataclass
from datetime import date, datetime, time, timedelta
from textwrap import dedent
from typing import (
TYPE_CHECKING,
Any,
Final,
Literal,
Union,
cast,
overload,
)
from typing_extensions import TypeAlias
from streamlit.elements.lib.form_utils import current_form_id
from streamlit.elements.lib.policies import (
check_widget_policies,
maybe_raise_label_warnings,
)
from streamlit.elements.lib.utils import (
Key,
LabelVisibility,
compute_and_register_element_id,
get_label_visibility_proto_value,
to_key,
)
from streamlit.errors import StreamlitAPIException
from streamlit.proto.DateInput_pb2 import DateInput as DateInputProto
from streamlit.proto.TimeInput_pb2 import TimeInput as TimeInputProto
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.runtime.state import (
WidgetArgs,
WidgetCallback,
WidgetKwargs,
get_session_state,
register_widget,
)
from streamlit.time_util import adjust_years
if TYPE_CHECKING:
from streamlit.delta_generator import DeltaGenerator
# Type for things that point to a specific time (even if a default time, though not None).
TimeValue: TypeAlias = Union[time, datetime, str, Literal["now"]]
# Type for things that point to a specific date (even if a default date, including None).
NullableScalarDateValue: TypeAlias = Union[date, datetime, str, Literal["today"], None]
# The accepted input value for st.date_input. Can be a date scalar or a date range.
DateValue: TypeAlias = Union[NullableScalarDateValue, Sequence[NullableScalarDateValue]]
# The return value of st.date_input.
DateWidgetRangeReturn: TypeAlias = Union[
tuple[()],
tuple[date],
tuple[date, date],
]
DateWidgetReturn: TypeAlias = Union[date, DateWidgetRangeReturn, None]
DEFAULT_STEP_MINUTES: Final = 15
ALLOWED_DATE_FORMATS: Final = re.compile(
r"^(YYYY[/.\-]MM[/.\-]DD|DD[/.\-]MM[/.\-]YYYY|MM[/.\-]DD[/.\-]YYYY)$"
)
def _convert_timelike_to_time(value: TimeValue) -> time:
if value == "now":
# Set value default.
return datetime.now().time().replace(second=0, microsecond=0)
if isinstance(value, str):
try:
return time.fromisoformat(value)
except ValueError:
try:
return (
datetime.fromisoformat(value)
.time()
.replace(second=0, microsecond=0)
)
except ValueError:
# We throw an error below.
pass
if isinstance(value, datetime):
return value.time().replace(second=0, microsecond=0)
if isinstance(value, time):
return value
raise StreamlitAPIException(
"The type of value should be one of datetime, time, ISO string or None"
)
def _convert_datelike_to_date(
value: NullableScalarDateValue,
) -> date:
if isinstance(value, datetime):
return value.date()
if isinstance(value, date):
return value
if value in {"today"}:
return datetime.now().date()
if isinstance(value, str):
try:
return date.fromisoformat(value)
except ValueError:
try:
return datetime.fromisoformat(value).date()
except ValueError:
# We throw an error below.
pass
raise StreamlitAPIException(
'Date value should either be an date/datetime or an ISO string or "today"'
)
def _parse_date_value(value: DateValue) -> tuple[list[date] | None, bool]:
if value is None:
return None, False
value_tuple: Sequence[NullableScalarDateValue]
if isinstance(value, Sequence) and not isinstance(value, str):
is_range = True
value_tuple = value
else:
is_range = False
value_tuple = [cast("NullableScalarDateValue", value)]
if len(value_tuple) not in {0, 1, 2}:
raise StreamlitAPIException(
"DateInput value should either be an date/datetime or a list/tuple of "
"0 - 2 date/datetime values"
)
parsed_dates = [_convert_datelike_to_date(v) for v in value_tuple]
return parsed_dates, is_range
def _parse_min_date(
min_value: NullableScalarDateValue,
parsed_dates: Sequence[date] | None,
) -> date:
parsed_min_date: date
if isinstance(min_value, (datetime, date, str)):
parsed_min_date = _convert_datelike_to_date(min_value)
elif min_value is None:
if parsed_dates:
parsed_min_date = adjust_years(parsed_dates[0], years=-10)
else:
parsed_min_date = adjust_years(date.today(), years=-10)
else:
raise StreamlitAPIException(
"DateInput min should either be a date/datetime or None"
)
return parsed_min_date
def _parse_max_date(
max_value: NullableScalarDateValue,
parsed_dates: Sequence[date] | None,
) -> date:
parsed_max_date: date
if isinstance(max_value, (datetime, date, str)):
parsed_max_date = _convert_datelike_to_date(max_value)
elif max_value is None:
if parsed_dates:
parsed_max_date = adjust_years(parsed_dates[-1], years=10)
else:
parsed_max_date = adjust_years(date.today(), years=10)
else:
raise StreamlitAPIException(
"DateInput max should either be a date/datetime or None"
)
return parsed_max_date
@dataclass(frozen=True)
class _DateInputValues:
value: Sequence[date] | None
is_range: bool
max: date
min: date
@classmethod
def from_raw_values(
cls,
value: DateValue,
min_value: NullableScalarDateValue,
max_value: NullableScalarDateValue,
) -> _DateInputValues:
parsed_value, is_range = _parse_date_value(value=value)
parsed_min = _parse_min_date(
min_value=min_value,
parsed_dates=parsed_value,
)
parsed_max = _parse_max_date(
max_value=max_value,
parsed_dates=parsed_value,
)
if value == "today":
v = cast("list[date]", parsed_value)[0]
if v < parsed_min:
parsed_value = [parsed_min]
if v > parsed_max:
parsed_value = [parsed_max]
return cls(
value=parsed_value,
is_range=is_range,
min=parsed_min,
max=parsed_max,
)
def __post_init__(self) -> None:
if self.min > self.max:
raise StreamlitAPIException(
f"The `min_value`, set to {self.min}, shouldn't be larger "
f"than the `max_value`, set to {self.max}."
)
if self.value:
start_value = self.value[0]
end_value = self.value[-1]
if (start_value < self.min) or (end_value > self.max):
raise StreamlitAPIException(
f"The default `value` of {self.value} "
f"must lie between the `min_value` of {self.min} "
f"and the `max_value` of {self.max}, inclusively."
)
@dataclass
class TimeInputSerde:
value: time | None
def deserialize(self, ui_value: str | None, widget_id: Any = "") -> time | None:
return (
datetime.strptime(ui_value, "%H:%M").time()
if ui_value is not None
else self.value
)
def serialize(self, v: datetime | time | None) -> str | None:
if v is None:
return None
if isinstance(v, datetime):
v = v.time()
return time.strftime(v, "%H:%M")
@dataclass
class DateInputSerde:
value: _DateInputValues
def deserialize(
self,
ui_value: Any,
widget_id: str = "",
) -> DateWidgetReturn:
return_value: Sequence[date] | None
if ui_value is not None:
return_value = tuple(
datetime.strptime(v, "%Y/%m/%d").date() for v in ui_value
)
else:
return_value = self.value.value
if return_value is None or len(return_value) == 0:
return () if self.value.is_range else None
if not self.value.is_range:
return return_value[0]
return cast("DateWidgetReturn", tuple(return_value))
def serialize(self, v: DateWidgetReturn) -> list[str]:
if v is None:
return []
to_serialize = list(v) if isinstance(v, Sequence) else [v]
return [date.strftime(v, "%Y/%m/%d") for v in to_serialize]
class TimeWidgetsMixin:
@overload
def time_input(
self,
label: str,
value: TimeValue = "now",
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
step: int | timedelta = timedelta(minutes=DEFAULT_STEP_MINUTES),
) -> time:
pass
@overload
def time_input(
self,
label: str,
value: None = None,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
step: int | timedelta = timedelta(minutes=DEFAULT_STEP_MINUTES),
) -> time | None:
pass
@gather_metrics("time_input")
def time_input(
self,
label: str,
value: TimeValue | None = "now",
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
step: int | timedelta = timedelta(minutes=DEFAULT_STEP_MINUTES),
) -> time | None:
r"""Display a time input widget.
Parameters
----------
label : str
A short label explaining to the user what this time input is for.
The label can optionally contain GitHub-flavored Markdown of the
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
and Images. Images display like icons, with a max height equal to
the font height.
Unsupported Markdown elements are unwrapped so only their children
(text contents) render. Display unsupported elements as literal
characters by backslash-escaping them. E.g.,
``"1\. Not an ordered list"``.
See the ``body`` parameter of |st.markdown|_ for additional,
supported Markdown directives.
For accessibility reasons, you should never set an empty label, but
you can hide it with ``label_visibility`` if needed. In the future,
we may disallow empty labels by raising an exception.
.. |st.markdown| replace:: ``st.markdown``
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
value : "now", datetime.time, datetime.datetime, str, or None
The value of this widget when it first renders. This can be one of
the following:
- ``"now"`` (default): The widget initializes with the current time.
- A ``datetime.time`` or ``datetime.datetime`` object: The widget
initializes with the given time, ignoring any date if included.
- An ISO-formatted time ("hh:mm", "hh:mm:ss", or "hh:mm:ss.sss") or
datetime ("YYYY-MM-DD hh:mm:ss") string: The widget initializes
with the given time, ignoring any date if included.
- ``None``: The widget initializes with no time and returns
``None`` until the user selects a time.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. No two widgets may have the same key.
help : str or None
A tooltip that gets displayed next to the widget label. Streamlit
only displays the tooltip when ``label_visibility="visible"``. If
this is ``None`` (default), no tooltip is displayed.
The tooltip can optionally contain GitHub-flavored Markdown,
including the Markdown directives described in the ``body``
parameter of ``st.markdown``.
on_change : callable
An optional callback invoked when this time_input's value changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
disabled : bool
An optional boolean that disables the time input if set to
``True``. The default is ``False``.
label_visibility : "visible", "hidden", or "collapsed"
The visibility of the label. The default is ``"visible"``. If this
is ``"hidden"``, Streamlit displays an empty spacer instead of the
label, which can help keep the widget alligned with other widgets.
If this is ``"collapsed"``, Streamlit displays no label or spacer.
step : int or timedelta
The stepping interval in seconds. Defaults to 900, i.e. 15 minutes.
You can also pass a datetime.timedelta object.
Returns
-------
datetime.time or None
The current value of the time input widget or ``None`` if no time has been
selected.
Example
-------
>>> import datetime
>>> import streamlit as st
>>>
>>> t = st.time_input("Set an alarm for", datetime.time(8, 45))
>>> st.write("Alarm is set for", t)
.. output::
https://doc-time-input.streamlit.app/
height: 260px
To initialize an empty time input, use ``None`` as the value:
>>> import datetime
>>> import streamlit as st
>>>
>>> t = st.time_input("Set an alarm for", value=None)
>>> st.write("Alarm is set for", t)
.. output::
https://doc-time-input-empty.streamlit.app/
height: 260px
"""
ctx = get_script_run_ctx()
return self._time_input(
label=label,
value=value,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
disabled=disabled,
label_visibility=label_visibility,
step=step,
ctx=ctx,
)
def _time_input(
self,
label: str,
value: TimeValue | None = "now",
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
step: int | timedelta = timedelta(minutes=DEFAULT_STEP_MINUTES),
ctx: ScriptRunContext | None = None,
) -> time | None:
key = to_key(key)
check_widget_policies(
self.dg,
key,
on_change,
default_value=value if value != "now" else None,
)
maybe_raise_label_warnings(label, label_visibility)
parsed_time: time | None
if value is None:
parsed_time = None
else:
parsed_time = _convert_timelike_to_time(value)
element_id = compute_and_register_element_id(
"time_input",
user_key=key,
form_id=current_form_id(self.dg),
label=label,
value=parsed_time if isinstance(value, (datetime, time)) else value,
help=help,
step=step,
)
del value
session_state = get_session_state().filtered_state
if key is not None and key in session_state and session_state[key] is None:
parsed_time = None
time_input_proto = TimeInputProto()
time_input_proto.id = element_id
time_input_proto.label = label
if parsed_time is not None:
time_input_proto.default = time.strftime(parsed_time, "%H:%M")
time_input_proto.form_id = current_form_id(self.dg)
if not isinstance(step, (int, timedelta)):
raise StreamlitAPIException(
f"`step` can only be `int` or `timedelta` but {type(step)} is provided."
)
if isinstance(step, timedelta):
step = step.seconds
if step < 60 or step > timedelta(hours=23).seconds:
raise StreamlitAPIException(
f"`step` must be between 60 seconds and 23 hours but is currently set to {step} seconds."
)
time_input_proto.step = step
time_input_proto.disabled = disabled
time_input_proto.label_visibility.value = get_label_visibility_proto_value(
label_visibility
)
if help is not None:
time_input_proto.help = dedent(help)
serde = TimeInputSerde(parsed_time)
widget_state = register_widget(
time_input_proto.id,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=serde.deserialize,
serializer=serde.serialize,
ctx=ctx,
value_type="string_value",
)
if widget_state.value_changed:
if (serialized_value := serde.serialize(widget_state.value)) is not None:
time_input_proto.value = serialized_value
time_input_proto.set_value = True
self.dg._enqueue("time_input", time_input_proto)
return widget_state.value
@overload
def date_input(
self,
label: str,
value: date | datetime | str | Literal["today"] = "today",
min_value: NullableScalarDateValue = None,
max_value: NullableScalarDateValue = None,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
format: str = "YYYY/MM/DD",
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> date: ...
@overload
def date_input(
self,
label: str,
value: None,
min_value: NullableScalarDateValue = None,
max_value: NullableScalarDateValue = None,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
format: str = "YYYY/MM/DD",
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> date | None: ...
@overload
def date_input(
self,
label: str,
value: tuple[NullableScalarDateValue]
| tuple[NullableScalarDateValue, NullableScalarDateValue]
| list[NullableScalarDateValue],
min_value: NullableScalarDateValue = None,
max_value: NullableScalarDateValue = None,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
format: str = "YYYY/MM/DD",
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> DateWidgetRangeReturn: ...
@gather_metrics("date_input")
def date_input(
self,
label: str,
value: DateValue = "today",
min_value: NullableScalarDateValue = None,
max_value: NullableScalarDateValue = None,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
format: str = "YYYY/MM/DD",
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
) -> DateWidgetReturn:
r"""Display a date input widget.
The first day of the week is determined from the user's locale in their
browser.
Parameters
----------
label : str
A short label explaining to the user what this date input is for.
The label can optionally contain GitHub-flavored Markdown of the
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
and Images. Images display like icons, with a max height equal to
the font height.
Unsupported Markdown elements are unwrapped so only their children
(text contents) render. Display unsupported elements as literal
characters by backslash-escaping them. E.g.,
``"1\. Not an ordered list"``.
See the ``body`` parameter of |st.markdown|_ for additional,
supported Markdown directives.
For accessibility reasons, you should never set an empty label, but
you can hide it with ``label_visibility`` if needed. In the future,
we may disallow empty labels by raising an exception.
.. |st.markdown| replace:: ``st.markdown``
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
value : "today", datetime.date, datetime.datetime, str, list/tuple of these, or None
The value of this widget when it first renders. This can be one of
the following:
- ``"today"`` (default): The widget initializes with the current date.
- A ``datetime.date`` or ``datetime.datetime`` object: The widget
initializes with the given date, ignoring any time if included.
- An ISO-formatted date ("YYYY-MM-DD") or datetime
("YYYY-MM-DD hh:mm:ss") string: The widget initializes with the
given date, ignoring any time if included.
- A list or tuple with up to two of the above: The widget will
initialize with the given date interval and return a tuple of the
selected interval. You can pass an empty list to initialize the
widget with an empty interval or a list with one value to
initialize only the beginning date of the iterval.
- ``None``: The widget initializes with no date and returns
``None`` until the user selects a date.
min_value : "today", datetime.date, datetime.datetime, str, or None
The minimum selectable date. This can be any of the date types
accepted by ``value``, except list or tuple.
If this is ``None`` (default), the minimum selectable date is ten
years before the initial value. If the initial value is an
interval, the minimum selectable date is ten years before the start
date of the interval. If no initial value is set, the minimum
selectable date is ten years before today.
max_value : "today", datetime.date, datetime.datetime, str, or None
The maximum selectable date. This can be any of the date types
accepted by ``value``, except list or tuple.
If this is ``None`` (default), the maximum selectable date is ten
years after the initial value. If the initial value is an interval,
the maximum selectable date is ten years after the end date of the
interval. If no initial value is set, the maximum selectable date
is ten years after today.
key : str or int
An optional string or integer to use as the unique key for the widget.
If this is omitted, a key will be generated for the widget
based on its content. No two widgets may have the same key.
help : str or None
A tooltip that gets displayed next to the widget label. Streamlit
only displays the tooltip when ``label_visibility="visible"``. If
this is ``None`` (default), no tooltip is displayed.
The tooltip can optionally contain GitHub-flavored Markdown,
including the Markdown directives described in the ``body``
parameter of ``st.markdown``.
on_change : callable
An optional callback invoked when this date_input's value changes.
args : tuple
An optional tuple of args to pass to the callback.
kwargs : dict
An optional dict of kwargs to pass to the callback.
format : str
A format string controlling how the interface should display dates.
Supports "YYYY/MM/DD" (default), "DD/MM/YYYY", or "MM/DD/YYYY".
You may also use a period (.) or hyphen (-) as separators.
disabled : bool
An optional boolean that disables the date input if set to
``True``. The default is ``False``.
label_visibility : "visible", "hidden", or "collapsed"
The visibility of the label. The default is ``"visible"``. If this
is ``"hidden"``, Streamlit displays an empty spacer instead of the
label, which can help keep the widget alligned with other widgets.
If this is ``"collapsed"``, Streamlit displays no label or spacer.
Returns
-------
datetime.date or a tuple with 0-2 dates or None
The current value of the date input widget or ``None`` if no date has been
selected.
Examples
--------
>>> import datetime
>>> import streamlit as st
>>>
>>> d = st.date_input("When's your birthday", datetime.date(2019, 7, 6))
>>> st.write("Your birthday is:", d)
.. output::
https://doc-date-input.streamlit.app/
height: 380px
>>> import datetime
>>> import streamlit as st
>>>
>>> today = datetime.datetime.now()
>>> next_year = today.year + 1
>>> jan_1 = datetime.date(next_year, 1, 1)
>>> dec_31 = datetime.date(next_year, 12, 31)
>>>
>>> d = st.date_input(
... "Select your vacation for next year",
... (jan_1, datetime.date(next_year, 1, 7)),
... jan_1,
... dec_31,
... format="MM.DD.YYYY",
... )
>>> d
.. output::
https://doc-date-input1.streamlit.app/
height: 380px
To initialize an empty date input, use ``None`` as the value:
>>> import datetime
>>> import streamlit as st
>>>
>>> d = st.date_input("When's your birthday", value=None)
>>> st.write("Your birthday is:", d)
.. output::
https://doc-date-input-empty.streamlit.app/
height: 380px
"""
ctx = get_script_run_ctx()
return self._date_input(
label=label,
value=value,
min_value=min_value,
max_value=max_value,
key=key,
help=help,
on_change=on_change,
args=args,
kwargs=kwargs,
disabled=disabled,
label_visibility=label_visibility,
format=format,
ctx=ctx,
)
def _date_input(
self,
label: str,
value: DateValue = "today",
min_value: NullableScalarDateValue = None,
max_value: NullableScalarDateValue = None,
key: Key | None = None,
help: str | None = None,
on_change: WidgetCallback | None = None,
args: WidgetArgs | None = None,
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
format: str = "YYYY/MM/DD",
disabled: bool = False,
label_visibility: LabelVisibility = "visible",
ctx: ScriptRunContext | None = None,
) -> DateWidgetReturn:
key = to_key(key)
check_widget_policies(
self.dg,
key,
on_change,
default_value=value if value != "today" else None,
)
maybe_raise_label_warnings(label, label_visibility)
def parse_date_deterministic_for_id(v: NullableScalarDateValue) -> str | None:
if v == "today":
# For ID purposes, no need to parse the input string.
return None
if isinstance(v, str):
# For ID purposes, no need to parse the input string.
return v
if isinstance(v, datetime):
return date.strftime(v.date(), "%Y/%m/%d")
if isinstance(v, date):
return date.strftime(v, "%Y/%m/%d")
return None
parsed_min_date = parse_date_deterministic_for_id(min_value)
parsed_max_date = parse_date_deterministic_for_id(max_value)
parsed: str | None | list[str | None]
if value == "today":
parsed = None
elif isinstance(value, Sequence):
parsed = [
parse_date_deterministic_for_id(cast("NullableScalarDateValue", v))
for v in value
]
else:
parsed = parse_date_deterministic_for_id(value)
# TODO: this is missing the error path, integrate with the dateinputvalues parsing
element_id = compute_and_register_element_id(
"date_input",
user_key=key,
form_id=current_form_id(self.dg),
label=label,
value=parsed,
min_value=parsed_min_date,
max_value=parsed_max_date,
help=help,
format=format,
)
if not bool(ALLOWED_DATE_FORMATS.match(format)):
raise StreamlitAPIException(
f"The provided format (`{format}`) is not valid. DateInput format "
"should be one of `YYYY/MM/DD`, `DD/MM/YYYY`, or `MM/DD/YYYY` "
"and can also use a period (.) or hyphen (-) as separators."
)
parsed_values = _DateInputValues.from_raw_values(
value=value,
min_value=min_value,
max_value=max_value,
)
if value == "today":
# We need to know if this is a single or range date_input, but don't have
# a default value, so we check if session_state can tell us.
# We already calculated the id, so there is no risk of this causing
# the id to change.
session_state = get_session_state().filtered_state
if key is not None and key in session_state:
state_value = session_state[key]
parsed_values = _DateInputValues.from_raw_values(
value=state_value,
min_value=min_value,
max_value=max_value,
)
del value, min_value, max_value
date_input_proto = DateInputProto()
date_input_proto.id = element_id
date_input_proto.is_range = parsed_values.is_range
date_input_proto.disabled = disabled
date_input_proto.label_visibility.value = get_label_visibility_proto_value(
label_visibility
)
date_input_proto.format = format
date_input_proto.label = label
if parsed_values.value is None:
# An empty array represents the empty state. The reason for using an empty
# array here is that we cannot optional keyword for repeated fields
# in protobuf.
date_input_proto.default[:] = []
else:
date_input_proto.default[:] = [
date.strftime(v, "%Y/%m/%d") for v in parsed_values.value
]
date_input_proto.min = date.strftime(parsed_values.min, "%Y/%m/%d")
date_input_proto.max = date.strftime(parsed_values.max, "%Y/%m/%d")
date_input_proto.form_id = current_form_id(self.dg)
if help is not None:
date_input_proto.help = dedent(help)
serde = DateInputSerde(parsed_values)
widget_state = register_widget(
date_input_proto.id,
on_change_handler=on_change,
args=args,
kwargs=kwargs,
deserializer=serde.deserialize,
serializer=serde.serialize,
ctx=ctx,
value_type="string_array_value",
)
if widget_state.value_changed:
date_input_proto.value[:] = serde.serialize(widget_state.value)
date_input_proto.set_value = True
self.dg._enqueue("date_input", date_input_proto)
return widget_state.value
@property
def dg(self) -> DeltaGenerator:
"""Get our DeltaGenerator."""
return cast("DeltaGenerator", self)