Mise à jour de Monitor.py et autres scripts
This commit is contained in:
@@ -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.
|
||||
@@ -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
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user