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.
|
||||
234
myenv/lib/python3.11/site-packages/streamlit/elements/alert.py
Normal file
234
myenv/lib/python3.11/site-packages/streamlit/elements/alert.py
Normal file
@@ -0,0 +1,234 @@
|
||||
# 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 typing import TYPE_CHECKING, cast
|
||||
|
||||
from streamlit.proto.Alert_pb2 import Alert as AlertProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.string_util import clean_text, validate_icon_or_emoji
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.type_util import SupportsStr
|
||||
|
||||
|
||||
class AlertMixin:
|
||||
@gather_metrics("error")
|
||||
def error(
|
||||
self,
|
||||
body: SupportsStr,
|
||||
*, # keyword-only args:
|
||||
icon: str | None = None,
|
||||
) -> DeltaGenerator:
|
||||
"""Display error message.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : str
|
||||
The text to display as GitHub-flavored Markdown. Syntax
|
||||
information can be found at: https://github.github.com/gfm.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
icon : str, None
|
||||
An optional emoji or icon to display next to the alert. If ``icon``
|
||||
is ``None`` (default), no icon is displayed. If ``icon`` is a
|
||||
string, the following options are valid:
|
||||
|
||||
- A single-character emoji. For example, you can set ``icon="🚨"``
|
||||
or ``icon="🔥"``. 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.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.error('This is an error', icon="🚨")
|
||||
|
||||
"""
|
||||
alert_proto = AlertProto()
|
||||
|
||||
alert_proto.icon = validate_icon_or_emoji(icon)
|
||||
alert_proto.body = clean_text(body)
|
||||
alert_proto.format = AlertProto.ERROR
|
||||
return self.dg._enqueue("alert", alert_proto)
|
||||
|
||||
@gather_metrics("warning")
|
||||
def warning(
|
||||
self,
|
||||
body: SupportsStr,
|
||||
*, # keyword-only args:
|
||||
icon: str | None = None,
|
||||
) -> DeltaGenerator:
|
||||
"""Display warning message.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : str
|
||||
The text to display as GitHub-flavored Markdown. Syntax
|
||||
information can be found at: https://github.github.com/gfm.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
icon : str, None
|
||||
An optional emoji or icon to display next to the alert. If ``icon``
|
||||
is ``None`` (default), no icon is displayed. If ``icon`` is a
|
||||
string, the following options are valid:
|
||||
|
||||
- A single-character emoji. For example, you can set ``icon="🚨"``
|
||||
or ``icon="🔥"``. 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.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.warning('This is a warning', icon="⚠️")
|
||||
|
||||
"""
|
||||
alert_proto = AlertProto()
|
||||
alert_proto.body = clean_text(body)
|
||||
alert_proto.icon = validate_icon_or_emoji(icon)
|
||||
alert_proto.format = AlertProto.WARNING
|
||||
return self.dg._enqueue("alert", alert_proto)
|
||||
|
||||
@gather_metrics("info")
|
||||
def info(
|
||||
self,
|
||||
body: SupportsStr,
|
||||
*, # keyword-only args:
|
||||
icon: str | None = None,
|
||||
) -> DeltaGenerator:
|
||||
"""Display an informational message.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : str
|
||||
The text to display as GitHub-flavored Markdown. Syntax
|
||||
information can be found at: https://github.github.com/gfm.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
icon : str, None
|
||||
An optional emoji or icon to display next to the alert. If ``icon``
|
||||
is ``None`` (default), no icon is displayed. If ``icon`` is a
|
||||
string, the following options are valid:
|
||||
|
||||
- A single-character emoji. For example, you can set ``icon="🚨"``
|
||||
or ``icon="🔥"``. 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.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.info('This is a purely informational message', icon="ℹ️")
|
||||
|
||||
"""
|
||||
|
||||
alert_proto = AlertProto()
|
||||
alert_proto.body = clean_text(body)
|
||||
alert_proto.icon = validate_icon_or_emoji(icon)
|
||||
alert_proto.format = AlertProto.INFO
|
||||
return self.dg._enqueue("alert", alert_proto)
|
||||
|
||||
@gather_metrics("success")
|
||||
def success(
|
||||
self,
|
||||
body: SupportsStr,
|
||||
*, # keyword-only args:
|
||||
icon: str | None = None,
|
||||
) -> DeltaGenerator:
|
||||
"""Display a success message.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : str
|
||||
The text to display as GitHub-flavored Markdown. Syntax
|
||||
information can be found at: https://github.github.com/gfm.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
icon : str, None
|
||||
An optional emoji or icon to display next to the alert. If ``icon``
|
||||
is ``None`` (default), no icon is displayed. If ``icon`` is a
|
||||
string, the following options are valid:
|
||||
|
||||
- A single-character emoji. For example, you can set ``icon="🚨"``
|
||||
or ``icon="🔥"``. 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.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.success('This is a success message!', icon="✅")
|
||||
|
||||
"""
|
||||
alert_proto = AlertProto()
|
||||
alert_proto.body = clean_text(body)
|
||||
alert_proto.icon = validate_icon_or_emoji(icon)
|
||||
alert_proto.format = AlertProto.SUCCESS
|
||||
return self.dg._enqueue("alert", alert_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
961
myenv/lib/python3.11/site-packages/streamlit/elements/arrow.py
Normal file
961
myenv/lib/python3.11/site-packages/streamlit/elements/arrow.py
Normal file
@@ -0,0 +1,961 @@
|
||||
# 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 typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Final,
|
||||
Literal,
|
||||
TypedDict,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit import dataframe_util
|
||||
from streamlit.elements.lib.column_config_utils import (
|
||||
INDEX_IDENTIFIER,
|
||||
ColumnConfigMappingInput,
|
||||
apply_data_specific_configs,
|
||||
marshall_column_config,
|
||||
process_config_mapping,
|
||||
update_column_config,
|
||||
)
|
||||
from streamlit.elements.lib.event_utils import AttributeDictionary
|
||||
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.proto.ForwardMsg_pb2 import ForwardMsg
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner_utils.script_run_context import (
|
||||
enqueue_message,
|
||||
get_script_run_ctx,
|
||||
)
|
||||
from streamlit.runtime.state import WidgetCallback, register_widget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Hashable, Iterable
|
||||
|
||||
from numpy import typing as npt
|
||||
from pandas import DataFrame
|
||||
|
||||
from streamlit.dataframe_util import Data
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.elements.lib.built_in_chart_utils import AddRowsMetadata
|
||||
|
||||
|
||||
SelectionMode: TypeAlias = Literal[
|
||||
"single-row", "multi-row", "single-column", "multi-column"
|
||||
]
|
||||
_SELECTION_MODES: Final[set[SelectionMode]] = {
|
||||
"single-row",
|
||||
"multi-row",
|
||||
"single-column",
|
||||
"multi-column",
|
||||
}
|
||||
|
||||
|
||||
class DataframeSelectionState(TypedDict, total=False):
|
||||
"""
|
||||
The schema for the dataframe selection state.
|
||||
|
||||
The selection state is stored in a dictionary-like object that supports both
|
||||
key and attribute notation. Selection states cannot be programmatically
|
||||
changed or set through Session State.
|
||||
|
||||
.. warning::
|
||||
If a user sorts a dataframe, row selections will be reset. If your
|
||||
users need to sort and filter the dataframe to make selections, direct
|
||||
them to use the search function in the dataframe toolbar instead.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
rows : list[int]
|
||||
The selected rows, identified by their integer position. The integer
|
||||
positions match the original dataframe, even if the user sorts the
|
||||
dataframe in their browser. For a ``pandas.DataFrame``, you can
|
||||
retrieve data from its interger position using methods like ``.iloc[]``
|
||||
or ``.iat[]``.
|
||||
columns : list[str]
|
||||
The selected columns, identified by their names.
|
||||
|
||||
Example
|
||||
-------
|
||||
The following example has multi-row and multi-column selections enabled.
|
||||
Try selecting some rows. To select multiple columns, hold ``Ctrl`` while
|
||||
selecting columns. Hold ``Shift`` to select a range of columns.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> if "df" not in st.session_state:
|
||||
>>> st.session_state.df = pd.DataFrame(
|
||||
... np.random.randn(12, 5), columns=["a", "b", "c", "d", "e"]
|
||||
... )
|
||||
>>>
|
||||
>>> event = st.dataframe(
|
||||
... st.session_state.df,
|
||||
... key="data",
|
||||
... on_select="rerun",
|
||||
... selection_mode=["multi-row", "multi-column"],
|
||||
... )
|
||||
>>>
|
||||
>>> event.selection
|
||||
|
||||
.. output::
|
||||
https://doc-dataframe-events-selection-state.streamlit.app
|
||||
height: 600px
|
||||
|
||||
"""
|
||||
|
||||
rows: list[int]
|
||||
columns: list[str]
|
||||
|
||||
|
||||
class DataframeState(TypedDict, total=False):
|
||||
"""
|
||||
The schema for the dataframe event state.
|
||||
|
||||
The event state is stored in a dictionary-like object that supports both
|
||||
key and attribute notation. Event states cannot be programmatically
|
||||
changed or set through Session State.
|
||||
|
||||
Only selection events are supported at this time.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
selection : dict
|
||||
The state of the ``on_select`` event. This attribute returns a
|
||||
dictionary-like object that supports both key and attribute notation.
|
||||
The attributes are described by the ``DataframeSelectionState``
|
||||
dictionary schema.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
selection: DataframeSelectionState
|
||||
|
||||
|
||||
@dataclass
|
||||
class DataframeSelectionSerde:
|
||||
"""DataframeSelectionSerde is used to serialize and deserialize the dataframe selection state."""
|
||||
|
||||
def deserialize(self, ui_value: str | None, widget_id: str = "") -> DataframeState:
|
||||
empty_selection_state: DataframeState = {
|
||||
"selection": {
|
||||
"rows": [],
|
||||
"columns": [],
|
||||
},
|
||||
}
|
||||
selection_state: DataframeState = (
|
||||
empty_selection_state if ui_value is None else json.loads(ui_value)
|
||||
)
|
||||
|
||||
if "selection" not in selection_state:
|
||||
selection_state = empty_selection_state
|
||||
|
||||
return cast("DataframeState", AttributeDictionary(selection_state))
|
||||
|
||||
def serialize(self, editing_state: DataframeState) -> str:
|
||||
return json.dumps(editing_state, default=str)
|
||||
|
||||
|
||||
def parse_selection_mode(
|
||||
selection_mode: SelectionMode | Iterable[SelectionMode],
|
||||
) -> set[ArrowProto.SelectionMode.ValueType]:
|
||||
"""Parse and check the user provided selection modes."""
|
||||
if isinstance(selection_mode, str):
|
||||
# Only a single selection mode was passed
|
||||
selection_mode_set = {selection_mode}
|
||||
else:
|
||||
# Multiple selection modes were passed
|
||||
selection_mode_set = set(selection_mode)
|
||||
|
||||
if not selection_mode_set.issubset(_SELECTION_MODES):
|
||||
raise StreamlitAPIException(
|
||||
f"Invalid selection mode: {selection_mode}. "
|
||||
f"Valid options are: {_SELECTION_MODES}"
|
||||
)
|
||||
|
||||
if selection_mode_set.issuperset({"single-row", "multi-row"}):
|
||||
raise StreamlitAPIException(
|
||||
"Only one of `single-row` or `multi-row` can be selected as selection mode."
|
||||
)
|
||||
|
||||
if selection_mode_set.issuperset({"single-column", "multi-column"}):
|
||||
raise StreamlitAPIException(
|
||||
"Only one of `single-column` or `multi-column` can be selected as selection mode."
|
||||
)
|
||||
|
||||
parsed_selection_modes = []
|
||||
for selection_mode in selection_mode_set:
|
||||
if selection_mode == "single-row":
|
||||
parsed_selection_modes.append(ArrowProto.SelectionMode.SINGLE_ROW)
|
||||
elif selection_mode == "multi-row":
|
||||
parsed_selection_modes.append(ArrowProto.SelectionMode.MULTI_ROW)
|
||||
elif selection_mode == "single-column":
|
||||
parsed_selection_modes.append(ArrowProto.SelectionMode.SINGLE_COLUMN)
|
||||
elif selection_mode == "multi-column":
|
||||
parsed_selection_modes.append(ArrowProto.SelectionMode.MULTI_COLUMN)
|
||||
return set(parsed_selection_modes)
|
||||
|
||||
|
||||
class ArrowMixin:
|
||||
@overload
|
||||
def dataframe(
|
||||
self,
|
||||
data: Data = None,
|
||||
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,
|
||||
key: Key | None = None,
|
||||
on_select: Literal["ignore"] = "ignore",
|
||||
selection_mode: SelectionMode | Iterable[SelectionMode] = "multi-row",
|
||||
row_height: int | None = None,
|
||||
) -> DeltaGenerator: ...
|
||||
|
||||
@overload
|
||||
def dataframe(
|
||||
self,
|
||||
data: Data = None,
|
||||
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,
|
||||
key: Key | None = None,
|
||||
on_select: Literal["rerun"] | WidgetCallback,
|
||||
selection_mode: SelectionMode | Iterable[SelectionMode] = "multi-row",
|
||||
row_height: int | None = None,
|
||||
) -> DataframeState: ...
|
||||
|
||||
@gather_metrics("dataframe")
|
||||
def dataframe(
|
||||
self,
|
||||
data: Data = None,
|
||||
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,
|
||||
key: Key | None = None,
|
||||
on_select: Literal["ignore", "rerun"] | WidgetCallback = "ignore",
|
||||
selection_mode: SelectionMode | Iterable[SelectionMode] = "multi-row",
|
||||
row_height: int | None = None,
|
||||
) -> DeltaGenerator | DataframeState:
|
||||
"""Display a dataframe as an interactive table.
|
||||
|
||||
This command works with a wide variety of collection-like and
|
||||
dataframe-like object types.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : dataframe-like, collection-like, or None
|
||||
The data to display.
|
||||
|
||||
Dataframe-like objects include dataframe and series objects from
|
||||
popular libraries like Dask, Modin, Numpy, pandas, Polars, PyArrow,
|
||||
Snowpark, Xarray, and more. You can use database cursors and
|
||||
clients that comply with the
|
||||
`Python Database API Specification v2.0 (PEP 249)
|
||||
<https://peps.python.org/pep-0249/>`_. Additionally, you can use
|
||||
anything that supports the `Python dataframe interchange protocol
|
||||
<https://data-apis.org/dataframe-protocol/latest/>`_.
|
||||
|
||||
For example, you can use the following:
|
||||
|
||||
- ``pandas.DataFrame``, ``pandas.Series``, ``pandas.Index``,
|
||||
``pandas.Styler``, and ``pandas.Array``
|
||||
- ``polars.DataFrame``, ``polars.LazyFrame``, and ``polars.Series``
|
||||
- ``snowflake.snowpark.dataframe.DataFrame``,
|
||||
``snowflake.snowpark.table.Table``
|
||||
|
||||
If a data type is not recognized, Streamlit will convert the object
|
||||
to a ``pandas.DataFrame`` or ``pyarrow.Table`` using a
|
||||
``.to_pandas()`` or ``.to_arrow()`` method, respectively, if
|
||||
available.
|
||||
|
||||
If ``data`` is a ``pandas.Styler``, it will be used to style its
|
||||
underlying ``pandas.DataFrame``. Streamlit supports custom cell
|
||||
values and colors. It does not support some of the more exotic
|
||||
styling options, like bar charts, hovering, and captions. For
|
||||
these styling options, use column configuration instead. Text and
|
||||
number formatting from ``column_config`` always takes precedence
|
||||
over text and number formatting from ``pandas.Styler``.
|
||||
|
||||
Collection-like objects include all Python-native ``Collection``
|
||||
types, such as ``dict``, ``list``, and ``set``.
|
||||
|
||||
If ``data`` is ``None``, Streamlit renders an empty table.
|
||||
|
||||
width : int or None
|
||||
Desired width of the dataframe expressed in pixels. If ``width`` is
|
||||
``None`` (default), Streamlit sets the dataframe 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
|
||||
dataframe width to match the width of the parent container.
|
||||
|
||||
height : int or None
|
||||
Desired height of the dataframe expressed in pixels. If ``height``
|
||||
is ``None`` (default), Streamlit sets the height to show at most
|
||||
ten rows. Vertical scrolling within the dataframe 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 dataframe to match the width of the parent container. If
|
||||
this is ``False``, Streamlit sets the dataframe'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
|
||||
The ordered list of columns to display. If ``column_order`` is
|
||||
``None`` (default), Streamlit displays all columns in the order
|
||||
inherited from the underlying data structure. If ``column_order``
|
||||
is a list, the indicated columns will display in the order they
|
||||
appear within the list. Columns may be omitted or repeated within
|
||||
the list.
|
||||
|
||||
For example, ``column_order=("col2", "col1")`` will display
|
||||
``"col2"`` first, followed by ``"col1"``, and will hide all other
|
||||
non-index columns.
|
||||
|
||||
column_config : dict or None
|
||||
Configuration to customize how columns display. If ``column_config``
|
||||
is ``None`` (default), columns are styled based on the underlying
|
||||
data type of each column.
|
||||
|
||||
Column configuration can modify column names, visibility, type,
|
||||
width, or format, among other things. ``column_config`` must be a
|
||||
dictionary where each key is a column name and the associated value
|
||||
is one of the following:
|
||||
|
||||
- ``None``: Streamlit hides the column.
|
||||
|
||||
- A string: Streamlit changes the display label of the column to
|
||||
the given string.
|
||||
|
||||
- A column type within ``st.column_config``: Streamlit applies the
|
||||
defined configuration to the column. For example, use
|
||||
``st.column_config.NumberColumn("Dollar values”, format=”$ %d")``
|
||||
to change the displayed name of the column to "Dollar values"
|
||||
and add a "$" prefix in each cell. For more info on the
|
||||
available column types and config options, see
|
||||
`Column configuration <https://docs.streamlit.io/develop/api-reference/data/st.column_config>`_.
|
||||
|
||||
To configure the index column(s), use ``_index`` as the column name.
|
||||
|
||||
key : str
|
||||
An optional string to use for giving this element a stable
|
||||
identity. If ``key`` is ``None`` (default), this element's identity
|
||||
will be determined based on the values of the other parameters.
|
||||
|
||||
Additionally, if selections are activated and ``key`` is provided,
|
||||
Streamlit will register the key in Session State to store the
|
||||
selection state. The selection state is read-only.
|
||||
|
||||
on_select : "ignore" or "rerun" or callable
|
||||
How the dataframe should respond to user selection events. This
|
||||
controls whether or not the dataframe behaves like an input widget.
|
||||
``on_select`` can be one of the following:
|
||||
|
||||
- ``"ignore"`` (default): Streamlit will not react to any selection
|
||||
events in the dataframe. The dataframe will not behave like an
|
||||
input widget.
|
||||
|
||||
- ``"rerun"``: Streamlit will rerun the app when the user selects
|
||||
rows or columns in the dataframe. In this case, ``st.dataframe``
|
||||
will return the selection data as a dictionary.
|
||||
|
||||
- A ``callable``: Streamlit will rerun the app and execute the
|
||||
``callable`` as a callback function before the rest of the app.
|
||||
In this case, ``st.dataframe`` will return the selection data
|
||||
as a dictionary.
|
||||
|
||||
selection_mode : "single-row", "multi-row", "single-column", \
|
||||
"multi-column", or Iterable of these
|
||||
The types of selections Streamlit should allow when selections are
|
||||
enabled with ``on_select``. This can be one of the following:
|
||||
|
||||
- "multi-row" (default): Multiple rows can be selected at a time.
|
||||
- "single-row": Only one row can be selected at a time.
|
||||
- "multi-column": Multiple columns can be selected at a time.
|
||||
- "single-column": Only one column can be selected at a time.
|
||||
- An ``Iterable`` of the above options: The table will allow
|
||||
selection based on the modes specified.
|
||||
|
||||
When column selections are enabled, column sorting is disabled.
|
||||
|
||||
row_height : int or None
|
||||
The height of each row in the dataframe in pixels. If ``row_height``
|
||||
is ``None`` (default), Streamlit will use a default row height,
|
||||
which fits one line of text.
|
||||
|
||||
Returns
|
||||
-------
|
||||
element or dict
|
||||
If ``on_select`` is ``"ignore"`` (default), this command returns an
|
||||
internal placeholder for the dataframe element that can be used
|
||||
with the ``.add_rows()`` method. Otherwise, this command returns a
|
||||
dictionary-like object that supports both key and attribute
|
||||
notation. The attributes are described by the ``DataframeState``
|
||||
dictionary schema.
|
||||
|
||||
Examples
|
||||
--------
|
||||
**Example 1: Display a dataframe**
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> df = pd.DataFrame(np.random.randn(50, 20), columns=("col %d" % i for i in range(20)))
|
||||
>>>
|
||||
>>> st.dataframe(df) # Same as st.write(df)
|
||||
|
||||
.. output::
|
||||
https://doc-dataframe.streamlit.app/
|
||||
height: 500px
|
||||
|
||||
**Example 2: Use Pandas Styler**
|
||||
|
||||
You can also pass a Pandas Styler object to change the style of
|
||||
the rendered DataFrame:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> df = pd.DataFrame(np.random.randn(10, 20), columns=("col %d" % i for i in range(20)))
|
||||
>>>
|
||||
>>> st.dataframe(df.style.highlight_max(axis=0))
|
||||
|
||||
.. output::
|
||||
https://doc-dataframe1.streamlit.app/
|
||||
height: 500px
|
||||
|
||||
**Example 3: Use column configuration**
|
||||
|
||||
You can customize a dataframe via ``column_config``, ``hide_index``, or ``column_order``.
|
||||
|
||||
>>> import random
|
||||
>>> import pandas as pd
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> df = pd.DataFrame(
|
||||
>>> {
|
||||
>>> "name": ["Roadmap", "Extras", "Issues"],
|
||||
>>> "url": ["https://roadmap.streamlit.app", "https://extras.streamlit.app", "https://issues.streamlit.app"],
|
||||
>>> "stars": [random.randint(0, 1000) for _ in range(3)],
|
||||
>>> "views_history": [[random.randint(0, 5000) for _ in range(30)] for _ in range(3)],
|
||||
>>> }
|
||||
>>> )
|
||||
>>> st.dataframe(
|
||||
>>> df,
|
||||
>>> column_config={
|
||||
>>> "name": "App name",
|
||||
>>> "stars": st.column_config.NumberColumn(
|
||||
>>> "Github Stars",
|
||||
>>> help="Number of stars on GitHub",
|
||||
>>> format="%d ⭐",
|
||||
>>> ),
|
||||
>>> "url": st.column_config.LinkColumn("App URL"),
|
||||
>>> "views_history": st.column_config.LineChartColumn(
|
||||
>>> "Views (past 30 days)", y_min=0, y_max=5000
|
||||
>>> ),
|
||||
>>> },
|
||||
>>> hide_index=True,
|
||||
>>> )
|
||||
|
||||
.. output::
|
||||
https://doc-dataframe-config.streamlit.app/
|
||||
height: 350px
|
||||
|
||||
**Example 4: Customize your index**
|
||||
|
||||
You can use column configuration to format your index.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>> from datetime import date
|
||||
>>>
|
||||
>>> df = pd.DataFrame(
|
||||
>>> {
|
||||
>>> "Date": [date(2024, 1, 1), date(2024, 2, 1), date(2024, 3, 1)],
|
||||
>>> "Total": [13429, 23564, 23452],
|
||||
>>> }
|
||||
>>> )
|
||||
>>> df.set_index("Date", inplace=True)
|
||||
>>>
|
||||
>>> config = {
|
||||
>>> "_index": st.column_config.DateColumn("Month", format="MMM YYYY"),
|
||||
>>> "Total": st.column_config.NumberColumn("Total ($)"),
|
||||
>>> }
|
||||
>>>
|
||||
>>> st.dataframe(df, column_config=config)
|
||||
|
||||
.. output::
|
||||
https://doc-dataframe-config-index.streamlit.app/
|
||||
height: 225px
|
||||
|
||||
"""
|
||||
import pyarrow as pa
|
||||
|
||||
if on_select not in ["ignore", "rerun"] and not callable(on_select):
|
||||
raise StreamlitAPIException(
|
||||
f"You have passed {on_select} to `on_select`. But only 'ignore', "
|
||||
"'rerun', or a callable is supported."
|
||||
)
|
||||
|
||||
key = to_key(key)
|
||||
is_selection_activated = on_select != "ignore"
|
||||
|
||||
if is_selection_activated:
|
||||
# Run some checks that are only relevant when selections are activated
|
||||
is_callback = callable(on_select)
|
||||
check_widget_policies(
|
||||
self.dg,
|
||||
key,
|
||||
on_change=cast("WidgetCallback", on_select) if is_callback else None,
|
||||
default_value=None,
|
||||
writes_allowed=False,
|
||||
enable_check_callback_rules=is_callback,
|
||||
)
|
||||
|
||||
# Convert the user provided column config into the frontend compatible format:
|
||||
column_config_mapping = process_config_mapping(column_config)
|
||||
|
||||
proto = ArrowProto()
|
||||
|
||||
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
|
||||
|
||||
proto.editing_mode = ArrowProto.EditingMode.READ_ONLY
|
||||
|
||||
if isinstance(data, pa.Table):
|
||||
# For pyarrow tables, we can just serialize the table directly
|
||||
proto.data = dataframe_util.convert_arrow_table_to_arrow_bytes(data)
|
||||
else:
|
||||
# For all other data formats, we need to convert them to a pandas.DataFrame
|
||||
# thereby, we also apply some data specific configs
|
||||
|
||||
# Determine the input data format
|
||||
data_format = dataframe_util.determine_data_format(data)
|
||||
|
||||
if dataframe_util.is_pandas_styler(data):
|
||||
# If pandas.Styler uuid is not provided, a hash of the position
|
||||
# of the element will be used. This will cause a rerender of the table
|
||||
# when the position of the element is changed.
|
||||
delta_path = self.dg._get_delta_path_str()
|
||||
default_uuid = str(hash(delta_path))
|
||||
marshall_styler(proto, data, default_uuid)
|
||||
|
||||
# Convert the input data into a pandas.DataFrame
|
||||
data_df = dataframe_util.convert_anything_to_pandas_df(
|
||||
data, ensure_copy=False
|
||||
)
|
||||
apply_data_specific_configs(column_config_mapping, data_format)
|
||||
# Serialize the data to bytes:
|
||||
proto.data = dataframe_util.convert_pandas_df_to_arrow_bytes(data_df)
|
||||
|
||||
if hide_index is not None:
|
||||
update_column_config(
|
||||
column_config_mapping, INDEX_IDENTIFIER, {"hidden": hide_index}
|
||||
)
|
||||
marshall_column_config(proto, column_config_mapping)
|
||||
|
||||
if is_selection_activated:
|
||||
# If selection events are activated, we need to register the dataframe
|
||||
# element as a widget.
|
||||
proto.selection_mode.extend(parse_selection_mode(selection_mode))
|
||||
proto.form_id = current_form_id(self.dg)
|
||||
|
||||
ctx = get_script_run_ctx()
|
||||
proto.id = compute_and_register_element_id(
|
||||
"dataframe",
|
||||
user_key=key,
|
||||
form_id=proto.form_id,
|
||||
data=proto.data,
|
||||
width=width,
|
||||
height=height,
|
||||
use_container_width=use_container_width,
|
||||
column_order=proto.column_order,
|
||||
column_config=proto.columns,
|
||||
selection_mode=selection_mode,
|
||||
is_selection_activated=is_selection_activated,
|
||||
row_height=row_height,
|
||||
)
|
||||
|
||||
serde = DataframeSelectionSerde()
|
||||
widget_state = register_widget(
|
||||
proto.id,
|
||||
on_change_handler=on_select if callable(on_select) else None,
|
||||
deserializer=serde.deserialize,
|
||||
serializer=serde.serialize,
|
||||
ctx=ctx,
|
||||
value_type="string_value",
|
||||
)
|
||||
self.dg._enqueue("arrow_data_frame", proto)
|
||||
return cast("DataframeState", widget_state.value)
|
||||
else:
|
||||
return self.dg._enqueue("arrow_data_frame", proto)
|
||||
|
||||
@gather_metrics("table")
|
||||
def table(self, data: Data = None) -> DeltaGenerator:
|
||||
"""Display a static table.
|
||||
|
||||
While ``st.dataframe`` is geared towards large datasets and interactive
|
||||
data exploration, ``st.table`` is useful for displaying small, styled
|
||||
tables without sorting or scrolling. For example, ``st.table`` may be
|
||||
the preferred way to display a confusion matrix or leaderboard.
|
||||
Additionally, ``st.table`` supports Markdown.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : Anything supported by st.dataframe
|
||||
The table data.
|
||||
|
||||
All cells including the index and column headers can optionally
|
||||
contain GitHub-flavored Markdown. Syntax information can be found
|
||||
at: https://github.github.com/gfm.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
Examples
|
||||
--------
|
||||
**Example 1: Display a simple dataframe as a static table**
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> df = pd.DataFrame(
|
||||
... np.random.randn(10, 5), columns=("col %d" % i for i in range(5))
|
||||
... )
|
||||
>>>
|
||||
>>> st.table(df)
|
||||
|
||||
.. output::
|
||||
https://doc-table.streamlit.app/
|
||||
height: 480px
|
||||
|
||||
**Example 2: Display a table of Markdown strings**
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>>
|
||||
>>> df = pd.DataFrame(
|
||||
... {
|
||||
... "Command": ["**st.table**", "*st.dataframe*"],
|
||||
... "Type": ["`static`", "`interactive`"],
|
||||
... "Docs": [
|
||||
... "[:rainbow[docs]](https://docs.streamlit.io/develop/api-reference/data/st.dataframe)",
|
||||
... "[:book:](https://docs.streamlit.io/develop/api-reference/data/st.table)",
|
||||
... ],
|
||||
... }
|
||||
... )
|
||||
>>> st.table(df)
|
||||
|
||||
.. output::
|
||||
https://doc-table-markdown.streamlit.app/
|
||||
height: 200px
|
||||
"""
|
||||
|
||||
# Check if data is uncollected, and collect it but with 100 rows max, instead of
|
||||
# 10k rows, which is done in all other cases.
|
||||
# We use 100 rows in st.table, because large tables render slowly,
|
||||
# take too much screen space, and can crush the app.
|
||||
if dataframe_util.is_unevaluated_data_object(data):
|
||||
data = dataframe_util.convert_anything_to_pandas_df(
|
||||
data, max_unevaluated_rows=100
|
||||
)
|
||||
|
||||
# If pandas.Styler uuid is not provided, a hash of the position
|
||||
# of the element will be used. This will cause a rerender of the table
|
||||
# when the position of the element is changed.
|
||||
delta_path = self.dg._get_delta_path_str()
|
||||
default_uuid = str(hash(delta_path))
|
||||
|
||||
proto = ArrowProto()
|
||||
marshall(proto, data, default_uuid)
|
||||
return self.dg._enqueue("arrow_table", proto)
|
||||
|
||||
@gather_metrics("add_rows")
|
||||
def add_rows(self, data: Data = None, **kwargs) -> DeltaGenerator | None:
|
||||
"""Concatenate a dataframe to the bottom of the current one.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : pandas.DataFrame, pandas.Styler, pyarrow.Table, numpy.ndarray, pyspark.sql.DataFrame, snowflake.snowpark.dataframe.DataFrame, Iterable, dict, or None
|
||||
Table to concat. Optional.
|
||||
|
||||
**kwargs : pandas.DataFrame, numpy.ndarray, Iterable, dict, or None
|
||||
The named dataset to concat. Optional. You can only pass in 1
|
||||
dataset (including the one in the data parameter).
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> df1 = pd.DataFrame(
|
||||
... np.random.randn(50, 20), columns=("col %d" % i for i in range(20))
|
||||
... )
|
||||
>>>
|
||||
>>> my_table = st.table(df1)
|
||||
>>>
|
||||
>>> df2 = pd.DataFrame(
|
||||
... np.random.randn(50, 20), columns=("col %d" % i for i in range(20))
|
||||
... )
|
||||
>>>
|
||||
>>> my_table.add_rows(df2)
|
||||
>>> # Now the table shown in the Streamlit app contains the data for
|
||||
>>> # df1 followed by the data for df2.
|
||||
|
||||
You can do the same thing with plots. For example, if you want to add
|
||||
more data to a line chart:
|
||||
|
||||
>>> # Assuming df1 and df2 from the example above still exist...
|
||||
>>> my_chart = st.line_chart(df1)
|
||||
>>> my_chart.add_rows(df2)
|
||||
>>> # Now the chart shown in the Streamlit app contains the data for
|
||||
>>> # df1 followed by the data for df2.
|
||||
|
||||
And for plots whose datasets are named, you can pass the data with a
|
||||
keyword argument where the key is the name:
|
||||
|
||||
>>> my_chart = st.vega_lite_chart(
|
||||
... {
|
||||
... "mark": "line",
|
||||
... "encoding": {"x": "a", "y": "b"},
|
||||
... "datasets": {
|
||||
... "some_fancy_name": df1, # <-- named dataset
|
||||
... },
|
||||
... "data": {"name": "some_fancy_name"},
|
||||
... }
|
||||
... )
|
||||
>>> my_chart.add_rows(some_fancy_name=df2) # <-- name used as keyword
|
||||
|
||||
"""
|
||||
return _arrow_add_rows(self.dg, data, **kwargs)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
def _prep_data_for_add_rows(
|
||||
data: Data,
|
||||
add_rows_metadata: AddRowsMetadata | None,
|
||||
) -> tuple[Data, AddRowsMetadata | None]:
|
||||
if not add_rows_metadata:
|
||||
if dataframe_util.is_pandas_styler(data):
|
||||
# When calling add_rows on st.table or st.dataframe we want styles to
|
||||
# pass through.
|
||||
return data, None
|
||||
return dataframe_util.convert_anything_to_pandas_df(data), None
|
||||
|
||||
# If add_rows_metadata is set, it indicates that the add_rows used called
|
||||
# on a chart based on our built-in chart commands.
|
||||
|
||||
# For built-in chart commands we have to reshape the data structure
|
||||
# otherwise the input data and the actual data used
|
||||
# by vega_lite will be different, and it will throw an error.
|
||||
from streamlit.elements.lib.built_in_chart_utils import prep_chart_data_for_add_rows
|
||||
|
||||
return prep_chart_data_for_add_rows(data, add_rows_metadata)
|
||||
|
||||
|
||||
def _arrow_add_rows(
|
||||
dg: DeltaGenerator,
|
||||
data: Data = None,
|
||||
**kwargs: (
|
||||
DataFrame | npt.NDArray[Any] | Iterable[Any] | dict[Hashable, Any] | None
|
||||
),
|
||||
) -> DeltaGenerator | None:
|
||||
"""Concatenate a dataframe to the bottom of the current one.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : pandas.DataFrame, pandas.Styler, numpy.ndarray, Iterable, dict, or None
|
||||
Table to concat. Optional.
|
||||
|
||||
**kwargs : pandas.DataFrame, numpy.ndarray, Iterable, dict, or None
|
||||
The named dataset to concat. Optional. You can only pass in 1
|
||||
dataset (including the one in the data parameter).
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> df1 = pd.DataFrame(
|
||||
... np.random.randn(50, 20), columns=("col %d" % i for i in range(20))
|
||||
... )
|
||||
>>> my_table = st.table(df1)
|
||||
>>>
|
||||
>>> df2 = pd.DataFrame(
|
||||
... np.random.randn(50, 20), columns=("col %d" % i for i in range(20))
|
||||
... )
|
||||
>>> my_table.add_rows(df2)
|
||||
>>> # Now the table shown in the Streamlit app contains the data for
|
||||
>>> # df1 followed by the data for df2.
|
||||
|
||||
You can do the same thing with plots. For example, if you want to add
|
||||
more data to a line chart:
|
||||
|
||||
>>> # Assuming df1 and df2 from the example above still exist...
|
||||
>>> my_chart = st.line_chart(df1)
|
||||
>>> my_chart.add_rows(df2)
|
||||
>>> # Now the chart shown in the Streamlit app contains the data for
|
||||
>>> # df1 followed by the data for df2.
|
||||
|
||||
And for plots whose datasets are named, you can pass the data with a
|
||||
keyword argument where the key is the name:
|
||||
|
||||
>>> my_chart = st.vega_lite_chart(
|
||||
... {
|
||||
... "mark": "line",
|
||||
... "encoding": {"x": "a", "y": "b"},
|
||||
... "datasets": {
|
||||
... "some_fancy_name": df1, # <-- named dataset
|
||||
... },
|
||||
... "data": {"name": "some_fancy_name"},
|
||||
... }
|
||||
... )
|
||||
>>> my_chart.add_rows(some_fancy_name=df2) # <-- name used as keyword
|
||||
|
||||
"""
|
||||
if dg._root_container is None or dg._cursor is None:
|
||||
return dg
|
||||
|
||||
if not dg._cursor.is_locked:
|
||||
raise StreamlitAPIException("Only existing elements can `add_rows`.")
|
||||
|
||||
# Accept syntax st._arrow_add_rows(df).
|
||||
if data is not None and len(kwargs) == 0:
|
||||
name = ""
|
||||
# Accept syntax st._arrow_add_rows(foo=df).
|
||||
elif len(kwargs) == 1:
|
||||
name, data = kwargs.popitem()
|
||||
# Raise error otherwise.
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
"Wrong number of arguments to add_rows()."
|
||||
"Command requires exactly one dataset"
|
||||
)
|
||||
|
||||
# When doing _arrow_add_rows on an element that does not already have data
|
||||
# (for example, st.line_chart() without any args), call the original
|
||||
# st.foo() element with new data instead of doing a _arrow_add_rows().
|
||||
if (
|
||||
"add_rows_metadata" in dg._cursor.props
|
||||
and dg._cursor.props["add_rows_metadata"]
|
||||
and dg._cursor.props["add_rows_metadata"].last_index is None
|
||||
):
|
||||
st_method = getattr(dg, dg._cursor.props["add_rows_metadata"].chart_command)
|
||||
st_method(data, **kwargs)
|
||||
return None
|
||||
|
||||
new_data, dg._cursor.props["add_rows_metadata"] = _prep_data_for_add_rows(
|
||||
data,
|
||||
dg._cursor.props["add_rows_metadata"],
|
||||
)
|
||||
|
||||
msg = ForwardMsg()
|
||||
msg.metadata.delta_path[:] = dg._cursor.delta_path
|
||||
|
||||
default_uuid = str(hash(dg._get_delta_path_str()))
|
||||
marshall(msg.delta.arrow_add_rows.data, new_data, default_uuid)
|
||||
|
||||
if name:
|
||||
msg.delta.arrow_add_rows.name = name
|
||||
msg.delta.arrow_add_rows.has_name = True
|
||||
|
||||
enqueue_message(msg)
|
||||
|
||||
return dg
|
||||
|
||||
|
||||
def marshall(proto: ArrowProto, data: Data, default_uuid: str | None = None) -> None:
|
||||
"""Marshall pandas.DataFrame into an Arrow proto.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
proto : proto.Arrow
|
||||
Output. The protobuf for Streamlit Arrow proto.
|
||||
|
||||
data : pandas.DataFrame, pandas.Styler, pyarrow.Table, numpy.ndarray, pyspark.sql.DataFrame, snowflake.snowpark.DataFrame, Iterable, dict, or None
|
||||
Something that is or can be converted to a dataframe.
|
||||
|
||||
default_uuid : str | None
|
||||
If pandas.Styler UUID is not provided, this value will be used.
|
||||
This attribute is optional and only used for pandas.Styler, other elements
|
||||
(e.g. charts) can ignore it.
|
||||
|
||||
"""
|
||||
|
||||
if dataframe_util.is_pandas_styler(data):
|
||||
# default_uuid is a string only if the data is a `Styler`,
|
||||
# and `None` otherwise.
|
||||
assert isinstance(default_uuid, str), (
|
||||
"Default UUID must be a string for Styler data."
|
||||
)
|
||||
marshall_styler(proto, data, default_uuid)
|
||||
|
||||
proto.data = dataframe_util.convert_anything_to_arrow_bytes(data)
|
||||
@@ -0,0 +1,47 @@
|
||||
# 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 typing import TYPE_CHECKING, cast
|
||||
|
||||
from streamlit.proto.Balloons_pb2 import Balloons as BalloonsProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
class BalloonsMixin:
|
||||
@gather_metrics("balloons")
|
||||
def balloons(self) -> DeltaGenerator:
|
||||
"""Draw celebratory balloons.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.balloons()
|
||||
|
||||
...then watch your app and get ready for a celebration!
|
||||
|
||||
"""
|
||||
balloons_proto = BalloonsProto()
|
||||
balloons_proto.show = True
|
||||
return self.dg._enqueue("balloons", balloons_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
@@ -0,0 +1,133 @@
|
||||
# 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.
|
||||
|
||||
"""A Python wrapper around Bokeh."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING, Final, cast
|
||||
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.BokehChart_pb2 import BokehChart as BokehChartProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.util import calc_md5
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bokeh.plotting.figure import Figure
|
||||
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
ST_BOKEH_VERSION: Final = "2.4.3"
|
||||
|
||||
|
||||
class BokehMixin:
|
||||
@gather_metrics("bokeh_chart")
|
||||
def bokeh_chart(
|
||||
self,
|
||||
figure: Figure,
|
||||
use_container_width: bool = True,
|
||||
) -> DeltaGenerator:
|
||||
"""Display an interactive Bokeh chart.
|
||||
|
||||
Bokeh is a charting library for Python. The arguments to this function
|
||||
closely follow the ones for Bokeh's ``show`` function. You can find
|
||||
more about Bokeh at https://bokeh.pydata.org.
|
||||
|
||||
To show Bokeh charts in Streamlit, call ``st.bokeh_chart``
|
||||
wherever you would call Bokeh's ``show``.
|
||||
|
||||
.. Important::
|
||||
You must install ``bokeh==2.4.3`` and ``numpy<2`` to use this
|
||||
command.
|
||||
|
||||
If you need a newer version of Bokeh, use our |streamlit-bokeh|_
|
||||
custom component instead.
|
||||
|
||||
.. |streamlit-bokeh| replace:: ``streamlit-bokeh``
|
||||
.. _streamlit-bokeh: https://github.com/streamlit/streamlit-bokeh
|
||||
|
||||
Parameters
|
||||
----------
|
||||
figure : bokeh.plotting.figure.Figure
|
||||
A Bokeh figure to plot.
|
||||
|
||||
use_container_width : bool
|
||||
Whether to override the figure's native width with the width of
|
||||
the parent container. If ``use_container_width`` is ``True`` (default),
|
||||
Streamlit sets the width of the figure to match the width of the parent
|
||||
container. If ``use_container_width`` is ``False``, Streamlit sets the
|
||||
width of the chart to fit its contents according to the plotting library,
|
||||
up to the width of the parent container.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>> from bokeh.plotting import figure
|
||||
>>>
|
||||
>>> x = [1, 2, 3, 4, 5]
|
||||
>>> y = [6, 7, 2, 4, 5]
|
||||
>>>
|
||||
>>> p = figure(title="simple line example", x_axis_label="x", y_axis_label="y")
|
||||
>>> p.line(x, y, legend_label="Trend", line_width=2)
|
||||
>>>
|
||||
>>> st.bokeh_chart(p)
|
||||
|
||||
.. output::
|
||||
https://doc-bokeh-chart.streamlit.app/
|
||||
height: 700px
|
||||
|
||||
"""
|
||||
import bokeh
|
||||
|
||||
if bokeh.__version__ != ST_BOKEH_VERSION:
|
||||
raise StreamlitAPIException(
|
||||
f"Streamlit only supports Bokeh version {ST_BOKEH_VERSION}, "
|
||||
f"but you have version {bokeh.__version__} installed. Please "
|
||||
f"run `pip install --force-reinstall --no-deps bokeh=="
|
||||
f"{ST_BOKEH_VERSION}` to install the correct version.\n\n\n"
|
||||
f"To use the latest version of Bokeh, install our custom component, "
|
||||
f"[streamlit-bokeh](https://github.com/streamlit/streamlit-bokeh)."
|
||||
)
|
||||
|
||||
# Generate element ID from delta path
|
||||
delta_path = self.dg._get_delta_path_str()
|
||||
|
||||
element_id = calc_md5(delta_path.encode())
|
||||
bokeh_chart_proto = BokehChartProto()
|
||||
marshall(bokeh_chart_proto, figure, use_container_width, element_id)
|
||||
return self.dg._enqueue("bokeh_chart", bokeh_chart_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
def marshall(
|
||||
proto: BokehChartProto,
|
||||
figure: Figure,
|
||||
use_container_width: bool,
|
||||
element_id: str,
|
||||
) -> None:
|
||||
"""Construct a Bokeh chart object.
|
||||
|
||||
See DeltaGenerator.bokeh_chart for docs.
|
||||
"""
|
||||
from bokeh.embed import json_item
|
||||
|
||||
data = json_item(figure)
|
||||
proto.figure = json.dumps(data)
|
||||
proto.use_container_width = use_container_width
|
||||
proto.element_id = element_id
|
||||
114
myenv/lib/python3.11/site-packages/streamlit/elements/code.py
Normal file
114
myenv/lib/python3.11/site-packages/streamlit/elements/code.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# 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 typing import TYPE_CHECKING, cast
|
||||
|
||||
from streamlit.proto.Code_pb2 import Code as CodeProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.string_util import clean_text
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.type_util import SupportsStr
|
||||
|
||||
|
||||
class CodeMixin:
|
||||
@gather_metrics("code")
|
||||
def code(
|
||||
self,
|
||||
body: SupportsStr,
|
||||
language: str | None = "python",
|
||||
*,
|
||||
line_numbers: bool = False,
|
||||
wrap_lines: bool = False,
|
||||
height: int | None = None,
|
||||
) -> DeltaGenerator:
|
||||
"""Display a code block with optional syntax highlighting.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : str
|
||||
The string to display as code or monospace text.
|
||||
|
||||
language : str or None
|
||||
The language that the code is written in, for syntax highlighting.
|
||||
This defaults to ``"python"``. If this is ``None``, the code will
|
||||
be plain, monospace text.
|
||||
|
||||
For a list of available ``language`` values, see
|
||||
`react-syntax-highlighter
|
||||
<https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_PRISM.MD>`_
|
||||
on GitHub.
|
||||
|
||||
line_numbers : bool
|
||||
An optional boolean indicating whether to show line numbers to the
|
||||
left of the code block. This defaults to ``False``.
|
||||
|
||||
wrap_lines : bool
|
||||
An optional boolean indicating whether to wrap lines. This defaults
|
||||
to ``False``.
|
||||
|
||||
height : int or None
|
||||
Desired height of the code block expressed in pixels. If ``height``
|
||||
is ``None`` (default), Streamlit sets the element's height to fit
|
||||
its content. Vertical scrolling within the element is enabled when
|
||||
the height does not accomodate all lines.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> code = '''def hello():
|
||||
... print("Hello, Streamlit!")'''
|
||||
>>> st.code(code, language="python")
|
||||
|
||||
.. output ::
|
||||
https://doc-code.streamlit.app/
|
||||
height: 220px
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> code = '''Is it a crown or boat?
|
||||
... ii
|
||||
... iiiiii
|
||||
... WWw .iiiiiiii. ...:
|
||||
... WWWWWWw .iiiiiiiiiiii. ........
|
||||
... WWWWWWWWWWw iiiiiiiiiiiiiiii ...........
|
||||
... WWWWWWWWWWWWWWwiiiiiiiiiiiiiiiii............
|
||||
... WWWWWWWWWWWWWWWWWWwiiiiiiiiiiiiii.........
|
||||
... WWWWWWWWWWWWWWWWWWWWWWwiiiiiiiiii.......
|
||||
... WWWWWWWWWWWWWWWWWWWWWWWWWWwiiiiiii....
|
||||
... WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWwiiii.
|
||||
... -MMMWWWWWWWWWWWWWWWWWWWWWWMMM-
|
||||
... '''
|
||||
>>> st.code(code, language=None)
|
||||
|
||||
.. output ::
|
||||
https://doc-code-ascii.streamlit.app/
|
||||
height: 380px
|
||||
"""
|
||||
code_proto = CodeProto()
|
||||
code_proto.code_text = clean_text(body)
|
||||
code_proto.language = language or "plaintext"
|
||||
code_proto.show_line_numbers = line_numbers
|
||||
code_proto.wrap_lines = wrap_lines
|
||||
if height:
|
||||
code_proto.height = height
|
||||
return self.dg._enqueue("code", code_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
@@ -0,0 +1,546 @@
|
||||
# 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 typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Final,
|
||||
Literal,
|
||||
TypedDict,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit import config
|
||||
from streamlit.elements.lib.event_utils import AttributeDictionary
|
||||
from streamlit.elements.lib.form_utils import current_form_id
|
||||
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.DeckGlJsonChart_pb2 import DeckGlJsonChart as PydeckProto
|
||||
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 (
|
||||
WidgetCallback,
|
||||
register_widget,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable, Mapping
|
||||
|
||||
from pydeck import Deck
|
||||
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
# Mapping used when no data is passed.
|
||||
EMPTY_MAP: Final[Mapping[str, Any]] = {
|
||||
"initialViewState": {"latitude": 0, "longitude": 0, "pitch": 0, "zoom": 1},
|
||||
}
|
||||
|
||||
SelectionMode: TypeAlias = Literal["single-object", "multi-object"]
|
||||
_SELECTION_MODES: Final[set[SelectionMode]] = {
|
||||
"single-object",
|
||||
"multi-object",
|
||||
}
|
||||
|
||||
|
||||
def parse_selection_mode(
|
||||
selection_mode: SelectionMode | Iterable[SelectionMode],
|
||||
) -> set[PydeckProto.SelectionMode.ValueType]:
|
||||
"""Parse and check the user provided selection modes."""
|
||||
if isinstance(selection_mode, str):
|
||||
# Only a single selection mode was passed
|
||||
selection_mode_set = {selection_mode}
|
||||
else:
|
||||
# Multiple selection modes were passed.
|
||||
# This is not yet supported as a functionality, but the infra is here to
|
||||
# support it in the future!
|
||||
# @see DeckGlJsonChart.tsx
|
||||
raise StreamlitAPIException(
|
||||
f"Invalid selection mode: {selection_mode}. ",
|
||||
"Selection mode must be a single value, but got a set instead.",
|
||||
)
|
||||
|
||||
if not selection_mode_set.issubset(_SELECTION_MODES):
|
||||
raise StreamlitAPIException(
|
||||
f"Invalid selection mode: {selection_mode}. "
|
||||
f"Valid options are: {_SELECTION_MODES}"
|
||||
)
|
||||
|
||||
if selection_mode_set.issuperset({"single-object", "multi-object"}):
|
||||
raise StreamlitAPIException(
|
||||
"Only one of `single-object` or `multi-object` can be selected as selection mode."
|
||||
)
|
||||
|
||||
parsed_selection_modes = []
|
||||
for selection_mode in selection_mode_set:
|
||||
if selection_mode == "single-object":
|
||||
parsed_selection_modes.append(PydeckProto.SelectionMode.SINGLE_OBJECT)
|
||||
elif selection_mode == "multi-object":
|
||||
parsed_selection_modes.append(PydeckProto.SelectionMode.MULTI_OBJECT)
|
||||
return set(parsed_selection_modes)
|
||||
|
||||
|
||||
class PydeckSelectionState(TypedDict, total=False):
|
||||
r"""
|
||||
The schema for the PyDeck chart selection state.
|
||||
|
||||
The selection state is stored in a dictionary-like object that supports
|
||||
both key and attribute notation. Selection states cannot be
|
||||
programmatically changed or set through Session State.
|
||||
|
||||
You must define ``id`` in ``pydeck.Layer`` to ensure statefulness when
|
||||
using selections with ``st.pydeck_chart``.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
indices : dict[str, list[int]]
|
||||
A dictionary of selected objects by layer. Each key in the dictionary
|
||||
is a layer id, and each value is a list of object indices within that
|
||||
layer.
|
||||
objects : dict[str, list[dict[str, Any]]]
|
||||
A dictionary of object attributes by layer. Each key in the dictionary
|
||||
is a layer id, and each value is a list of metadata dictionaries for
|
||||
the selected objects in that layer.
|
||||
|
||||
Examples
|
||||
--------
|
||||
The following example has multi-object selection enabled. The chart
|
||||
displays US state capitals by population (2023 US Census estimate). You
|
||||
can access this `data
|
||||
<https://github.com/streamlit/docs/blob/main/python/api-examples-source/data/capitals.csv>`_
|
||||
from GitHub.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pydeck
|
||||
>>> import pandas as pd
|
||||
>>>
|
||||
>>> capitals = pd.read_csv(
|
||||
... "capitals.csv",
|
||||
... header=0,
|
||||
... names=[
|
||||
... "Capital",
|
||||
... "State",
|
||||
... "Abbreviation",
|
||||
... "Latitude",
|
||||
... "Longitude",
|
||||
... "Population",
|
||||
... ],
|
||||
... )
|
||||
>>> capitals["size"] = capitals.Population / 10
|
||||
>>>
|
||||
>>> point_layer = pydeck.Layer(
|
||||
... "ScatterplotLayer",
|
||||
... data=capitals,
|
||||
... id="capital-cities",
|
||||
... get_position=["Longitude", "Latitude"],
|
||||
... get_color="[255, 75, 75]",
|
||||
... pickable=True,
|
||||
... auto_highlight=True,
|
||||
... get_radius="size",
|
||||
... )
|
||||
>>>
|
||||
>>> view_state = pydeck.ViewState(
|
||||
... latitude=40, longitude=-117, controller=True, zoom=2.4, pitch=30
|
||||
... )
|
||||
>>>
|
||||
>>> chart = pydeck.Deck(
|
||||
... point_layer,
|
||||
... initial_view_state=view_state,
|
||||
... tooltip={"text": "{Capital}, {Abbreviation}\nPopulation: {Population}"},
|
||||
... )
|
||||
>>>
|
||||
>>> event = st.pydeck_chart(chart, on_select="rerun", selection_mode="multi-object")
|
||||
>>>
|
||||
>>> event.selection
|
||||
|
||||
.. output ::
|
||||
https://doc-pydeck-event-state-selections.streamlit.app/
|
||||
height: 700px
|
||||
|
||||
This is an example of the selection state when selecting a single object
|
||||
from a layer with id, ``"captial-cities"``:
|
||||
|
||||
>>> {
|
||||
>>> "indices":{
|
||||
>>> "capital-cities":[
|
||||
>>> 2
|
||||
>>> ]
|
||||
>>> },
|
||||
>>> "objects":{
|
||||
>>> "capital-cities":[
|
||||
>>> {
|
||||
>>> "Abbreviation":" AZ"
|
||||
>>> "Capital":"Phoenix"
|
||||
>>> "Latitude":33.448457
|
||||
>>> "Longitude":-112.073844
|
||||
>>> "Population":1650070
|
||||
>>> "State":" Arizona"
|
||||
>>> "size":165007.0
|
||||
>>> }
|
||||
>>> ]
|
||||
>>> }
|
||||
>>> }
|
||||
|
||||
"""
|
||||
|
||||
indices: dict[str, list[int]]
|
||||
objects: dict[str, list[dict[str, Any]]]
|
||||
|
||||
|
||||
class PydeckState(TypedDict, total=False):
|
||||
"""
|
||||
The schema for the PyDeck event state.
|
||||
|
||||
The event state is stored in a dictionary-like object that supports both
|
||||
key and attribute notation. Event states cannot be programmatically changed
|
||||
or set through Session State.
|
||||
|
||||
Only selection events are supported at this time.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
selection : dict
|
||||
The state of the ``on_select`` event. This attribute returns a
|
||||
dictionary-like object that supports both key and attribute notation.
|
||||
The attributes are described by the ``PydeckSelectionState``
|
||||
dictionary schema.
|
||||
|
||||
"""
|
||||
|
||||
selection: PydeckSelectionState
|
||||
|
||||
|
||||
@dataclass
|
||||
class PydeckSelectionSerde:
|
||||
"""PydeckSelectionSerde is used to serialize and deserialize the Pydeck selection state."""
|
||||
|
||||
def deserialize(self, ui_value: str | None, widget_id: str = "") -> PydeckState:
|
||||
empty_selection_state: PydeckState = {
|
||||
"selection": {
|
||||
"indices": {},
|
||||
"objects": {},
|
||||
}
|
||||
}
|
||||
|
||||
selection_state = (
|
||||
empty_selection_state if ui_value is None else json.loads(ui_value)
|
||||
)
|
||||
|
||||
# We have seen some situations where the ui_value was just an empty
|
||||
# dict, so we want to ensure that it always returns the empty state in
|
||||
# case this happens.
|
||||
if "selection" not in selection_state:
|
||||
selection_state = empty_selection_state
|
||||
|
||||
return cast("PydeckState", AttributeDictionary(selection_state))
|
||||
|
||||
def serialize(self, selection_state: PydeckState) -> str:
|
||||
return json.dumps(selection_state, default=str)
|
||||
|
||||
|
||||
class PydeckMixin:
|
||||
@overload
|
||||
def pydeck_chart(
|
||||
self,
|
||||
pydeck_obj: Deck | None = None,
|
||||
*,
|
||||
use_container_width: bool = True,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
selection_mode: Literal[
|
||||
"single-object"
|
||||
], # Selection mode will only be activated by on_select param, this is a default value here to make it work with mypy
|
||||
on_select: Literal["ignore"], # No default value here to make it work with mypy
|
||||
key: Key | None = None,
|
||||
) -> DeltaGenerator: ...
|
||||
|
||||
@overload
|
||||
def pydeck_chart(
|
||||
self,
|
||||
pydeck_obj: Deck | None = None,
|
||||
*,
|
||||
use_container_width: bool = True,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
selection_mode: SelectionMode = "single-object",
|
||||
on_select: Literal["rerun"] | WidgetCallback = "rerun",
|
||||
key: Key | None = None,
|
||||
) -> PydeckState: ...
|
||||
|
||||
@gather_metrics("pydeck_chart")
|
||||
def pydeck_chart(
|
||||
self,
|
||||
pydeck_obj: Deck | None = None,
|
||||
*,
|
||||
use_container_width: bool = True,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
selection_mode: SelectionMode = "single-object",
|
||||
on_select: Literal["rerun", "ignore"] | WidgetCallback = "ignore",
|
||||
key: Key | None = None,
|
||||
) -> DeltaGenerator | PydeckState:
|
||||
"""Draw a chart using the PyDeck library.
|
||||
|
||||
This supports 3D maps, point clouds, and more! More info about PyDeck
|
||||
at https://deckgl.readthedocs.io/en/latest/.
|
||||
|
||||
These docs are also quite useful:
|
||||
|
||||
- DeckGL docs: https://github.com/uber/deck.gl/tree/master/docs
|
||||
- DeckGL JSON docs: https://github.com/uber/deck.gl/tree/master/modules/json
|
||||
|
||||
When using this command, Mapbox provides the map tiles to render map
|
||||
content. Note that Mapbox is a third-party product and Streamlit accepts
|
||||
no responsibility or liability of any kind for Mapbox or for any content
|
||||
or information made available by Mapbox.
|
||||
|
||||
Mapbox requires users to register and provide a token before users can
|
||||
request map tiles. Currently, Streamlit provides this token for you, but
|
||||
this could change at any time. We strongly recommend all users create and
|
||||
use their own personal Mapbox token to avoid any disruptions to their
|
||||
experience. You can do this with the ``mapbox.token`` config option. The
|
||||
use of Mapbox is governed by Mapbox's Terms of Use.
|
||||
|
||||
To get a token for yourself, create an account at https://mapbox.com.
|
||||
For more info on how to set config options, see
|
||||
https://docs.streamlit.io/develop/api-reference/configuration/config.toml.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
pydeck_obj : pydeck.Deck or None
|
||||
Object specifying the PyDeck chart to draw.
|
||||
use_container_width : bool
|
||||
Whether to override the figure's native width with the width of
|
||||
the parent container. If ``use_container_width`` is ``True`` (default),
|
||||
Streamlit sets the width of the figure to match the width of the parent
|
||||
container. If ``use_container_width`` is ``False``, Streamlit sets the
|
||||
width of the chart to fit its contents according to the plotting library,
|
||||
up to the width of the parent container.
|
||||
width : int or None
|
||||
Desired width of the chart expressed in pixels. If ``width`` is
|
||||
``None`` (default), Streamlit sets the width of the chart to fit
|
||||
its contents according to the plotting library, up to the width of
|
||||
the parent container. If ``width`` is greater than the width of the
|
||||
parent container, Streamlit sets the chart width to match the width
|
||||
of the parent container.
|
||||
|
||||
To use ``width``, you must set ``use_container_width=False``.
|
||||
height : int or None
|
||||
Desired height of the chart expressed in pixels. If ``height`` is
|
||||
``None`` (default), Streamlit sets the height of the chart to fit
|
||||
its contents according to the plotting library.
|
||||
on_select : "ignore" or "rerun" or callable
|
||||
How the figure should respond to user selection events. This controls
|
||||
whether or not the chart behaves like an input widget.
|
||||
``on_select`` can be one of the following:
|
||||
|
||||
- ``"ignore"`` (default): Streamlit will not react to any selection
|
||||
events in the chart. The figure will not behave like an
|
||||
input widget.
|
||||
- ``"rerun"``: Streamlit will rerun the app when the user selects
|
||||
data in the chart. In this case, ``st.pydeck_chart`` will return
|
||||
the selection data as a dictionary.
|
||||
- A ``callable``: Streamlit will rerun the app and execute the callable
|
||||
as a callback function before the rest of the app. In this case,
|
||||
``st.pydeck_chart`` will return the selection data as a
|
||||
dictionary.
|
||||
|
||||
If ``on_select`` is not ``"ignore"``, all layers must have a
|
||||
declared ``id`` to keep the chart stateful across reruns.
|
||||
selection_mode : "single-object" or "multi-object"
|
||||
The selection mode of the chart. This can be one of the following:
|
||||
|
||||
- ``"single-object"`` (default): Only one object can be selected at
|
||||
a time.
|
||||
- ``"multi-object"``: Multiple objects can be selected at a time.
|
||||
|
||||
key : str
|
||||
An optional string to use for giving this element a stable
|
||||
identity. If ``key`` is ``None`` (default), this element's identity
|
||||
will be determined based on the values of the other parameters.
|
||||
|
||||
Additionally, if selections are activated and ``key`` is provided,
|
||||
Streamlit will register the key in Session State to store the
|
||||
selection state. The selection state is read-only.
|
||||
|
||||
Returns
|
||||
-------
|
||||
element or dict
|
||||
If ``on_select`` is ``"ignore"`` (default), this command returns an
|
||||
internal placeholder for the chart element. Otherwise, this method
|
||||
returns a dictionary-like object that supports both key and
|
||||
attribute notation. The attributes are described by the
|
||||
``PydeckState`` dictionary schema.
|
||||
|
||||
Example
|
||||
-------
|
||||
Here's a chart using a HexagonLayer and a ScatterplotLayer. It uses either the
|
||||
light or dark map style, based on which Streamlit theme is currently active:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>> import numpy as np
|
||||
>>> import pydeck as pdk
|
||||
>>>
|
||||
>>> chart_data = pd.DataFrame(
|
||||
... np.random.randn(1000, 2) / [50, 50] + [37.76, -122.4],
|
||||
... columns=["lat", "lon"],
|
||||
... )
|
||||
>>>
|
||||
>>> st.pydeck_chart(
|
||||
... pdk.Deck(
|
||||
... map_style=None,
|
||||
... initial_view_state=pdk.ViewState(
|
||||
... latitude=37.76,
|
||||
... longitude=-122.4,
|
||||
... zoom=11,
|
||||
... pitch=50,
|
||||
... ),
|
||||
... layers=[
|
||||
... pdk.Layer(
|
||||
... "HexagonLayer",
|
||||
... data=chart_data,
|
||||
... get_position="[lon, lat]",
|
||||
... radius=200,
|
||||
... elevation_scale=4,
|
||||
... elevation_range=[0, 1000],
|
||||
... pickable=True,
|
||||
... extruded=True,
|
||||
... ),
|
||||
... pdk.Layer(
|
||||
... "ScatterplotLayer",
|
||||
... data=chart_data,
|
||||
... get_position="[lon, lat]",
|
||||
... get_color="[200, 30, 0, 160]",
|
||||
... get_radius=200,
|
||||
... ),
|
||||
... ],
|
||||
... )
|
||||
... )
|
||||
|
||||
.. output::
|
||||
https://doc-pydeck-chart.streamlit.app/
|
||||
height: 530px
|
||||
|
||||
.. note::
|
||||
To make the PyDeck chart's style consistent with Streamlit's theme,
|
||||
you can set ``map_style=None`` in the ``pydeck.Deck`` object.
|
||||
|
||||
"""
|
||||
pydeck_proto = PydeckProto()
|
||||
|
||||
ctx = get_script_run_ctx()
|
||||
|
||||
if pydeck_obj is None:
|
||||
spec = json.dumps(EMPTY_MAP)
|
||||
else:
|
||||
spec = pydeck_obj.to_json()
|
||||
|
||||
pydeck_proto.json = spec
|
||||
pydeck_proto.use_container_width = use_container_width
|
||||
|
||||
if width:
|
||||
pydeck_proto.width = width
|
||||
if height:
|
||||
pydeck_proto.height = height
|
||||
|
||||
tooltip = _get_pydeck_tooltip(pydeck_obj)
|
||||
if tooltip:
|
||||
pydeck_proto.tooltip = json.dumps(tooltip)
|
||||
|
||||
mapbox_token = config.get_option("mapbox.token")
|
||||
if mapbox_token:
|
||||
pydeck_proto.mapbox_token = mapbox_token
|
||||
|
||||
key = to_key(key)
|
||||
is_selection_activated = on_select != "ignore"
|
||||
|
||||
if on_select not in ["ignore", "rerun"] and not callable(on_select):
|
||||
raise StreamlitAPIException(
|
||||
f"You have passed {on_select} to `on_select`. But only 'ignore', 'rerun', or a callable is supported."
|
||||
)
|
||||
|
||||
if is_selection_activated:
|
||||
# Selections are activated, treat Pydeck as a widget:
|
||||
pydeck_proto.selection_mode.extend(parse_selection_mode(selection_mode))
|
||||
|
||||
# Run some checks that are only relevant when selections are activated
|
||||
is_callback = callable(on_select)
|
||||
check_widget_policies(
|
||||
self.dg,
|
||||
key,
|
||||
on_change=cast("WidgetCallback", on_select) if is_callback else None,
|
||||
default_value=None,
|
||||
writes_allowed=False,
|
||||
enable_check_callback_rules=is_callback,
|
||||
)
|
||||
pydeck_proto.form_id = current_form_id(self.dg)
|
||||
|
||||
pydeck_proto.id = compute_and_register_element_id(
|
||||
"deck_gl_json_chart",
|
||||
user_key=key,
|
||||
is_selection_activated=is_selection_activated,
|
||||
selection_mode=selection_mode,
|
||||
use_container_width=use_container_width,
|
||||
spec=spec,
|
||||
form_id=pydeck_proto.form_id,
|
||||
)
|
||||
|
||||
serde = PydeckSelectionSerde()
|
||||
|
||||
widget_state = register_widget(
|
||||
pydeck_proto.id,
|
||||
ctx=ctx,
|
||||
deserializer=serde.deserialize,
|
||||
on_change_handler=on_select if callable(on_select) else None,
|
||||
serializer=serde.serialize,
|
||||
value_type="string_value",
|
||||
)
|
||||
|
||||
self.dg._enqueue("deck_gl_json_chart", pydeck_proto)
|
||||
|
||||
return cast("PydeckState", widget_state.value)
|
||||
|
||||
return self.dg._enqueue("deck_gl_json_chart", pydeck_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
def _get_pydeck_tooltip(pydeck_obj: Deck | None) -> dict[str, str] | None:
|
||||
if pydeck_obj is None:
|
||||
return None
|
||||
|
||||
# For pydeck <0.8.1 or pydeck>=0.8.1 when jupyter extra is installed.
|
||||
desk_widget = getattr(pydeck_obj, "deck_widget", None)
|
||||
if desk_widget is not None and isinstance(desk_widget.tooltip, dict):
|
||||
return desk_widget.tooltip
|
||||
|
||||
# For pydeck >=0.8.1 when jupyter extra is not installed.
|
||||
# For details, see: https://github.com/visgl/deck.gl/pull/7125/files
|
||||
tooltip = getattr(pydeck_obj, "_tooltip", None)
|
||||
if tooltip is not None and isinstance(tooltip, dict):
|
||||
return cast("dict[str, str]", tooltip)
|
||||
|
||||
return None
|
||||
@@ -0,0 +1,267 @@
|
||||
# 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 functools import wraps
|
||||
from typing import TYPE_CHECKING, Callable, TypeVar, cast, overload
|
||||
|
||||
from streamlit.delta_generator_singletons import (
|
||||
get_dg_singleton_instance,
|
||||
get_last_dg_added_to_context_stack,
|
||||
)
|
||||
from streamlit.deprecation_util import (
|
||||
make_deprecated_name_warning,
|
||||
show_deprecation_warning,
|
||||
)
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.runtime.fragment import _fragment
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.elements.lib.dialog import DialogWidth
|
||||
|
||||
|
||||
def _assert_no_nested_dialogs() -> None:
|
||||
"""Check the current stack for existing DeltaGenerator's of type 'dialog'.
|
||||
Note that the check like this only works when Dialog is called as a context manager,
|
||||
as this populates the dg_stack in delta_generator correctly.
|
||||
|
||||
This does not detect the edge case in which someone calls, for example,
|
||||
`with st.sidebar` inside of a dialog function and opens a dialog in there, as
|
||||
`with st.sidebar` pushes the new DeltaGenerator to the stack. In order to check for
|
||||
that edge case, we could try to check all DeltaGenerators in the stack, and not only
|
||||
the last one. Since we deem this to be an edge case, we lean towards simplicity
|
||||
here.
|
||||
|
||||
Raises
|
||||
------
|
||||
StreamlitAPIException
|
||||
Raised if the user tries to nest dialogs inside of each other.
|
||||
"""
|
||||
last_dg_in_current_context = get_last_dg_added_to_context_stack()
|
||||
if last_dg_in_current_context and "dialog" in set(
|
||||
last_dg_in_current_context._ancestor_block_types
|
||||
):
|
||||
raise StreamlitAPIException("Dialogs may not be nested inside other dialogs.")
|
||||
|
||||
|
||||
F = TypeVar("F", bound=Callable[..., None])
|
||||
|
||||
|
||||
def _dialog_decorator(
|
||||
non_optional_func: F,
|
||||
title: str,
|
||||
*,
|
||||
width: DialogWidth = "small",
|
||||
should_show_deprecation_warning: bool = False,
|
||||
) -> F:
|
||||
if title is None or title == "":
|
||||
raise StreamlitAPIException(
|
||||
"A non-empty `title` argument has to be provided for dialogs, for example "
|
||||
'`@st.dialog("Example Title")`.'
|
||||
)
|
||||
|
||||
@wraps(non_optional_func)
|
||||
def wrap(*args, **kwargs) -> None:
|
||||
_assert_no_nested_dialogs()
|
||||
# Call the Dialog on the event_dg because it lives outside of the normal
|
||||
# Streamlit UI flow. For example, if it is called from the sidebar, it should
|
||||
# not inherit the sidebar theming.
|
||||
dialog = get_dg_singleton_instance().event_dg._dialog(
|
||||
title=title, dismissible=True, width=width
|
||||
)
|
||||
dialog.open()
|
||||
|
||||
def dialog_content() -> None:
|
||||
if should_show_deprecation_warning:
|
||||
show_deprecation_warning(
|
||||
make_deprecated_name_warning(
|
||||
"experimental_dialog",
|
||||
"dialog",
|
||||
"2025-01-01",
|
||||
)
|
||||
)
|
||||
|
||||
# if the dialog should be closed, st.rerun() has to be called
|
||||
# (same behavior as with st.fragment)
|
||||
_ = non_optional_func(*args, **kwargs)
|
||||
return None
|
||||
|
||||
# the fragment decorator has multiple return types so that you can pass
|
||||
# arguments to it. Here we know the return type, so we cast
|
||||
fragmented_dialog_content = cast(
|
||||
"Callable[[], None]",
|
||||
_fragment(
|
||||
dialog_content, additional_hash_info=non_optional_func.__qualname__
|
||||
),
|
||||
)
|
||||
|
||||
with dialog:
|
||||
fragmented_dialog_content()
|
||||
return None
|
||||
|
||||
return cast("F", wrap)
|
||||
|
||||
|
||||
@overload
|
||||
def dialog_decorator(
|
||||
title: str, *, width: DialogWidth = "small"
|
||||
) -> Callable[[F], F]: ...
|
||||
|
||||
|
||||
# 'title' can be a function since `dialog_decorator` is a decorator.
|
||||
# We just call it 'title' here though to make the user-doc more friendly as
|
||||
# we want the user to pass a title, not a function. The user is supposed to
|
||||
# call it like @st.dialog("my_title") , which makes 'title' a positional arg, hence
|
||||
# this 'trick'. The overload is required to have a good type hint for the decorated
|
||||
# function args.
|
||||
@overload
|
||||
def dialog_decorator(title: F, *, width: DialogWidth = "small") -> F: ...
|
||||
|
||||
|
||||
@gather_metrics("dialog")
|
||||
def dialog_decorator(
|
||||
title: F | str, *, width: DialogWidth = "small"
|
||||
) -> F | Callable[[F], F]:
|
||||
"""Function decorator to create a modal dialog.
|
||||
|
||||
A function decorated with ``@st.dialog`` becomes a dialog
|
||||
function. When you call a dialog function, Streamlit inserts a modal dialog
|
||||
into your app. Streamlit element commands called within the dialog function
|
||||
render inside the modal dialog.
|
||||
|
||||
The dialog function can accept arguments that can be passed when it is
|
||||
called. Any values from the dialog that need to be accessed from the wider
|
||||
app should generally be stored in Session State.
|
||||
|
||||
A user can dismiss a modal dialog by clicking outside of it, clicking the
|
||||
"**X**" in its upper-right corner, or pressing ``ESC`` on their keyboard.
|
||||
Dismissing a modal dialog does not trigger an app rerun. To close the modal
|
||||
dialog programmatically, call ``st.rerun()`` explicitly inside of the
|
||||
dialog function.
|
||||
|
||||
``st.dialog`` inherits behavior from |st.fragment|_.
|
||||
When a user interacts with an input widget created inside a dialog function,
|
||||
Streamlit only reruns the dialog function instead of the full script.
|
||||
|
||||
Calling ``st.sidebar`` in a dialog function is not supported.
|
||||
|
||||
Dialog code can interact with Session State, imported modules, and other
|
||||
Streamlit elements created outside the dialog. Note that these interactions
|
||||
are additive across multiple dialog reruns. You are responsible for
|
||||
handling any side effects of that behavior.
|
||||
|
||||
.. warning::
|
||||
Only one dialog function may be called in a script run, which means
|
||||
that only one dialog can be open at any given time.
|
||||
|
||||
.. |st.fragment| replace:: ``st.fragment``
|
||||
.. _st.fragment: https://docs.streamlit.io/develop/api-reference/execution-flow/st.fragment
|
||||
|
||||
Parameters
|
||||
----------
|
||||
title : str
|
||||
The title to display at the top of the modal dialog. It cannot be empty.
|
||||
width : "small", "large"
|
||||
The width of the modal dialog. If ``width`` is ``"small`` (default), the
|
||||
modal dialog will be 500 pixels wide. If ``width`` is ``"large"``, the
|
||||
modal dialog will be about 750 pixels wide.
|
||||
|
||||
Examples
|
||||
--------
|
||||
The following example demonstrates the basic usage of ``@st.dialog``.
|
||||
In this app, clicking "**A**" or "**B**" will open a modal dialog and prompt you
|
||||
to enter a reason for your vote. In the modal dialog, click "**Submit**" to record
|
||||
your vote into Session State and rerun the app. This will close the modal dialog
|
||||
since the dialog function is not called during the full-script rerun.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> @st.dialog("Cast your vote")
|
||||
>>> def vote(item):
|
||||
>>> st.write(f"Why is {item} your favorite?")
|
||||
>>> reason = st.text_input("Because...")
|
||||
>>> if st.button("Submit"):
|
||||
>>> st.session_state.vote = {"item": item, "reason": reason}
|
||||
>>> st.rerun()
|
||||
>>>
|
||||
>>> if "vote" not in st.session_state:
|
||||
>>> st.write("Vote for your favorite")
|
||||
>>> if st.button("A"):
|
||||
>>> vote("A")
|
||||
>>> if st.button("B"):
|
||||
>>> vote("B")
|
||||
>>> else:
|
||||
>>> f"You voted for {st.session_state.vote['item']} because {st.session_state.vote['reason']}"
|
||||
|
||||
.. output::
|
||||
https://doc-modal-dialog.streamlit.app/
|
||||
height: 350px
|
||||
|
||||
"""
|
||||
|
||||
func_or_title = title
|
||||
if isinstance(func_or_title, str):
|
||||
# Support passing the params via function decorator
|
||||
def wrapper(f: F) -> F:
|
||||
title: str = func_or_title
|
||||
return _dialog_decorator(non_optional_func=f, title=title, width=width)
|
||||
|
||||
return wrapper
|
||||
|
||||
func: F = func_or_title
|
||||
return _dialog_decorator(func, "", width=width)
|
||||
|
||||
|
||||
@overload
|
||||
def experimental_dialog_decorator(
|
||||
title: str, *, width: DialogWidth = "small"
|
||||
) -> Callable[[F], F]: ...
|
||||
|
||||
|
||||
# 'title' can be a function since `dialog_decorator` is a decorator. We just call it
|
||||
# 'title' here though to make the user-doc more friendly as we want the user to pass a
|
||||
# title, not a function. The user is supposed to call it like @st.dialog("my_title"),
|
||||
# which makes 'title' a positional arg, hence this 'trick'. The overload is required to
|
||||
# have a good type hint for the decorated function args.
|
||||
@overload
|
||||
def experimental_dialog_decorator(title: F, *, width: DialogWidth = "small") -> F: ...
|
||||
|
||||
|
||||
@gather_metrics("experimental_dialog")
|
||||
def experimental_dialog_decorator(
|
||||
title: F | str, *, width: DialogWidth = "small"
|
||||
) -> F | Callable[[F], F]:
|
||||
"""Deprecated alias for @st.dialog.
|
||||
See the docstring for the decorator's new name.
|
||||
"""
|
||||
func_or_title = title
|
||||
if isinstance(func_or_title, str):
|
||||
# Support passing the params via function decorator
|
||||
def wrapper(f: F) -> F:
|
||||
title: str = func_or_title
|
||||
return _dialog_decorator(
|
||||
non_optional_func=f,
|
||||
title=title,
|
||||
width=width,
|
||||
should_show_deprecation_warning=True,
|
||||
)
|
||||
|
||||
return wrapper
|
||||
|
||||
func: F = func_or_title
|
||||
return _dialog_decorator(
|
||||
func, "", width=width, should_show_deprecation_warning=True
|
||||
)
|
||||
@@ -0,0 +1,558 @@
|
||||
# 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.
|
||||
|
||||
"""Allows us to create and absorb changes (aka Deltas) to elements."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import contextlib
|
||||
import inspect
|
||||
import re
|
||||
import types
|
||||
from typing import TYPE_CHECKING, Any, Final, cast
|
||||
|
||||
import streamlit
|
||||
from streamlit.proto.DocString_pb2 import DocString as DocStringProto
|
||||
from streamlit.proto.DocString_pb2 import Member as MemberProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner.script_runner import (
|
||||
__file__ as SCRIPTRUNNER_FILENAME,
|
||||
)
|
||||
from streamlit.runtime.secrets import Secrets
|
||||
from streamlit.string_util import is_mem_address_str
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
CONFUSING_STREAMLIT_SIG_PREFIXES: Final = ("(element, ",)
|
||||
|
||||
|
||||
class HelpMixin:
|
||||
@gather_metrics("help")
|
||||
def help(self, obj: Any = streamlit) -> DeltaGenerator:
|
||||
"""Display help and other information for a given object.
|
||||
|
||||
Depending on the type of object that is passed in, this displays the
|
||||
object's name, type, value, signature, docstring, and member variables,
|
||||
methods — as well as the values/docstring of members and methods.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : any
|
||||
The object whose information should be displayed. If left
|
||||
unspecified, this call will display help for Streamlit itself.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
Don't remember how to initialize a dataframe? Try this:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas
|
||||
>>>
|
||||
>>> st.help(pandas.DataFrame)
|
||||
|
||||
.. output::
|
||||
https://doc-string.streamlit.app/
|
||||
height: 700px
|
||||
|
||||
Want to quickly check what data type is output by a certain function?
|
||||
Try:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> x = my_poorly_documented_function()
|
||||
>>> st.help(x)
|
||||
|
||||
Want to quickly inspect an object? No sweat:
|
||||
|
||||
>>> class Dog:
|
||||
>>> '''A typical dog.'''
|
||||
>>>
|
||||
>>> def __init__(self, breed, color):
|
||||
>>> self.breed = breed
|
||||
>>> self.color = color
|
||||
>>>
|
||||
>>> def bark(self):
|
||||
>>> return 'Woof!'
|
||||
>>>
|
||||
>>>
|
||||
>>> fido = Dog("poodle", "white")
|
||||
>>>
|
||||
>>> st.help(fido)
|
||||
|
||||
.. output::
|
||||
https://doc-string1.streamlit.app/
|
||||
height: 300px
|
||||
|
||||
And if you're using Magic, you can get help for functions, classes,
|
||||
and modules without even typing ``st.help``:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas
|
||||
>>>
|
||||
>>> # Get help for Pandas read_csv:
|
||||
>>> pandas.read_csv
|
||||
>>>
|
||||
>>> # Get help for Streamlit itself:
|
||||
>>> st
|
||||
|
||||
.. output::
|
||||
https://doc-string2.streamlit.app/
|
||||
height: 700px
|
||||
"""
|
||||
doc_string_proto = DocStringProto()
|
||||
_marshall(doc_string_proto, obj)
|
||||
return self.dg._enqueue("doc_string", doc_string_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
def _marshall(doc_string_proto: DocStringProto, obj: Any) -> None:
|
||||
"""Construct a DocString object.
|
||||
|
||||
See DeltaGenerator.help for docs.
|
||||
"""
|
||||
var_name = _get_variable_name()
|
||||
if var_name is not None:
|
||||
doc_string_proto.name = var_name
|
||||
|
||||
obj_type = _get_type_as_str(obj)
|
||||
doc_string_proto.type = obj_type
|
||||
|
||||
obj_docs = _get_docstring(obj)
|
||||
if obj_docs is not None:
|
||||
doc_string_proto.doc_string = obj_docs
|
||||
|
||||
obj_value = _get_value(obj, var_name)
|
||||
if obj_value is not None:
|
||||
doc_string_proto.value = obj_value
|
||||
|
||||
doc_string_proto.members.extend(_get_members(obj))
|
||||
|
||||
|
||||
def _get_name(obj):
|
||||
# Try to get the fully-qualified name of the object.
|
||||
# For example:
|
||||
# st.help(bar.Baz(123))
|
||||
#
|
||||
# The name is bar.Baz
|
||||
name = getattr(obj, "__qualname__", None)
|
||||
if name:
|
||||
return name
|
||||
|
||||
# Try to get the name of the object.
|
||||
# For example:
|
||||
# st.help(bar.Baz(123))
|
||||
#
|
||||
# The name is Baz
|
||||
return getattr(obj, "__name__", None)
|
||||
|
||||
|
||||
def _get_module(obj):
|
||||
return getattr(obj, "__module__", None)
|
||||
|
||||
|
||||
def _get_signature(obj):
|
||||
if not inspect.isclass(obj) and not callable(obj):
|
||||
return None
|
||||
|
||||
sig = ""
|
||||
|
||||
# TODO: Can we replace below with this?
|
||||
# with contextlib.suppress(ValueError):
|
||||
# sig = str(inspect.signature(obj))
|
||||
|
||||
try:
|
||||
sig = str(inspect.signature(obj))
|
||||
except ValueError:
|
||||
sig = "(...)"
|
||||
except TypeError:
|
||||
return None
|
||||
|
||||
is_delta_gen = False
|
||||
with contextlib.suppress(AttributeError):
|
||||
is_delta_gen = obj.__module__ == "streamlit.delta_generator"
|
||||
# Functions such as numpy.minimum don't have a __module__ attribute,
|
||||
# since we're only using it to check if its a DeltaGenerator, its ok
|
||||
# to continue
|
||||
|
||||
if is_delta_gen:
|
||||
for prefix in CONFUSING_STREAMLIT_SIG_PREFIXES:
|
||||
if sig.startswith(prefix):
|
||||
sig = sig.replace(prefix, "(")
|
||||
break
|
||||
|
||||
return sig
|
||||
|
||||
|
||||
def _get_docstring(obj):
|
||||
doc_string = inspect.getdoc(obj)
|
||||
|
||||
# Sometimes an object has no docstring, but the object's type does.
|
||||
# If that's the case here, use the type's docstring.
|
||||
# For objects where type is "type" we do not print the docs (e.g. int).
|
||||
# We also do not print the docs for functions and methods if the docstring is empty.
|
||||
if doc_string is None:
|
||||
obj_type = type(obj)
|
||||
|
||||
if (
|
||||
obj_type is not type
|
||||
and obj_type is not types.ModuleType
|
||||
and not inspect.isfunction(obj)
|
||||
and not inspect.ismethod(obj)
|
||||
):
|
||||
doc_string = inspect.getdoc(obj_type)
|
||||
|
||||
if doc_string:
|
||||
return doc_string.strip()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _get_variable_name():
|
||||
"""Try to get the name of the variable in the current line, as set by the user.
|
||||
|
||||
For example:
|
||||
foo = bar.Baz(123)
|
||||
st.help(foo)
|
||||
|
||||
The name is "foo"
|
||||
"""
|
||||
code = _get_current_line_of_code_as_str()
|
||||
|
||||
if code is None:
|
||||
return None
|
||||
|
||||
return _get_variable_name_from_code_str(code)
|
||||
|
||||
|
||||
def _get_variable_name_from_code_str(code):
|
||||
tree = ast.parse(code)
|
||||
|
||||
# Example:
|
||||
#
|
||||
# tree = Module(
|
||||
# body=[
|
||||
# Expr(
|
||||
# value=Call(
|
||||
# args=[
|
||||
# Name(id='the variable name')
|
||||
# ],
|
||||
# keywords=[
|
||||
# ???
|
||||
# ],
|
||||
# )
|
||||
# )
|
||||
# ]
|
||||
# )
|
||||
|
||||
# Check if this is an magic call (i.e. it's not st.help or st.write).
|
||||
# If that's the case, just clean it up and return it.
|
||||
if not _is_stcommand(tree, command_name="help") and not _is_stcommand(
|
||||
tree, command_name="write"
|
||||
):
|
||||
# A common pattern is to add "," at the end of a magic command to make it print.
|
||||
# This removes that final ",", so it looks nicer.
|
||||
code = code.removesuffix(",")
|
||||
|
||||
return code
|
||||
|
||||
arg_node = _get_stcommand_arg(tree)
|
||||
|
||||
# If st.help() is called without an argument, return no variable name.
|
||||
if not arg_node:
|
||||
return None
|
||||
|
||||
# If walrus, get name.
|
||||
# E.g. st.help(foo := 123) should give you "foo".
|
||||
elif type(arg_node) is ast.NamedExpr:
|
||||
# This next "if" will always be true, but need to add this for the type-checking test to
|
||||
# pass.
|
||||
if type(arg_node.target) is ast.Name:
|
||||
return arg_node.target.id
|
||||
|
||||
# If constant, there's no variable name.
|
||||
# E.g. st.help("foo") or st.help(123) should give you None.
|
||||
elif type(arg_node) is ast.Constant:
|
||||
return None
|
||||
|
||||
# Otherwise, return whatever is inside st.help(<-- here -->)
|
||||
|
||||
# But, if multiline, only return the first line.
|
||||
code_lines = code.split("\n")
|
||||
is_multiline = len(code_lines) > 1
|
||||
|
||||
start_offset = arg_node.col_offset
|
||||
|
||||
if is_multiline:
|
||||
first_lineno = arg_node.lineno - 1 # Lines are 1-indexed!
|
||||
first_line = code_lines[first_lineno]
|
||||
end_offset = None
|
||||
|
||||
else:
|
||||
first_line = code_lines[0]
|
||||
end_offset = getattr(arg_node, "end_col_offset", -1)
|
||||
|
||||
return first_line[start_offset:end_offset]
|
||||
|
||||
|
||||
_NEWLINES = re.compile(r"[\n\r]+")
|
||||
|
||||
|
||||
def _get_current_line_of_code_as_str():
|
||||
scriptrunner_frame = _get_scriptrunner_frame()
|
||||
|
||||
if scriptrunner_frame is None:
|
||||
# If there's no ScriptRunner frame, something weird is going on. This
|
||||
# can happen when the script is executed with `python myscript.py`.
|
||||
# Either way, let's bail out nicely just in case there's some valid
|
||||
# edge case where this is OK.
|
||||
return None
|
||||
|
||||
code_context = scriptrunner_frame.code_context
|
||||
|
||||
if not code_context:
|
||||
# Sometimes a frame has no code_context. This can happen inside certain exec() calls, for
|
||||
# example. If this happens, we can't determine the variable name. Just return.
|
||||
# For the background on why exec() doesn't produce code_context, see
|
||||
# https://stackoverflow.com/a/12072941
|
||||
return None
|
||||
|
||||
code_as_string = "".join(code_context)
|
||||
return re.sub(_NEWLINES, "", code_as_string.strip())
|
||||
|
||||
|
||||
def _get_scriptrunner_frame():
|
||||
prev_frame = None
|
||||
scriptrunner_frame = None
|
||||
|
||||
# Look back in call stack to get the variable name passed into st.help().
|
||||
# The frame *before* the ScriptRunner frame is the correct one.
|
||||
# IMPORTANT: This will change if we refactor the code. But hopefully our tests will catch the
|
||||
# issue and we'll fix it before it lands upstream!
|
||||
for frame in inspect.stack():
|
||||
# Check if this is running inside a funny "exec()" block that won't provide the info we
|
||||
# need. If so, just quit.
|
||||
if frame.code_context is None:
|
||||
return None
|
||||
|
||||
if frame.filename == SCRIPTRUNNER_FILENAME:
|
||||
scriptrunner_frame = prev_frame
|
||||
break
|
||||
|
||||
prev_frame = frame
|
||||
|
||||
return scriptrunner_frame
|
||||
|
||||
|
||||
def _is_stcommand(tree, command_name):
|
||||
"""Checks whether the AST in tree is a call for command_name."""
|
||||
root_node = tree.body[0].value
|
||||
|
||||
if not isinstance(root_node, ast.Call):
|
||||
return False
|
||||
|
||||
return (
|
||||
# st call called without module. E.g. "help()"
|
||||
getattr(root_node.func, "id", None) == command_name
|
||||
or
|
||||
# st call called with module. E.g. "foo.help()" (where usually "foo" is "st")
|
||||
getattr(root_node.func, "attr", None) == command_name
|
||||
)
|
||||
|
||||
|
||||
def _get_stcommand_arg(tree):
|
||||
"""Gets the argument node for the st command in tree (AST)."""
|
||||
|
||||
root_node = tree.body[0].value
|
||||
|
||||
if root_node.args:
|
||||
return root_node.args[0]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _get_type_as_str(obj):
|
||||
if inspect.isclass(obj):
|
||||
return "class"
|
||||
|
||||
return str(type(obj).__name__)
|
||||
|
||||
|
||||
def _get_first_line(text):
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
left, _, _ = text.partition("\n")
|
||||
return left
|
||||
|
||||
|
||||
def _get_weight(value):
|
||||
if inspect.ismodule(value):
|
||||
return 3
|
||||
if inspect.isclass(value):
|
||||
return 2
|
||||
if callable(value):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def _get_value(obj, var_name):
|
||||
obj_value = _get_human_readable_value(obj)
|
||||
|
||||
if obj_value is not None:
|
||||
return obj_value
|
||||
|
||||
# If there's no human-readable value, it's some complex object.
|
||||
# So let's provide other info about it.
|
||||
name = _get_name(obj)
|
||||
|
||||
if name:
|
||||
name_obj = obj
|
||||
else:
|
||||
# If the object itself doesn't have a name, then it's probably an instance
|
||||
# of some class Foo. So let's show info about Foo in the value slot.
|
||||
name_obj = type(obj)
|
||||
name = _get_name(name_obj)
|
||||
|
||||
module = _get_module(name_obj)
|
||||
sig = _get_signature(name_obj) or ""
|
||||
|
||||
if name:
|
||||
if module:
|
||||
obj_value = f"{module}.{name}{sig}"
|
||||
else:
|
||||
obj_value = f"{name}{sig}"
|
||||
|
||||
if obj_value == var_name:
|
||||
# No need to repeat the same info.
|
||||
# For example: st.help(re) shouldn't show "re module re", just "re module".
|
||||
obj_value = None
|
||||
|
||||
return obj_value
|
||||
|
||||
|
||||
def _get_human_readable_value(value):
|
||||
if isinstance(value, Secrets):
|
||||
# Don't want to read secrets.toml because that will show a warning if there's no
|
||||
# secrets.toml file.
|
||||
return None
|
||||
|
||||
if inspect.isclass(value) or inspect.ismodule(value) or callable(value):
|
||||
return None
|
||||
|
||||
value_str = repr(value)
|
||||
|
||||
if isinstance(value, str):
|
||||
# Special-case strings as human-readable because they're allowed to look like
|
||||
# "<foo blarg at 0x15ee6f9a0>".
|
||||
return _shorten(value_str)
|
||||
|
||||
if is_mem_address_str(value_str):
|
||||
# If value_str looks like "<foo blarg at 0x15ee6f9a0>" it's not human readable.
|
||||
return None
|
||||
|
||||
return _shorten(value_str)
|
||||
|
||||
|
||||
def _shorten(s, length=300):
|
||||
s = s.strip()
|
||||
return s[:length] + "..." if len(s) > length else s
|
||||
|
||||
|
||||
def _is_computed_property(obj, attr_name):
|
||||
obj_class = getattr(obj, "__class__", None)
|
||||
|
||||
if not obj_class:
|
||||
return False
|
||||
|
||||
# Go through superclasses in order of inheritance (mro) to see if any of them have an
|
||||
# attribute called attr_name. If so, check if it's a @property.
|
||||
for parent_class in inspect.getmro(obj_class):
|
||||
class_attr = getattr(parent_class, attr_name, None)
|
||||
|
||||
if class_attr is None:
|
||||
continue
|
||||
|
||||
# If is property, return it.
|
||||
if isinstance(class_attr, property) or inspect.isgetsetdescriptor(class_attr):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _get_members(obj):
|
||||
members_for_sorting = []
|
||||
|
||||
for attr_name in dir(obj):
|
||||
if attr_name.startswith("_"):
|
||||
continue
|
||||
|
||||
try:
|
||||
is_computed_value = _is_computed_property(obj, attr_name)
|
||||
if is_computed_value:
|
||||
parent_attr = getattr(obj.__class__, attr_name)
|
||||
|
||||
member_type = "property"
|
||||
|
||||
weight = 0
|
||||
member_docs = _get_docstring(parent_attr)
|
||||
member_value = None
|
||||
else:
|
||||
attr_value = getattr(obj, attr_name)
|
||||
weight = _get_weight(attr_value)
|
||||
|
||||
human_readable_value = _get_human_readable_value(attr_value)
|
||||
|
||||
member_type = _get_type_as_str(attr_value)
|
||||
|
||||
if human_readable_value is None:
|
||||
member_docs = _get_docstring(attr_value)
|
||||
member_value = None
|
||||
else:
|
||||
member_docs = None
|
||||
member_value = human_readable_value
|
||||
except AttributeError:
|
||||
# If there's an AttributeError, we can just skip it.
|
||||
# This can happen when members are exposed with `dir()`
|
||||
# but are conditionally unavailable.
|
||||
continue
|
||||
|
||||
if member_type == "module":
|
||||
# Don't pollute the output with all imported modules.
|
||||
continue
|
||||
|
||||
member = MemberProto()
|
||||
member.name = attr_name
|
||||
member.type = member_type
|
||||
|
||||
if member_docs is not None:
|
||||
member.doc_string = _get_first_line(member_docs)
|
||||
|
||||
if member_value is not None:
|
||||
member.value = member_value
|
||||
|
||||
members_for_sorting.append((weight, member))
|
||||
|
||||
if members_for_sorting:
|
||||
sorted_members = sorted(members_for_sorting, key=lambda x: (x[0], x[1].name))
|
||||
return [m for _, m in sorted_members]
|
||||
|
||||
return []
|
||||
130
myenv/lib/python3.11/site-packages/streamlit/elements/empty.py
Normal file
130
myenv/lib/python3.11/site-packages/streamlit/elements/empty.py
Normal file
@@ -0,0 +1,130 @@
|
||||
# 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 typing import TYPE_CHECKING, cast
|
||||
|
||||
from streamlit.proto.Empty_pb2 import Empty as EmptyProto
|
||||
from streamlit.proto.Skeleton_pb2 import Skeleton as SkeletonProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
class EmptyMixin:
|
||||
@gather_metrics("empty")
|
||||
def empty(self) -> DeltaGenerator:
|
||||
"""Insert a single-element container.
|
||||
|
||||
Inserts a container into your app that can be used to hold a single element.
|
||||
This allows you to, for example, remove elements at any point, or replace
|
||||
several elements at once (using a child multi-element container).
|
||||
|
||||
To insert/replace/clear an element on the returned container, you can
|
||||
use ``with`` notation or just call methods directly on the returned object.
|
||||
See examples below.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Inside a ``with st.empty():`` block, each displayed element will
|
||||
replace the previous one.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import time
|
||||
>>>
|
||||
>>> with st.empty():
|
||||
... for seconds in range(10):
|
||||
... st.write(f"⏳ {seconds} seconds have passed")
|
||||
... time.sleep(1)
|
||||
... st.write(":material/check: 10 seconds over!")
|
||||
... st.button("Rerun")
|
||||
|
||||
.. output::
|
||||
https://doc-empty.streamlit.app/
|
||||
height: 220px
|
||||
|
||||
You can use an ``st.empty`` to replace multiple elements in
|
||||
succession. Use ``st.container`` inside ``st.empty`` to display (and
|
||||
later replace) a group of elements.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import time
|
||||
>>>
|
||||
>>> st.button("Start over")
|
||||
>>>
|
||||
>>> placeholder = st.empty()
|
||||
>>> placeholder.markdown("Hello")
|
||||
>>> time.sleep(1)
|
||||
>>>
|
||||
>>> placeholder.progress(0, "Wait for it...")
|
||||
>>> time.sleep(1)
|
||||
>>> placeholder.progress(50, "Wait for it...")
|
||||
>>> time.sleep(1)
|
||||
>>> placeholder.progress(100, "Wait for it...")
|
||||
>>> time.sleep(1)
|
||||
>>>
|
||||
>>> with placeholder.container():
|
||||
... st.line_chart({"data": [1, 5, 2, 6]})
|
||||
... time.sleep(1)
|
||||
... st.markdown("3...")
|
||||
... time.sleep(1)
|
||||
... st.markdown("2...")
|
||||
... time.sleep(1)
|
||||
... st.markdown("1...")
|
||||
... time.sleep(1)
|
||||
>>>
|
||||
>>> placeholder.markdown("Poof!")
|
||||
>>> time.sleep(1)
|
||||
>>>
|
||||
>>> placeholder.empty()
|
||||
|
||||
.. output::
|
||||
https://doc-empty-placeholder.streamlit.app/
|
||||
height: 600px
|
||||
|
||||
"""
|
||||
empty_proto = EmptyProto()
|
||||
return self.dg._enqueue("empty", empty_proto)
|
||||
|
||||
@gather_metrics("_skeleton")
|
||||
def _skeleton(self, *, height: int | None = None) -> DeltaGenerator:
|
||||
"""Insert a single-element container which displays a "skeleton" placeholder.
|
||||
|
||||
Inserts a container into your app that can be used to hold a single element.
|
||||
This allows you to, for example, remove elements at any point, or replace
|
||||
several elements at once (using a child multi-element container).
|
||||
|
||||
To insert/replace/clear an element on the returned container, you can
|
||||
use ``with`` notation or just call methods directly on the returned object.
|
||||
See some of the examples below.
|
||||
|
||||
This is an internal method and should not be used directly.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
height: int or None
|
||||
Desired height of the skeleton expressed in pixels. If None, a
|
||||
default height is used.
|
||||
"""
|
||||
skeleton_proto = SkeletonProto()
|
||||
if height:
|
||||
skeleton_proto.height = height
|
||||
return self.dg._enqueue("skeleton", skeleton_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
@@ -0,0 +1,341 @@
|
||||
# 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 os
|
||||
import traceback
|
||||
from typing import TYPE_CHECKING, Callable, Final, TypeVar, cast
|
||||
|
||||
from streamlit import config
|
||||
from streamlit.errors import (
|
||||
MarkdownFormattedException,
|
||||
StreamlitAPIWarning,
|
||||
)
|
||||
from streamlit.logger import get_logger
|
||||
from streamlit.proto.Exception_pb2 import Exception as ExceptionProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
_LOGGER: Final = get_logger(__name__)
|
||||
|
||||
# When client.showErrorDetails is False, we show a generic warning in the
|
||||
# frontend when we encounter an uncaught app exception.
|
||||
_GENERIC_UNCAUGHT_EXCEPTION_TEXT: Final = "This app has encountered an error. The original error message is redacted to prevent data leaks. Full error details have been recorded in the logs (if you're on Streamlit Cloud, click on 'Manage app' in the lower right of your app)."
|
||||
|
||||
|
||||
class ExceptionMixin:
|
||||
@gather_metrics("exception")
|
||||
def exception(self, exception: BaseException) -> DeltaGenerator:
|
||||
"""Display an exception.
|
||||
|
||||
In the lower-right corner of the exception, Streamlit displays links to
|
||||
Google and ChatGPT that are prefilled with the contents of the
|
||||
exception message.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
exception : Exception
|
||||
The exception to display.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> e = RuntimeError("This is an exception of type RuntimeError")
|
||||
>>> st.exception(e)
|
||||
|
||||
.. output ::
|
||||
https://doc-status-exception.streamlit.app/
|
||||
height: 220px
|
||||
|
||||
"""
|
||||
return _exception(self.dg, exception)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
# TODO(lawilby): confirm whether we want to track metrics here with lukasmasuch.
|
||||
@gather_metrics("exception")
|
||||
def _exception(
|
||||
dg: DeltaGenerator,
|
||||
exception: BaseException,
|
||||
is_uncaught_app_exception: bool = False,
|
||||
) -> DeltaGenerator:
|
||||
exception_proto = ExceptionProto()
|
||||
marshall(exception_proto, exception, is_uncaught_app_exception)
|
||||
return dg._enqueue("exception", exception_proto)
|
||||
|
||||
|
||||
def marshall(
|
||||
exception_proto: ExceptionProto,
|
||||
exception: BaseException,
|
||||
is_uncaught_app_exception: bool = False,
|
||||
) -> None:
|
||||
"""Marshalls an Exception.proto message.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
exception_proto : Exception.proto
|
||||
The Exception protobuf to fill out.
|
||||
|
||||
exception : BaseException
|
||||
The exception whose data we're extracting.
|
||||
|
||||
is_uncaught_app_exception: bool
|
||||
The exception originates from an uncaught error during script execution.
|
||||
"""
|
||||
is_markdown_exception = isinstance(exception, MarkdownFormattedException)
|
||||
|
||||
# Some exceptions (like UserHashError) have an alternate_name attribute so
|
||||
# we can pretend to the user that the exception is called something else.
|
||||
if getattr(exception, "alternate_name", None) is not None:
|
||||
exception_proto.type = exception.alternate_name # type: ignore[attr-defined]
|
||||
else:
|
||||
exception_proto.type = type(exception).__name__
|
||||
|
||||
stack_trace = _get_stack_trace_str_list(exception)
|
||||
|
||||
exception_proto.stack_trace.extend(stack_trace)
|
||||
exception_proto.is_warning = isinstance(exception, Warning)
|
||||
|
||||
try:
|
||||
if isinstance(exception, SyntaxError):
|
||||
# SyntaxErrors have additional fields (filename, text, lineno,
|
||||
# offset) that we can use for a nicely-formatted message telling
|
||||
# the user what to fix.
|
||||
exception_proto.message = _format_syntax_error_message(exception)
|
||||
else:
|
||||
exception_proto.message = str(exception).strip()
|
||||
exception_proto.message_is_markdown = is_markdown_exception
|
||||
|
||||
except Exception as str_exception:
|
||||
# Sometimes the exception's __str__/__unicode__ method itself
|
||||
# raises an error.
|
||||
exception_proto.message = ""
|
||||
_LOGGER.warning(
|
||||
"""
|
||||
|
||||
Streamlit was unable to parse the data from an exception in the user's script.
|
||||
This is usually due to a bug in the Exception object itself. Here is some info
|
||||
about that Exception object, so you can report a bug to the original author:
|
||||
|
||||
Exception type:
|
||||
%s
|
||||
|
||||
Problem:
|
||||
%s
|
||||
|
||||
Traceback:
|
||||
%s
|
||||
|
||||
""",
|
||||
type(exception).__name__,
|
||||
str_exception,
|
||||
"\n".join(_get_stack_trace_str_list(str_exception)),
|
||||
)
|
||||
|
||||
if is_uncaught_app_exception:
|
||||
show_error_details = config.get_option("client.showErrorDetails")
|
||||
|
||||
show_message = (
|
||||
show_error_details == config.ShowErrorDetailsConfigOptions.FULL
|
||||
or config.ShowErrorDetailsConfigOptions.is_true_variation(
|
||||
show_error_details
|
||||
)
|
||||
)
|
||||
# False is a legacy config option still in-use in community cloud. It is equivalent
|
||||
# to "stacktrace".
|
||||
show_trace = (
|
||||
show_message
|
||||
or show_error_details == config.ShowErrorDetailsConfigOptions.STACKTRACE
|
||||
or config.ShowErrorDetailsConfigOptions.is_false_variation(
|
||||
show_error_details
|
||||
)
|
||||
)
|
||||
show_type = (
|
||||
show_trace
|
||||
or show_error_details == config.ShowErrorDetailsConfigOptions.TYPE
|
||||
)
|
||||
|
||||
if not show_message:
|
||||
exception_proto.message = _GENERIC_UNCAUGHT_EXCEPTION_TEXT
|
||||
if not show_type:
|
||||
exception_proto.ClearField("type")
|
||||
else:
|
||||
type_str = str(type(exception))
|
||||
exception_proto.type = type_str.replace("<class '", "").replace("'>", "")
|
||||
if not show_trace:
|
||||
exception_proto.ClearField("stack_trace")
|
||||
|
||||
|
||||
def _format_syntax_error_message(exception: SyntaxError) -> str:
|
||||
"""Returns a nicely formatted SyntaxError message that emulates
|
||||
what the Python interpreter outputs.
|
||||
|
||||
For example:
|
||||
|
||||
> File "raven.py", line 3
|
||||
> st.write('Hello world!!'))
|
||||
> ^
|
||||
> SyntaxError: invalid syntax
|
||||
|
||||
"""
|
||||
if exception.text:
|
||||
if exception.offset is not None:
|
||||
caret_indent = " " * max(exception.offset - 1, 0)
|
||||
else:
|
||||
caret_indent = ""
|
||||
|
||||
return (
|
||||
'File "%(filename)s", line %(lineno)s\n'
|
||||
" %(text)s\n"
|
||||
" %(caret_indent)s^\n"
|
||||
"%(errname)s: %(msg)s"
|
||||
% {
|
||||
"filename": exception.filename,
|
||||
"lineno": exception.lineno,
|
||||
"text": exception.text.rstrip(),
|
||||
"caret_indent": caret_indent,
|
||||
"errname": type(exception).__name__,
|
||||
"msg": exception.msg,
|
||||
}
|
||||
)
|
||||
# If a few edge cases, SyntaxErrors don't have all these nice fields. So we
|
||||
# have a fall back here.
|
||||
# Example edge case error message: encoding declaration in Unicode string
|
||||
return str(exception)
|
||||
|
||||
|
||||
def _get_stack_trace_str_list(exception: BaseException) -> list[str]:
|
||||
"""Get the stack trace for the given exception.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
exception : BaseException
|
||||
The exception to extract the traceback from
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple of two string lists
|
||||
The exception traceback as two lists of strings. The first represents the part
|
||||
of the stack trace the users don't typically want to see, containing internal
|
||||
Streamlit code. The second is whatever comes after the Streamlit stack trace,
|
||||
which is usually what the user wants.
|
||||
|
||||
"""
|
||||
extracted_traceback: traceback.StackSummary | None = None
|
||||
if isinstance(exception, StreamlitAPIWarning):
|
||||
extracted_traceback = exception.tacked_on_stack
|
||||
elif hasattr(exception, "__traceback__"):
|
||||
extracted_traceback = traceback.extract_tb(exception.__traceback__)
|
||||
|
||||
# Format the extracted traceback and add it to the protobuf element.
|
||||
if extracted_traceback is None:
|
||||
trace_str_list = [
|
||||
"Cannot extract the stack trace for this exception. "
|
||||
"Try calling exception() within the `catch` block."
|
||||
]
|
||||
else:
|
||||
internal_frames, external_frames = _split_internal_streamlit_frames(
|
||||
extracted_traceback
|
||||
)
|
||||
|
||||
if external_frames:
|
||||
trace_str_list = traceback.format_list(external_frames)
|
||||
else:
|
||||
trace_str_list = traceback.format_list(internal_frames)
|
||||
|
||||
trace_str_list = [item.strip() for item in trace_str_list]
|
||||
|
||||
return trace_str_list
|
||||
|
||||
|
||||
def _is_in_package(file: str, package_path: str) -> bool:
|
||||
"""True if the given file is part of package_path."""
|
||||
try:
|
||||
common_prefix = os.path.commonprefix([os.path.realpath(file), package_path])
|
||||
except ValueError:
|
||||
# Raised if paths are on different drives.
|
||||
return False
|
||||
|
||||
return common_prefix == package_path
|
||||
|
||||
|
||||
def _split_internal_streamlit_frames(
|
||||
extracted_tb: traceback.StackSummary,
|
||||
) -> tuple[list[traceback.FrameSummary], list[traceback.FrameSummary]]:
|
||||
"""Split the traceback into a Streamlit-internal part and an external part.
|
||||
|
||||
The internal part is everything up to (but excluding) the first frame belonging to
|
||||
the user's code. The external part is everything else.
|
||||
|
||||
So if the stack looks like this:
|
||||
|
||||
1. Streamlit frame
|
||||
2. Pandas frame
|
||||
3. Altair frame
|
||||
4. Streamlit frame
|
||||
5. User frame
|
||||
6. User frame
|
||||
7. Streamlit frame
|
||||
8. Matplotlib frame
|
||||
|
||||
...then this should return 1-4 as the internal traceback and 5-8 as the external.
|
||||
|
||||
(Note that something like the example above is extremely unlikely to happen since
|
||||
it's not like Altair is calling Streamlit code, but you get the idea.)
|
||||
"""
|
||||
|
||||
ctx = get_script_run_ctx()
|
||||
|
||||
if not ctx:
|
||||
return [], list(extracted_tb)
|
||||
|
||||
package_path = os.path.join(os.path.realpath(str(ctx.main_script_parent)), "")
|
||||
|
||||
return _split_list(
|
||||
extracted_tb,
|
||||
split_point=lambda tb: _is_in_package(tb.filename, package_path),
|
||||
)
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def _split_list(
|
||||
orig_list: list[T], split_point: Callable[[T], bool]
|
||||
) -> tuple[list[T], list[T]]:
|
||||
before: list[T] = []
|
||||
after: list[T] = []
|
||||
|
||||
saw_split_point = False
|
||||
|
||||
for item in orig_list:
|
||||
if not saw_split_point:
|
||||
if split_point(item):
|
||||
saw_split_point = True
|
||||
|
||||
if saw_split_point:
|
||||
after.append(item)
|
||||
else:
|
||||
before.append(item)
|
||||
|
||||
return before, after
|
||||
354
myenv/lib/python3.11/site-packages/streamlit/elements/form.py
Normal file
354
myenv/lib/python3.11/site-packages/streamlit/elements/form.py
Normal file
@@ -0,0 +1,354 @@
|
||||
# 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 textwrap
|
||||
from typing import TYPE_CHECKING, Literal, cast
|
||||
|
||||
from streamlit.elements.lib.form_utils import FormData, current_form_id, is_in_form
|
||||
from streamlit.elements.lib.policies import (
|
||||
check_cache_replay_rules,
|
||||
check_session_state_rules,
|
||||
)
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto import Block_pb2
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.runtime.state import WidgetArgs, WidgetCallback, WidgetKwargs
|
||||
|
||||
|
||||
def _build_duplicate_form_message(user_key: str | None = None) -> str:
|
||||
if user_key is not None:
|
||||
message = textwrap.dedent(
|
||||
f"""
|
||||
There are multiple identical forms with `key='{user_key}'`.
|
||||
|
||||
To fix this, please make sure that the `key` argument is unique for
|
||||
each `st.form` you create.
|
||||
"""
|
||||
)
|
||||
else:
|
||||
message = textwrap.dedent(
|
||||
"""
|
||||
There are multiple identical forms with the same generated key.
|
||||
|
||||
When a form is created, it's assigned an internal key based on
|
||||
its structure. Multiple forms with an identical structure will
|
||||
result in the same internal key, which causes this error.
|
||||
|
||||
To fix this error, please pass a unique `key` argument to
|
||||
`st.form`.
|
||||
"""
|
||||
)
|
||||
|
||||
return message.strip("\n")
|
||||
|
||||
|
||||
class FormMixin:
|
||||
@gather_metrics("form")
|
||||
def form(
|
||||
self,
|
||||
key: str,
|
||||
clear_on_submit: bool = False,
|
||||
*,
|
||||
enter_to_submit: bool = True,
|
||||
border: bool = True,
|
||||
) -> DeltaGenerator:
|
||||
"""Create a form that batches elements together with a "Submit" button.
|
||||
|
||||
A form is a container that visually groups other elements and
|
||||
widgets together, and contains a Submit button. When the form's
|
||||
Submit button is pressed, all widget values inside the form will be
|
||||
sent to Streamlit in a batch.
|
||||
|
||||
To add elements to a form object, you can use ``with`` notation
|
||||
(preferred) or just call methods directly on the form. See
|
||||
examples below.
|
||||
|
||||
Forms have a few constraints:
|
||||
|
||||
- Every form must contain a ``st.form_submit_button``.
|
||||
- ``st.button`` and ``st.download_button`` cannot be added to a form.
|
||||
- Forms can appear anywhere in your app (sidebar, columns, etc),
|
||||
but they cannot be embedded inside other forms.
|
||||
- Within a form, the only widget that can have a callback function is
|
||||
``st.form_submit_button``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
key : str
|
||||
A string that identifies the form. Each form must have its own
|
||||
key. (This key is not displayed to the user in the interface.)
|
||||
clear_on_submit : bool
|
||||
If True, all widgets inside the form will be reset to their default
|
||||
values after the user presses the Submit button. Defaults to False.
|
||||
(Note that Custom Components are unaffected by this flag, and
|
||||
will not be reset to their defaults on form submission.)
|
||||
enter_to_submit : bool
|
||||
Whether to submit the form when a user presses Enter while
|
||||
interacting with a widget inside the form.
|
||||
|
||||
If this is ``True`` (default), pressing Enter while interacting
|
||||
with a form widget is equivalent to clicking the first
|
||||
``st.form_submit_button`` in the form.
|
||||
|
||||
If this is ``False``, the user must click an
|
||||
``st.form_submit_button`` to submit the form.
|
||||
|
||||
If the first ``st.form_submit_button`` in the form is disabled,
|
||||
the form will override submission behavior with
|
||||
``enter_to_submit=False``.
|
||||
|
||||
border : bool
|
||||
Whether to show a border around the form. Defaults to True.
|
||||
|
||||
.. note::
|
||||
Not showing a border can be confusing to viewers since interacting with a
|
||||
widget in the form will do nothing. You should only remove the border if
|
||||
there's another border (e.g. because of an expander) or the form is small
|
||||
(e.g. just a text input and a submit button).
|
||||
|
||||
Examples
|
||||
--------
|
||||
Inserting elements using ``with`` notation:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> with st.form("my_form"):
|
||||
... st.write("Inside the form")
|
||||
... slider_val = st.slider("Form slider")
|
||||
... checkbox_val = st.checkbox("Form checkbox")
|
||||
...
|
||||
... # Every form must have a submit button.
|
||||
... submitted = st.form_submit_button("Submit")
|
||||
... if submitted:
|
||||
... st.write("slider", slider_val, "checkbox", checkbox_val)
|
||||
>>> st.write("Outside the form")
|
||||
|
||||
.. output::
|
||||
https://doc-form1.streamlit.app/
|
||||
height: 425px
|
||||
|
||||
Inserting elements out of order:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> form = st.form("my_form")
|
||||
>>> form.slider("Inside the form")
|
||||
>>> st.slider("Outside the form")
|
||||
>>>
|
||||
>>> # Now add a submit button to the form:
|
||||
>>> form.form_submit_button("Submit")
|
||||
|
||||
.. output::
|
||||
https://doc-form2.streamlit.app/
|
||||
height: 375px
|
||||
|
||||
"""
|
||||
if is_in_form(self.dg):
|
||||
raise StreamlitAPIException("Forms cannot be nested in other forms.")
|
||||
|
||||
check_cache_replay_rules()
|
||||
check_session_state_rules(default_value=None, key=key, writes_allowed=False)
|
||||
|
||||
# A form is uniquely identified by its key.
|
||||
form_id = key
|
||||
|
||||
ctx = get_script_run_ctx()
|
||||
if ctx is not None:
|
||||
new_form_id = form_id not in ctx.form_ids_this_run
|
||||
if new_form_id:
|
||||
ctx.form_ids_this_run.add(form_id)
|
||||
else:
|
||||
raise StreamlitAPIException(_build_duplicate_form_message(key))
|
||||
|
||||
block_proto = Block_pb2.Block()
|
||||
block_proto.form.form_id = form_id
|
||||
block_proto.form.clear_on_submit = clear_on_submit
|
||||
block_proto.form.enter_to_submit = enter_to_submit
|
||||
block_proto.form.border = border
|
||||
block_dg = self.dg._block(block_proto)
|
||||
|
||||
# Attach the form's button info to the newly-created block's
|
||||
# DeltaGenerator.
|
||||
block_dg._form_data = FormData(form_id)
|
||||
return block_dg
|
||||
|
||||
@gather_metrics("form_submit_button")
|
||||
def form_submit_button(
|
||||
self,
|
||||
label: str = "Submit",
|
||||
help: str | None = None,
|
||||
on_click: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
*, # keyword-only arguments:
|
||||
type: Literal["primary", "secondary", "tertiary"] = "secondary",
|
||||
icon: str | None = None,
|
||||
disabled: bool = False,
|
||||
use_container_width: bool = False,
|
||||
) -> bool:
|
||||
r"""Display a form submit button.
|
||||
|
||||
When this button is clicked, all widget values inside the form will be
|
||||
sent from the user's browser to your Streamlit server in a batch.
|
||||
|
||||
Every form must have at least one ``st.form_submit_button``. An
|
||||
``st.form_submit_button`` cannot exist outside of a form.
|
||||
|
||||
For more information about forms, check out our `docs
|
||||
<https://docs.streamlit.io/develop/concepts/architecture/forms>`_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : str
|
||||
A short label explaining to the user what this button is for. This
|
||||
defaults to ``"Submit"``. 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.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
help : str or None
|
||||
A tooltip that gets displayed when the button is hovered over. 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_click : callable
|
||||
An optional callback invoked when this button is clicked.
|
||||
args : tuple
|
||||
An optional tuple of args to pass to the callback.
|
||||
kwargs : dict
|
||||
An optional dict of kwargs to pass to the callback.
|
||||
type : "primary", "secondary", or "tertiary"
|
||||
An optional string that specifies the button type. This can be one
|
||||
of the following:
|
||||
|
||||
- ``"primary"``: The button's background is the app's primary color
|
||||
for additional emphasis.
|
||||
- ``"secondary"`` (default): The button's background coordinates
|
||||
with the app's background color for normal emphasis.
|
||||
- ``"tertiary"``: The button is plain text without a border or
|
||||
background for subtly.
|
||||
|
||||
icon : str or None
|
||||
An optional emoji or icon to display next to the button label. If ``icon``
|
||||
is ``None`` (default), no icon is displayed. If ``icon`` is a
|
||||
string, the following options are valid:
|
||||
|
||||
- A single-character emoji. For example, you can set ``icon="🚨"``
|
||||
or ``icon="🔥"``. 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.
|
||||
disabled : bool
|
||||
Whether to disable the button. If this is ``False`` (default), the
|
||||
user can interact with the button. If this is ``True``, the button
|
||||
is grayed-out and can't be clicked.
|
||||
|
||||
If the first ``st.form_submit_button`` in the form is disabled,
|
||||
the form will override submission behavior with
|
||||
``enter_to_submit=False``.
|
||||
|
||||
use_container_width : bool
|
||||
Whether to expand the button's width to fill its parent container.
|
||||
If ``use_container_width`` is ``False`` (default), Streamlit sizes
|
||||
the button to fit its contents. If ``use_container_width`` is
|
||||
``True``, the width of the button matches its parent container.
|
||||
|
||||
In both cases, if the contents of the button are wider than the
|
||||
parent container, the contents will line wrap.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if the button was clicked.
|
||||
"""
|
||||
ctx = get_script_run_ctx()
|
||||
|
||||
# Checks whether the entered button type is one of the allowed options
|
||||
if type not in ["primary", "secondary", "tertiary"]:
|
||||
raise StreamlitAPIException(
|
||||
'The type argument to st.form_submit_button must be "primary", "secondary", or "tertiary". \n'
|
||||
f'The argument passed was "{type}".'
|
||||
)
|
||||
|
||||
return self._form_submit_button(
|
||||
label=label,
|
||||
help=help,
|
||||
on_click=on_click,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
type=type,
|
||||
icon=icon,
|
||||
disabled=disabled,
|
||||
use_container_width=use_container_width,
|
||||
ctx=ctx,
|
||||
)
|
||||
|
||||
def _form_submit_button(
|
||||
self,
|
||||
label: str = "Submit",
|
||||
help: str | None = None,
|
||||
on_click: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
*, # keyword-only arguments:
|
||||
type: Literal["primary", "secondary", "tertiary"] = "secondary",
|
||||
icon: str | None = None,
|
||||
disabled: bool = False,
|
||||
use_container_width: bool = False,
|
||||
ctx: ScriptRunContext | None = None,
|
||||
) -> bool:
|
||||
form_id = current_form_id(self.dg)
|
||||
submit_button_key = f"FormSubmitter:{form_id}-{label}"
|
||||
return self.dg._button(
|
||||
label=label,
|
||||
key=submit_button_key,
|
||||
help=help,
|
||||
is_form_submitter=True,
|
||||
on_click=on_click,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
type=type,
|
||||
icon=icon,
|
||||
disabled=disabled,
|
||||
use_container_width=use_container_width,
|
||||
ctx=ctx,
|
||||
)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
@@ -0,0 +1,150 @@
|
||||
# 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.
|
||||
|
||||
"""Streamlit support for GraphViz charts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Union, cast
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit import type_util
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.GraphVizChart_pb2 import GraphVizChart as GraphVizChartProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.util import calc_md5
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import graphviz
|
||||
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
FigureOrDot: TypeAlias = Union[
|
||||
"graphviz.Graph", "graphviz.Digraph", "graphviz.Source", str
|
||||
]
|
||||
|
||||
|
||||
class GraphvizMixin:
|
||||
@gather_metrics("graphviz_chart")
|
||||
def graphviz_chart(
|
||||
self,
|
||||
figure_or_dot: FigureOrDot,
|
||||
use_container_width: bool = False,
|
||||
) -> DeltaGenerator:
|
||||
"""Display a graph using the dagre-d3 library.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
figure_or_dot : graphviz.dot.Graph, graphviz.dot.Digraph, graphviz.sources.Source, str
|
||||
The Graphlib graph object or dot string to display
|
||||
|
||||
use_container_width : bool
|
||||
Whether to override the figure's native width with the width of
|
||||
the parent container. If ``use_container_width`` is ``False``
|
||||
(default), Streamlit sets the width of the chart to fit its contents
|
||||
according to the plotting library, up to the width of the parent
|
||||
container. If ``use_container_width`` is ``True``, Streamlit sets
|
||||
the width of the figure to match the width of the parent container.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>> import graphviz
|
||||
>>>
|
||||
>>> # Create a graphlib graph object
|
||||
>>> graph = graphviz.Digraph()
|
||||
>>> graph.edge("run", "intr")
|
||||
>>> graph.edge("intr", "runbl")
|
||||
>>> graph.edge("runbl", "run")
|
||||
>>> graph.edge("run", "kernel")
|
||||
>>> graph.edge("kernel", "zombie")
|
||||
>>> graph.edge("kernel", "sleep")
|
||||
>>> graph.edge("kernel", "runmem")
|
||||
>>> graph.edge("sleep", "swap")
|
||||
>>> graph.edge("swap", "runswap")
|
||||
>>> graph.edge("runswap", "new")
|
||||
>>> graph.edge("runswap", "runmem")
|
||||
>>> graph.edge("new", "runmem")
|
||||
>>> graph.edge("sleep", "runmem")
|
||||
>>>
|
||||
>>> st.graphviz_chart(graph)
|
||||
|
||||
Or you can render the chart from the graph using GraphViz's Dot
|
||||
language:
|
||||
|
||||
>>> st.graphviz_chart('''
|
||||
digraph {
|
||||
run -> intr
|
||||
intr -> runbl
|
||||
runbl -> run
|
||||
run -> kernel
|
||||
kernel -> zombie
|
||||
kernel -> sleep
|
||||
kernel -> runmem
|
||||
sleep -> swap
|
||||
swap -> runswap
|
||||
runswap -> new
|
||||
runswap -> runmem
|
||||
new -> runmem
|
||||
sleep -> runmem
|
||||
}
|
||||
''')
|
||||
|
||||
.. output::
|
||||
https://doc-graphviz-chart.streamlit.app/
|
||||
height: 600px
|
||||
|
||||
"""
|
||||
# Generate element ID from delta path
|
||||
delta_path = self.dg._get_delta_path_str()
|
||||
element_id = calc_md5(delta_path.encode())
|
||||
|
||||
graphviz_chart_proto = GraphVizChartProto()
|
||||
|
||||
marshall(graphviz_chart_proto, figure_or_dot, use_container_width, element_id)
|
||||
return self.dg._enqueue("graphviz_chart", graphviz_chart_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
def marshall(
|
||||
proto: GraphVizChartProto,
|
||||
figure_or_dot: FigureOrDot,
|
||||
use_container_width: bool,
|
||||
element_id: str,
|
||||
) -> None:
|
||||
"""Construct a GraphViz chart object.
|
||||
|
||||
See DeltaGenerator.graphviz_chart for docs.
|
||||
"""
|
||||
|
||||
if type_util.is_graphviz_chart(figure_or_dot):
|
||||
dot = figure_or_dot.source
|
||||
engine = figure_or_dot.engine
|
||||
elif isinstance(figure_or_dot, str):
|
||||
dot = figure_or_dot
|
||||
engine = "dot"
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
"Unhandled type for graphviz chart: %s" % type(figure_or_dot)
|
||||
)
|
||||
|
||||
proto.spec = dot
|
||||
proto.engine = engine
|
||||
proto.use_container_width = use_container_width
|
||||
proto.element_id = element_id
|
||||
302
myenv/lib/python3.11/site-packages/streamlit/elements/heading.py
Normal file
302
myenv/lib/python3.11/site-packages/streamlit/elements/heading.py
Normal file
@@ -0,0 +1,302 @@
|
||||
# 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 enum import Enum
|
||||
from typing import TYPE_CHECKING, Literal, Union, cast
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.Heading_pb2 import Heading as HeadingProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.string_util import clean_text
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.type_util import SupportsStr
|
||||
|
||||
|
||||
class HeadingProtoTag(Enum):
|
||||
TITLE_TAG = "h1"
|
||||
HEADER_TAG = "h2"
|
||||
SUBHEADER_TAG = "h3"
|
||||
|
||||
|
||||
Anchor: TypeAlias = Union[str, Literal[False], None]
|
||||
Divider: TypeAlias = Union[bool, str, None]
|
||||
|
||||
|
||||
class HeadingMixin:
|
||||
@gather_metrics("header")
|
||||
def header(
|
||||
self,
|
||||
body: SupportsStr,
|
||||
anchor: Anchor = None,
|
||||
*, # keyword-only arguments:
|
||||
help: str | None = None,
|
||||
divider: Divider = False,
|
||||
) -> DeltaGenerator:
|
||||
"""Display text in header formatting.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : str
|
||||
The text to display as GitHub-flavored Markdown. Syntax
|
||||
information can be found at: https://github.github.com/gfm.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
anchor : str or False
|
||||
The anchor name of the header that can be accessed with #anchor
|
||||
in the URL. If omitted, it generates an anchor using the body.
|
||||
If False, the anchor is not shown in the UI.
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed next to the header. 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``.
|
||||
|
||||
divider : bool or “blue”, “green”, “orange”, “red”, “violet”, “gray”/"grey", or “rainbow”
|
||||
Shows a colored divider below the header. If True, successive
|
||||
headers will cycle through divider colors. That is, the first
|
||||
header will have a blue line, the second header will have a
|
||||
green line, and so on. If a string, the color can be set to one of
|
||||
the following: blue, green, orange, red, violet, gray/grey, or
|
||||
rainbow.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.header("_Streamlit_ is :blue[cool] :sunglasses:")
|
||||
>>> st.header("This is a header with a divider", divider="gray")
|
||||
>>> st.header("These headers have rotating dividers", divider=True)
|
||||
>>> st.header("One", divider=True)
|
||||
>>> st.header("Two", divider=True)
|
||||
>>> st.header("Three", divider=True)
|
||||
>>> st.header("Four", divider=True)
|
||||
|
||||
.. output::
|
||||
https://doc-header.streamlit.app/
|
||||
height: 600px
|
||||
|
||||
"""
|
||||
return self.dg._enqueue(
|
||||
"heading",
|
||||
HeadingMixin._create_heading_proto(
|
||||
tag=HeadingProtoTag.HEADER_TAG,
|
||||
body=body,
|
||||
anchor=anchor,
|
||||
help=help,
|
||||
divider=divider,
|
||||
),
|
||||
)
|
||||
|
||||
@gather_metrics("subheader")
|
||||
def subheader(
|
||||
self,
|
||||
body: SupportsStr,
|
||||
anchor: Anchor = None,
|
||||
*, # keyword-only arguments:
|
||||
help: str | None = None,
|
||||
divider: Divider = False,
|
||||
) -> DeltaGenerator:
|
||||
"""Display text in subheader formatting.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : str
|
||||
The text to display as GitHub-flavored Markdown. Syntax
|
||||
information can be found at: https://github.github.com/gfm.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
anchor : str or False
|
||||
The anchor name of the header that can be accessed with #anchor
|
||||
in the URL. If omitted, it generates an anchor using the body.
|
||||
If False, the anchor is not shown in the UI.
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed next to the subheader. 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``.
|
||||
|
||||
divider : bool or “blue”, “green”, “orange”, “red”, “violet”, “gray”/"grey", or “rainbow”
|
||||
Shows a colored divider below the header. If True, successive
|
||||
headers will cycle through divider colors. That is, the first
|
||||
header will have a blue line, the second header will have a
|
||||
green line, and so on. If a string, the color can be set to one of
|
||||
the following: blue, green, orange, red, violet, gray/grey, or
|
||||
rainbow.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.subheader("_Streamlit_ is :blue[cool] :sunglasses:")
|
||||
>>> st.subheader("This is a subheader with a divider", divider="gray")
|
||||
>>> st.subheader("These subheaders have rotating dividers", divider=True)
|
||||
>>> st.subheader("One", divider=True)
|
||||
>>> st.subheader("Two", divider=True)
|
||||
>>> st.subheader("Three", divider=True)
|
||||
>>> st.subheader("Four", divider=True)
|
||||
|
||||
.. output::
|
||||
https://doc-subheader.streamlit.app/
|
||||
height: 500px
|
||||
|
||||
"""
|
||||
return self.dg._enqueue(
|
||||
"heading",
|
||||
HeadingMixin._create_heading_proto(
|
||||
tag=HeadingProtoTag.SUBHEADER_TAG,
|
||||
body=body,
|
||||
anchor=anchor,
|
||||
help=help,
|
||||
divider=divider,
|
||||
),
|
||||
)
|
||||
|
||||
@gather_metrics("title")
|
||||
def title(
|
||||
self,
|
||||
body: SupportsStr,
|
||||
anchor: Anchor = None,
|
||||
*, # keyword-only arguments:
|
||||
help: str | None = None,
|
||||
) -> DeltaGenerator:
|
||||
"""Display text in title formatting.
|
||||
|
||||
Each document should have a single `st.title()`, although this is not
|
||||
enforced.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : str
|
||||
The text to display as GitHub-flavored Markdown. Syntax
|
||||
information can be found at: https://github.github.com/gfm.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
anchor : str or False
|
||||
The anchor name of the header that can be accessed with #anchor
|
||||
in the URL. If omitted, it generates an anchor using the body.
|
||||
If False, the anchor is not shown in the UI.
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed next to the title. 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``.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.title("This is a title")
|
||||
>>> st.title("_Streamlit_ is :blue[cool] :sunglasses:")
|
||||
|
||||
.. output::
|
||||
https://doc-title.streamlit.app/
|
||||
height: 220px
|
||||
|
||||
"""
|
||||
return self.dg._enqueue(
|
||||
"heading",
|
||||
HeadingMixin._create_heading_proto(
|
||||
tag=HeadingProtoTag.TITLE_TAG, body=body, anchor=anchor, help=help
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
@staticmethod
|
||||
def _handle_divider_color(divider: Divider) -> str:
|
||||
if divider is True:
|
||||
return "auto"
|
||||
valid_colors = [
|
||||
"blue",
|
||||
"green",
|
||||
"orange",
|
||||
"red",
|
||||
"violet",
|
||||
"gray",
|
||||
"grey",
|
||||
"rainbow",
|
||||
]
|
||||
if divider in valid_colors:
|
||||
return cast("str", divider)
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
f"Divider parameter has invalid value: `{divider}`. Please choose from: {', '.join(valid_colors)}."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _create_heading_proto(
|
||||
tag: HeadingProtoTag,
|
||||
body: SupportsStr,
|
||||
anchor: Anchor = None,
|
||||
help: str | None = None,
|
||||
divider: Divider = False,
|
||||
) -> HeadingProto:
|
||||
proto = HeadingProto()
|
||||
proto.tag = tag.value
|
||||
proto.body = clean_text(body)
|
||||
if divider:
|
||||
proto.divider = HeadingMixin._handle_divider_color(divider)
|
||||
if anchor is not None:
|
||||
if anchor is False:
|
||||
proto.hide_anchor = True
|
||||
elif isinstance(anchor, str):
|
||||
proto.anchor = anchor
|
||||
elif anchor is True: # type: ignore
|
||||
raise StreamlitAPIException(
|
||||
"Anchor parameter has invalid value: %s. "
|
||||
"Supported values: None, any string or False" % anchor
|
||||
)
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
"Anchor parameter has invalid type: %s. "
|
||||
"Supported values: None, any string or False"
|
||||
% type(anchor).__name__
|
||||
)
|
||||
|
||||
if help:
|
||||
proto.help = help
|
||||
return proto
|
||||
105
myenv/lib/python3.11/site-packages/streamlit/elements/html.py
Normal file
105
myenv/lib/python3.11/site-packages/streamlit/elements/html.py
Normal file
@@ -0,0 +1,105 @@
|
||||
# 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 os
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from streamlit.proto.Html_pb2 import Html as HtmlProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.string_util import clean_text
|
||||
from streamlit.type_util import SupportsReprHtml, SupportsStr, has_callable_attr
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
class HtmlMixin:
|
||||
@gather_metrics("html")
|
||||
def html(
|
||||
self,
|
||||
body: str | Path | SupportsStr | SupportsReprHtml,
|
||||
) -> DeltaGenerator:
|
||||
"""Insert HTML into your app.
|
||||
|
||||
Adding custom HTML to your app impacts safety, styling, and
|
||||
maintainability. We sanitize HTML with `DOMPurify
|
||||
<https://github.com/cure53/DOMPurify>`_, but inserting HTML remains a
|
||||
developer risk. Passing untrusted code to ``st.html`` or dynamically
|
||||
loading external code can increase the risk of vulnerabilities in your
|
||||
app.
|
||||
|
||||
``st.html`` content is **not** iframed. Executing JavaScript is not
|
||||
supported at this time.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : any
|
||||
The HTML code to insert. This can be one of the following:
|
||||
|
||||
- A string of HTML code.
|
||||
- A path to a local file with HTML code. The path can be a ``str``
|
||||
or ``Path`` object. Paths can be absolute or relative to the
|
||||
working directory (where you execute ``streamlit run``).
|
||||
- Any object. If ``body`` is not a string or path, Streamlit will
|
||||
convert the object to a string. ``body._repr_html_()`` takes
|
||||
precedence over ``str(body)`` when available.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.html(
|
||||
... "<p><span style='text-decoration: line-through double red;'>Oops</span>!</p>"
|
||||
... )
|
||||
|
||||
.. output::
|
||||
https://doc-html.streamlit.app/
|
||||
height: 300px
|
||||
|
||||
"""
|
||||
html_proto = HtmlProto()
|
||||
|
||||
# If body supports _repr_html_, use that.
|
||||
if has_callable_attr(body, "_repr_html_"):
|
||||
html_proto.body = cast("SupportsReprHtml", body)._repr_html_()
|
||||
|
||||
# Check if the body is a file path. May include filesystem lookup.
|
||||
elif isinstance(body, Path) or _is_file(body):
|
||||
with open(cast("str", body), encoding="utf-8") as f:
|
||||
html_proto.body = f.read()
|
||||
|
||||
# OK, let's just try converting to string and hope for the best.
|
||||
else:
|
||||
html_proto.body = clean_text(cast("SupportsStr", body))
|
||||
|
||||
return self.dg._enqueue("html", html_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
def _is_file(obj: Any) -> bool:
|
||||
"""Checks if obj is a file, and doesn't throw if not.
|
||||
|
||||
The "not throwing" part is important!
|
||||
"""
|
||||
try:
|
||||
return os.path.isfile(obj)
|
||||
except TypeError:
|
||||
return False
|
||||
191
myenv/lib/python3.11/site-packages/streamlit/elements/iframe.py
Normal file
191
myenv/lib/python3.11/site-packages/streamlit/elements/iframe.py
Normal file
@@ -0,0 +1,191 @@
|
||||
# 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 typing import TYPE_CHECKING, cast
|
||||
|
||||
from streamlit.proto.IFrame_pb2 import IFrame as IFrameProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
class IframeMixin:
|
||||
@gather_metrics("_iframe")
|
||||
def _iframe(
|
||||
self,
|
||||
src: str,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
scrolling: bool = False,
|
||||
) -> DeltaGenerator:
|
||||
"""Load a remote URL in an iframe.
|
||||
|
||||
To use this function, import it from the ``streamlit.components.v1``
|
||||
module.
|
||||
|
||||
.. warning::
|
||||
Using ``st.components.v1.iframe`` directly (instead of importing
|
||||
its module) is deprecated and will be disallowed in a later version.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
src : str
|
||||
The URL of the page to embed.
|
||||
|
||||
width : int
|
||||
The width of the iframe in CSS pixels. By default, this is the
|
||||
app's default element width.
|
||||
|
||||
height : int
|
||||
The height of the frame in CSS pixels. By default, this is ``150``.
|
||||
|
||||
scrolling : bool
|
||||
Whether to allow scrolling in the iframe. If this ``False``
|
||||
(default), Streamlit crops any content larger than the iframe and
|
||||
does not show a scrollbar. If this is ``True``, Streamlit shows a
|
||||
scrollbar when the content is larger than the iframe.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
>>> import streamlit.components.v1 as components
|
||||
>>>
|
||||
>>> components.iframe("https://example.com", height=500)
|
||||
|
||||
"""
|
||||
iframe_proto = IFrameProto()
|
||||
marshall(
|
||||
iframe_proto,
|
||||
src=src,
|
||||
width=width,
|
||||
height=height,
|
||||
scrolling=scrolling,
|
||||
)
|
||||
return self.dg._enqueue("iframe", iframe_proto)
|
||||
|
||||
@gather_metrics("_html")
|
||||
def _html(
|
||||
self,
|
||||
html: str,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
scrolling: bool = False,
|
||||
) -> DeltaGenerator:
|
||||
"""Display an HTML string in an iframe.
|
||||
|
||||
To use this function, import it from the ``streamlit.components.v1``
|
||||
module.
|
||||
|
||||
If you want to insert HTML text into your app without an iframe, try
|
||||
``st.html`` instead.
|
||||
|
||||
.. warning::
|
||||
Using ``st.components.v1.html`` directly (instead of importing
|
||||
its module) is deprecated and will be disallowed in a later version.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
html : str
|
||||
The HTML string to embed in the iframe.
|
||||
|
||||
width : int
|
||||
The width of the iframe in CSS pixels. By default, this is the
|
||||
app's default element width.
|
||||
|
||||
height : int
|
||||
The height of the frame in CSS pixels. By default, this is ``150``.
|
||||
|
||||
scrolling : bool
|
||||
Whether to allow scrolling in the iframe. If this ``False``
|
||||
(default), Streamlit crops any content larger than the iframe and
|
||||
does not show a scrollbar. If this is ``True``, Streamlit shows a
|
||||
scrollbar when the content is larger than the iframe.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
>>> import streamlit.components.v1 as components
|
||||
>>>
|
||||
>>> components.html(
|
||||
>>> "<p><span style='text-decoration: line-through double red;'>Oops</span>!</p>"
|
||||
>>> )
|
||||
|
||||
"""
|
||||
iframe_proto = IFrameProto()
|
||||
marshall(
|
||||
iframe_proto,
|
||||
srcdoc=html,
|
||||
width=width,
|
||||
height=height,
|
||||
scrolling=scrolling,
|
||||
)
|
||||
return self.dg._enqueue("iframe", iframe_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
def marshall(
|
||||
proto: IFrameProto,
|
||||
src: str | None = None,
|
||||
srcdoc: str | None = None,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
scrolling: bool = False,
|
||||
) -> None:
|
||||
"""Marshalls data into an IFrame proto.
|
||||
|
||||
These parameters correspond directly to <iframe> attributes, which are
|
||||
described in more detail at
|
||||
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
proto : IFrame protobuf
|
||||
The protobuf object to marshall data into.
|
||||
src : str
|
||||
The URL of the page to embed.
|
||||
srcdoc : str
|
||||
Inline HTML to embed. Overrides src.
|
||||
width : int
|
||||
The width of the frame in CSS pixels. Defaults to the app's
|
||||
default element width.
|
||||
height : int
|
||||
The height of the frame in CSS pixels. Defaults to 150.
|
||||
scrolling : bool
|
||||
If true, show a scrollbar when the content is larger than the iframe.
|
||||
Otherwise, never show a scrollbar.
|
||||
|
||||
"""
|
||||
if src is not None:
|
||||
proto.src = src
|
||||
|
||||
if srcdoc is not None:
|
||||
proto.srcdoc = srcdoc
|
||||
|
||||
if width is not None:
|
||||
proto.width = width
|
||||
proto.has_width = True
|
||||
|
||||
if height is not None:
|
||||
proto.height = height
|
||||
else:
|
||||
proto.height = 150
|
||||
|
||||
proto.scrolling = scrolling
|
||||
196
myenv/lib/python3.11/site-packages/streamlit/elements/image.py
Normal file
196
myenv/lib/python3.11/site-packages/streamlit/elements/image.py
Normal file
@@ -0,0 +1,196 @@
|
||||
# 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.
|
||||
|
||||
# Some casts in this file are only occasionally necessary depending on the
|
||||
# user's Python version, and mypy doesn't have a good way of toggling this
|
||||
# specific config option at a per-line level.
|
||||
# mypy: no-warn-unused-ignores
|
||||
|
||||
"""Image marshalling."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Literal, Union, cast
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit.deprecation_util import show_deprecation_warning
|
||||
from streamlit.elements.lib.image_utils import (
|
||||
Channels,
|
||||
ImageFormatOrAuto,
|
||||
ImageOrImageList,
|
||||
WidthBehavior,
|
||||
marshall_images,
|
||||
)
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.Image_pb2 import ImageList as ImageListProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
UseColumnWith: TypeAlias = Union[Literal["auto", "always", "never"], bool, None]
|
||||
|
||||
|
||||
class ImageMixin:
|
||||
@gather_metrics("image")
|
||||
def image(
|
||||
self,
|
||||
image: ImageOrImageList,
|
||||
# TODO: Narrow type of caption, dependent on type of image,
|
||||
# by way of overload
|
||||
caption: str | list[str] | None = None,
|
||||
width: int | None = None,
|
||||
use_column_width: UseColumnWith = None,
|
||||
clamp: bool = False,
|
||||
channels: Channels = "RGB",
|
||||
output_format: ImageFormatOrAuto = "auto",
|
||||
*,
|
||||
use_container_width: bool = False,
|
||||
) -> DeltaGenerator:
|
||||
"""Display an image or list of images.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
image : numpy.ndarray, BytesIO, str, Path, or list of these
|
||||
The image to display. This can be one of the following:
|
||||
|
||||
- A URL (string) for a hosted image.
|
||||
- A path to a local image file. The path can be a ``str``
|
||||
or ``Path`` object. Paths can be absolute or relative to the
|
||||
working directory (where you execute ``streamlit run``).
|
||||
- An SVG string like ``<svg xmlns=...</svg>``.
|
||||
- A byte array defining an image. This includes monochrome images of
|
||||
shape (w,h) or (w,h,1), color images of shape (w,h,3), or RGBA
|
||||
images of shape (w,h,4), where w and h are the image width and
|
||||
height, respectively.
|
||||
- A list of any of the above. Streamlit displays the list as a
|
||||
row of images that overflow to additional rows as needed.
|
||||
caption : str or list of str
|
||||
Image caption(s). If this is ``None`` (default), no caption is
|
||||
displayed. If ``image`` is a list of multiple images, ``caption``
|
||||
must be a list of captions (one caption for each image) or
|
||||
``None``.
|
||||
|
||||
Captions can optionally contain GitHub-flavored Markdown. Syntax
|
||||
information can be found at: https://github.github.com/gfm.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
width : int or None
|
||||
Image width. If this is ``None`` (default), Streamlit will use the
|
||||
image's native width, up to the width of the parent container.
|
||||
When using an SVG image without a default width, you should declare
|
||||
``width`` or use ``use_container_width=True``.
|
||||
use_column_width : "auto", "always", "never", or bool
|
||||
If "auto", set the image's width to its natural size,
|
||||
but do not exceed the width of the column.
|
||||
If "always" or True, set the image's width to the column width.
|
||||
If "never" or False, set the image's width to its natural size.
|
||||
Note: if set, `use_column_width` takes precedence over the `width` parameter.
|
||||
clamp : bool
|
||||
Whether to clamp image pixel values to a valid range (0-255 per
|
||||
channel). This is only used for byte array images; the parameter is
|
||||
ignored for image URLs and files. If this is ``False`` (default)
|
||||
and an image has an out-of-range value, a ``RuntimeError`` will be
|
||||
raised.
|
||||
channels : "RGB" or "BGR"
|
||||
The color format when ``image`` is an ``nd.array``. This is ignored
|
||||
for other image types. If this is ``"RGB"`` (default),
|
||||
``image[:, :, 0]`` is the red channel, ``image[:, :, 1]`` is the
|
||||
green channel, and ``image[:, :, 2]`` is the blue channel. For
|
||||
images coming from libraries like OpenCV, you should set this to
|
||||
``"BGR"`` instead.
|
||||
output_format : "JPEG", "PNG", or "auto"
|
||||
The output format to use when transferring the image data. If this
|
||||
is ``"auto"`` (default), Streamlit identifies the compression type
|
||||
based on the type and format of the image. Photos should use the
|
||||
``"JPEG"`` format for lossy compression while diagrams should use
|
||||
the ``"PNG"`` format for lossless compression.
|
||||
|
||||
use_container_width : bool
|
||||
Whether to override ``width`` with the width of the parent
|
||||
container. If ``use_container_width`` is ``False`` (default),
|
||||
Streamlit sets the image's width according to ``width``. If
|
||||
``use_container_width`` is ``True``, Streamlit sets the width of
|
||||
the image to match the width of the parent container.
|
||||
|
||||
.. deprecated::
|
||||
``use_column_width`` is deprecated and will be removed in a future
|
||||
release. Please use the ``use_container_width`` parameter instead.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>> st.image("sunrise.jpg", caption="Sunrise by the mountains")
|
||||
|
||||
.. output::
|
||||
https://doc-image.streamlit.app/
|
||||
height: 710px
|
||||
|
||||
"""
|
||||
|
||||
if use_container_width is True and use_column_width is not None:
|
||||
raise StreamlitAPIException(
|
||||
"`use_container_width` and `use_column_width` cannot be set at the same time.",
|
||||
"Please utilize `use_container_width` since `use_column_width` is deprecated.",
|
||||
)
|
||||
|
||||
image_width: int = (
|
||||
WidthBehavior.ORIGINAL if (width is None or width <= 0) else width
|
||||
)
|
||||
|
||||
if use_column_width is not None:
|
||||
show_deprecation_warning(
|
||||
"The `use_column_width` parameter has been deprecated and will be removed "
|
||||
"in a future release. Please utilize the `use_container_width` parameter instead."
|
||||
)
|
||||
|
||||
if use_column_width == "auto":
|
||||
image_width = WidthBehavior.AUTO
|
||||
elif use_column_width == "always" or use_column_width is True:
|
||||
image_width = WidthBehavior.COLUMN
|
||||
elif use_column_width == "never" or use_column_width is False:
|
||||
image_width = WidthBehavior.ORIGINAL
|
||||
|
||||
else:
|
||||
if use_container_width is True:
|
||||
image_width = WidthBehavior.MAX_IMAGE_OR_CONTAINER
|
||||
elif image_width is not None and image_width > 0:
|
||||
# Use the given width. It will be capped on the frontend if it
|
||||
# exceeds the container width.
|
||||
pass
|
||||
elif use_container_width is False:
|
||||
image_width = WidthBehavior.MIN_IMAGE_OR_CONTAINER
|
||||
|
||||
image_list_proto = ImageListProto()
|
||||
marshall_images(
|
||||
self.dg._get_delta_path_str(),
|
||||
image,
|
||||
caption,
|
||||
image_width,
|
||||
image_list_proto,
|
||||
clamp,
|
||||
channels,
|
||||
output_format,
|
||||
)
|
||||
return self.dg._enqueue("imgs", image_list_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
139
myenv/lib/python3.11/site-packages/streamlit/elements/json.py
Normal file
139
myenv/lib/python3.11/site-packages/streamlit/elements/json.py
Normal file
@@ -0,0 +1,139 @@
|
||||
# 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
|
||||
import types
|
||||
from collections import ChainMap, UserDict
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from streamlit.proto.Json_pb2 import Json as JsonProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.type_util import (
|
||||
is_custom_dict,
|
||||
is_list_like,
|
||||
is_namedtuple,
|
||||
is_pydantic_model,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
def _ensure_serialization(o: object) -> str | list[Any]:
|
||||
"""A repr function for json.dumps default arg, which tries to serialize sets
|
||||
as lists.
|
||||
"""
|
||||
return list(o) if isinstance(o, set) else repr(o)
|
||||
|
||||
|
||||
class JsonMixin:
|
||||
@gather_metrics("json")
|
||||
def json(
|
||||
self,
|
||||
body: object,
|
||||
*, # keyword-only arguments:
|
||||
expanded: bool | int = True,
|
||||
) -> DeltaGenerator:
|
||||
"""Display an object or string as a pretty-printed, interactive JSON string.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : object or str
|
||||
The object to print as JSON. All referenced objects should be
|
||||
serializable to JSON as well. If object is a string, we assume it
|
||||
contains serialized JSON.
|
||||
|
||||
expanded : bool or int
|
||||
The initial expansion state of the JSON element. This can be one
|
||||
of the following:
|
||||
|
||||
- ``True`` (default): The element is fully expanded.
|
||||
- ``False``: The element is fully collapsed.
|
||||
- An integer: The element is expanded to the depth specified. The
|
||||
integer must be non-negative. ``expanded=0`` is equivalent to
|
||||
``expanded=False``.
|
||||
|
||||
Regardless of the initial expansion state, users can collapse or
|
||||
expand any key-value pair to show or hide any part of the object.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.json(
|
||||
... {
|
||||
... "foo": "bar",
|
||||
... "stuff": [
|
||||
... "stuff 1",
|
||||
... "stuff 2",
|
||||
... "stuff 3",
|
||||
... ],
|
||||
... "level1": {"level2": {"level3": {"a": "b"}}},
|
||||
... },
|
||||
... expanded=2,
|
||||
... )
|
||||
|
||||
.. output::
|
||||
https://doc-json.streamlit.app/
|
||||
height: 385px
|
||||
|
||||
"""
|
||||
|
||||
if is_custom_dict(body):
|
||||
body = body.to_dict()
|
||||
|
||||
if is_namedtuple(body):
|
||||
body = body._asdict()
|
||||
|
||||
if isinstance(
|
||||
body, (ChainMap, types.MappingProxyType, UserDict)
|
||||
) or is_pydantic_model(body):
|
||||
body = dict(body) # type: ignore
|
||||
|
||||
if is_list_like(body):
|
||||
body = list(body)
|
||||
|
||||
if not isinstance(body, str):
|
||||
try:
|
||||
# Serialize body to string and try to interpret sets as lists
|
||||
body = json.dumps(body, default=_ensure_serialization)
|
||||
except TypeError as err:
|
||||
self.dg.warning(
|
||||
"Warning: this data structure was not fully serializable as "
|
||||
f"JSON due to one or more unexpected keys. (Error was: {err})"
|
||||
)
|
||||
body = json.dumps(body, skipkeys=True, default=_ensure_serialization)
|
||||
|
||||
json_proto = JsonProto()
|
||||
json_proto.body = body
|
||||
|
||||
if isinstance(expanded, bool):
|
||||
json_proto.expanded = expanded
|
||||
elif isinstance(expanded, int):
|
||||
json_proto.expanded = True
|
||||
json_proto.max_expand_depth = expanded
|
||||
else:
|
||||
raise TypeError(
|
||||
f"The type {str(type(expanded))} of `expanded` is not supported"
|
||||
", must be bool or int."
|
||||
)
|
||||
|
||||
return self.dg._enqueue("json", json_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
874
myenv/lib/python3.11/site-packages/streamlit/elements/layouts.py
Normal file
874
myenv/lib/python3.11/site-packages/streamlit/elements/layouts.py
Normal file
@@ -0,0 +1,874 @@
|
||||
# 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 typing import TYPE_CHECKING, Literal, Union, cast
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit.delta_generator_singletons import get_dg_singleton_instance
|
||||
from streamlit.elements.lib.utils import Key, compute_and_register_element_id, to_key
|
||||
from streamlit.errors import (
|
||||
StreamlitAPIException,
|
||||
StreamlitInvalidColumnGapError,
|
||||
StreamlitInvalidColumnSpecError,
|
||||
StreamlitInvalidVerticalAlignmentError,
|
||||
)
|
||||
from streamlit.proto.Block_pb2 import Block as BlockProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.string_util import validate_icon_or_emoji
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.elements.lib.dialog import Dialog
|
||||
from streamlit.elements.lib.mutable_status_container import StatusContainer
|
||||
|
||||
SpecType: TypeAlias = Union[int, Sequence[Union[int, float]]]
|
||||
|
||||
|
||||
class LayoutsMixin:
|
||||
@gather_metrics("container")
|
||||
def container(
|
||||
self,
|
||||
*,
|
||||
height: int | None = None,
|
||||
border: bool | None = None,
|
||||
key: Key | None = None,
|
||||
) -> DeltaGenerator:
|
||||
"""Insert a multi-element container.
|
||||
|
||||
Inserts an invisible container into your app that can be used to hold
|
||||
multiple elements. This allows you to, for example, insert multiple
|
||||
elements into your app out of order.
|
||||
|
||||
To add elements to the returned container, you can use the ``with`` notation
|
||||
(preferred) or just call methods directly on the returned object. See
|
||||
examples below.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
height : int or None
|
||||
Desired height of the container expressed in pixels. If ``None`` (default)
|
||||
the container grows to fit its content. If a fixed height, scrolling is
|
||||
enabled for large content and a grey border is shown around the container
|
||||
to visually separate its scroll surface from the rest of the app.
|
||||
|
||||
.. note::
|
||||
Use containers with scroll sparingly. If you do, try to keep
|
||||
the height small (below 500 pixels). Otherwise, the scroll
|
||||
surface of the container might cover the majority of the screen
|
||||
on mobile devices, which makes it hard to scroll the rest of the app.
|
||||
|
||||
border : bool or None
|
||||
Whether to show a border around the container. If ``None`` (default), a
|
||||
border is shown if the container is set to a fixed height and not
|
||||
shown otherwise.
|
||||
|
||||
key : str or None
|
||||
An optional string to give this container a stable identity.
|
||||
|
||||
Additionally, if ``key`` is provided, it will be used as CSS
|
||||
class name prefixed with ``st-key-``.
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
Inserting elements using ``with`` notation:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> with st.container():
|
||||
... st.write("This is inside the container")
|
||||
...
|
||||
... # You can call any Streamlit command, including custom components:
|
||||
... st.bar_chart(np.random.randn(50, 3))
|
||||
>>>
|
||||
>>> st.write("This is outside the container")
|
||||
|
||||
.. output ::
|
||||
https://doc-container1.streamlit.app/
|
||||
height: 520px
|
||||
|
||||
Inserting elements out of order:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> container = st.container(border=True)
|
||||
>>> container.write("This is inside the container")
|
||||
>>> st.write("This is outside the container")
|
||||
>>>
|
||||
>>> # Now insert some more in the container
|
||||
>>> container.write("This is inside too")
|
||||
|
||||
.. output ::
|
||||
https://doc-container2.streamlit.app/
|
||||
height: 300px
|
||||
|
||||
Using ``height`` to make a grid:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> row1 = st.columns(3)
|
||||
>>> row2 = st.columns(3)
|
||||
>>>
|
||||
>>> for col in row1 + row2:
|
||||
>>> tile = col.container(height=120)
|
||||
>>> tile.title(":balloon:")
|
||||
|
||||
.. output ::
|
||||
https://doc-container3.streamlit.app/
|
||||
height: 350px
|
||||
|
||||
Using ``height`` to create a scrolling container for long content:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> long_text = "Lorem ipsum. " * 1000
|
||||
>>>
|
||||
>>> with st.container(height=300):
|
||||
>>> st.markdown(long_text)
|
||||
|
||||
.. output ::
|
||||
https://doc-container4.streamlit.app/
|
||||
height: 400px
|
||||
|
||||
"""
|
||||
key = to_key(key)
|
||||
block_proto = BlockProto()
|
||||
block_proto.allow_empty = False
|
||||
block_proto.vertical.border = border or False
|
||||
|
||||
if height:
|
||||
# Activate scrolling container behavior:
|
||||
block_proto.allow_empty = True
|
||||
block_proto.vertical.height = height
|
||||
if border is None:
|
||||
# If border is None, we activated the
|
||||
# border as default setting for scrolling
|
||||
# containers.
|
||||
block_proto.vertical.border = True
|
||||
|
||||
if key:
|
||||
# At the moment, the ID is only used for extracting the
|
||||
# key on the frontend and setting it as CSS class.
|
||||
# There are plans to use the ID for other container features
|
||||
# in the future. This might require including more container
|
||||
# parameters in the ID calculation.
|
||||
block_proto.id = compute_and_register_element_id(
|
||||
"container", user_key=key, form_id=None
|
||||
)
|
||||
|
||||
return self.dg._block(block_proto)
|
||||
|
||||
@gather_metrics("columns")
|
||||
def columns(
|
||||
self,
|
||||
spec: SpecType,
|
||||
*,
|
||||
gap: Literal["small", "medium", "large"] = "small",
|
||||
vertical_alignment: Literal["top", "center", "bottom"] = "top",
|
||||
border: bool = False,
|
||||
) -> list[DeltaGenerator]:
|
||||
"""Insert containers laid out as side-by-side columns.
|
||||
|
||||
Inserts a number of multi-element containers laid out side-by-side and
|
||||
returns a list of container objects.
|
||||
|
||||
To add elements to the returned containers, you can use the ``with`` notation
|
||||
(preferred) or just call methods directly on the returned object. See
|
||||
examples below.
|
||||
|
||||
Columns can only be placed inside other columns up to one level of nesting.
|
||||
|
||||
.. warning::
|
||||
Columns cannot be placed inside other columns in the sidebar. This
|
||||
is only possible in the main area of the app.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
spec : int or Iterable of numbers
|
||||
Controls the number and width of columns to insert. Can be one of:
|
||||
|
||||
- An integer that specifies the number of columns. All columns have equal
|
||||
width in this case.
|
||||
- An Iterable of numbers (int or float) that specify the relative width of
|
||||
each column. E.g. ``[0.7, 0.3]`` creates two columns where the first
|
||||
one takes up 70% of the available with and the second one takes up 30%.
|
||||
Or ``[1, 2, 3]`` creates three columns where the second one is two times
|
||||
the width of the first one, and the third one is three times that width.
|
||||
|
||||
gap : "small", "medium", or "large"
|
||||
The size of the gap between the columns. The default is ``"small"``.
|
||||
|
||||
vertical_alignment : "top", "center", or "bottom"
|
||||
The vertical alignment of the content inside the columns. The
|
||||
default is ``"top"``.
|
||||
|
||||
border : bool
|
||||
Whether to show a border around the column containers. If this is
|
||||
``False`` (default), no border is shown. If this is ``True``, a
|
||||
border is shown around each column.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list of containers
|
||||
A list of container objects.
|
||||
|
||||
Examples
|
||||
--------
|
||||
**Example 1: Use context management**
|
||||
|
||||
You can use the ``with`` statement to insert any element into a column:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> col1, col2, col3 = st.columns(3)
|
||||
>>>
|
||||
>>> with col1:
|
||||
... st.header("A cat")
|
||||
... st.image("https://static.streamlit.io/examples/cat.jpg")
|
||||
>>>
|
||||
>>> with col2:
|
||||
... st.header("A dog")
|
||||
... st.image("https://static.streamlit.io/examples/dog.jpg")
|
||||
>>>
|
||||
>>> with col3:
|
||||
... st.header("An owl")
|
||||
... st.image("https://static.streamlit.io/examples/owl.jpg")
|
||||
|
||||
.. output ::
|
||||
https://doc-columns1.streamlit.app/
|
||||
height: 620px
|
||||
|
||||
|
||||
**Example 2: Use commands as container methods**
|
||||
|
||||
You can just call methods directly on the returned objects:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> col1, col2 = st.columns([3, 1])
|
||||
>>> data = np.random.randn(10, 1)
|
||||
>>>
|
||||
>>> col1.subheader("A wide column with a chart")
|
||||
>>> col1.line_chart(data)
|
||||
>>>
|
||||
>>> col2.subheader("A narrow column with the data")
|
||||
>>> col2.write(data)
|
||||
|
||||
.. output ::
|
||||
https://doc-columns2.streamlit.app/
|
||||
height: 550px
|
||||
|
||||
**Example 3: Align widgets**
|
||||
|
||||
Use ``vertical_alignment="bottom"`` to align widgets.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> left, middle, right = st.columns(3, vertical_alignment="bottom")
|
||||
>>>
|
||||
>>> left.text_input("Write something")
|
||||
>>> middle.button("Click me", use_container_width=True)
|
||||
>>> right.checkbox("Check me")
|
||||
|
||||
.. output ::
|
||||
https://doc-columns-bottom-widgets.streamlit.app/
|
||||
height: 200px
|
||||
|
||||
**Example 4: Use vertical alignment to create grids**
|
||||
|
||||
Adjust vertical alignment to customize your grid layouts.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> vertical_alignment = st.selectbox(
|
||||
>>> "Vertical alignment", ["top", "center", "bottom"], index=2
|
||||
>>> )
|
||||
>>>
|
||||
>>> left, middle, right = st.columns(3, vertical_alignment=vertical_alignment)
|
||||
>>> left.image("https://static.streamlit.io/examples/cat.jpg")
|
||||
>>> middle.image("https://static.streamlit.io/examples/dog.jpg")
|
||||
>>> right.image("https://static.streamlit.io/examples/owl.jpg")
|
||||
|
||||
.. output ::
|
||||
https://doc-columns-vertical-alignment.streamlit.app/
|
||||
height: 600px
|
||||
|
||||
**Example 5: Add borders**
|
||||
|
||||
Add borders to your columns instead of nested containers for consistent
|
||||
heights.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> left, middle, right = st.columns(3, border=True)
|
||||
>>>
|
||||
>>> left.markdown("Lorem ipsum " * 10)
|
||||
>>> middle.markdown("Lorem ipsum " * 5)
|
||||
>>> right.markdown("Lorem ipsum ")
|
||||
|
||||
.. output ::
|
||||
https://doc-columns-borders.streamlit.app/
|
||||
height: 250px
|
||||
|
||||
"""
|
||||
weights = spec
|
||||
if isinstance(weights, int):
|
||||
# If the user provided a single number, expand into equal weights.
|
||||
# E.g. (1,) * 3 => (1, 1, 1)
|
||||
# NOTE: A negative/zero spec will expand into an empty tuple.
|
||||
weights = (1,) * weights
|
||||
|
||||
if len(weights) == 0 or any(weight <= 0 for weight in weights):
|
||||
raise StreamlitInvalidColumnSpecError()
|
||||
|
||||
vertical_alignment_mapping: dict[
|
||||
str, BlockProto.Column.VerticalAlignment.ValueType
|
||||
] = {
|
||||
"top": BlockProto.Column.VerticalAlignment.TOP,
|
||||
"center": BlockProto.Column.VerticalAlignment.CENTER,
|
||||
"bottom": BlockProto.Column.VerticalAlignment.BOTTOM,
|
||||
}
|
||||
|
||||
if vertical_alignment not in vertical_alignment_mapping:
|
||||
raise StreamlitInvalidVerticalAlignmentError(
|
||||
vertical_alignment=vertical_alignment
|
||||
)
|
||||
|
||||
def column_gap(gap):
|
||||
if isinstance(gap, str):
|
||||
gap_size = gap.lower()
|
||||
valid_sizes = ["small", "medium", "large"]
|
||||
|
||||
if gap_size in valid_sizes:
|
||||
return gap_size
|
||||
|
||||
raise StreamlitInvalidColumnGapError(gap=gap)
|
||||
|
||||
gap_size = column_gap(gap)
|
||||
|
||||
def column_proto(normalized_weight: float) -> BlockProto:
|
||||
col_proto = BlockProto()
|
||||
col_proto.column.weight = normalized_weight
|
||||
col_proto.column.gap = gap_size
|
||||
col_proto.column.vertical_alignment = vertical_alignment_mapping[
|
||||
vertical_alignment
|
||||
]
|
||||
col_proto.column.show_border = border
|
||||
col_proto.allow_empty = True
|
||||
return col_proto
|
||||
|
||||
block_proto = BlockProto()
|
||||
block_proto.horizontal.gap = gap_size
|
||||
row = self.dg._block(block_proto)
|
||||
total_weight = sum(weights)
|
||||
return [row._block(column_proto(w / total_weight)) for w in weights]
|
||||
|
||||
@gather_metrics("tabs")
|
||||
def tabs(self, tabs: Sequence[str]) -> Sequence[DeltaGenerator]:
|
||||
r"""Insert containers separated into tabs.
|
||||
|
||||
Inserts a number of multi-element containers as tabs.
|
||||
Tabs are a navigational element that allows users to easily
|
||||
move between groups of related content.
|
||||
|
||||
To add elements to the returned containers, you can use the ``with`` notation
|
||||
(preferred) or just call methods directly on the returned object. See
|
||||
examples below.
|
||||
|
||||
.. warning::
|
||||
All the content of every tab is always sent to and rendered on the frontend.
|
||||
Conditional rendering is currently not supported.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tabs : list of str
|
||||
Creates a tab for each string in the list. The first tab is selected
|
||||
by default. The string is used as the name of the tab and 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.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
Returns
|
||||
-------
|
||||
list of containers
|
||||
A list of container objects.
|
||||
|
||||
Examples
|
||||
--------
|
||||
You can use the ``with`` notation to insert any element into a tab:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> tab1, tab2, tab3 = st.tabs(["Cat", "Dog", "Owl"])
|
||||
>>>
|
||||
>>> with tab1:
|
||||
... st.header("A cat")
|
||||
... st.image("https://static.streamlit.io/examples/cat.jpg", width=200)
|
||||
>>> with tab2:
|
||||
... st.header("A dog")
|
||||
... st.image("https://static.streamlit.io/examples/dog.jpg", width=200)
|
||||
>>> with tab3:
|
||||
... st.header("An owl")
|
||||
... st.image("https://static.streamlit.io/examples/owl.jpg", width=200)
|
||||
|
||||
.. output ::
|
||||
https://doc-tabs1.streamlit.app/
|
||||
height: 620px
|
||||
|
||||
Or you can just call methods directly on the returned objects:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> tab1, tab2 = st.tabs(["📈 Chart", "🗃 Data"])
|
||||
>>> data = np.random.randn(10, 1)
|
||||
>>>
|
||||
>>> tab1.subheader("A tab with a chart")
|
||||
>>> tab1.line_chart(data)
|
||||
>>>
|
||||
>>> tab2.subheader("A tab with the data")
|
||||
>>> tab2.write(data)
|
||||
|
||||
|
||||
.. output ::
|
||||
https://doc-tabs2.streamlit.app/
|
||||
height: 700px
|
||||
|
||||
"""
|
||||
if not tabs:
|
||||
raise StreamlitAPIException(
|
||||
"The input argument to st.tabs must contain at least one tab label."
|
||||
)
|
||||
|
||||
if any(not isinstance(tab, str) for tab in tabs):
|
||||
raise StreamlitAPIException(
|
||||
"The tabs input list to st.tabs is only allowed to contain strings."
|
||||
)
|
||||
|
||||
def tab_proto(label: str) -> BlockProto:
|
||||
tab_proto = BlockProto()
|
||||
tab_proto.tab.label = label
|
||||
tab_proto.allow_empty = True
|
||||
return tab_proto
|
||||
|
||||
block_proto = BlockProto()
|
||||
block_proto.tab_container.SetInParent()
|
||||
tab_container = self.dg._block(block_proto)
|
||||
return tuple(tab_container._block(tab_proto(tab_label)) for tab_label in tabs)
|
||||
|
||||
@gather_metrics("expander")
|
||||
def expander(
|
||||
self,
|
||||
label: str,
|
||||
expanded: bool = False,
|
||||
*,
|
||||
icon: str | None = None,
|
||||
) -> DeltaGenerator:
|
||||
r"""Insert a multi-element container that can be expanded/collapsed.
|
||||
|
||||
Inserts a container into your app that can be used to hold multiple elements
|
||||
and can be expanded or collapsed by the user. When collapsed, all that is
|
||||
visible is the provided label.
|
||||
|
||||
To add elements to the returned container, you can use the ``with`` notation
|
||||
(preferred) or just call methods directly on the returned object. See
|
||||
examples below.
|
||||
|
||||
.. warning::
|
||||
Currently, you may not put expanders inside another expander.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : str
|
||||
A string to use as the header for the expander. 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.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
expanded : bool
|
||||
If True, initializes the expander in "expanded" state. Defaults to
|
||||
False (collapsed).
|
||||
|
||||
icon : str, None
|
||||
An optional emoji or icon to display next to the expander label. If ``icon``
|
||||
is ``None`` (default), no icon is displayed. If ``icon`` is a
|
||||
string, the following options are valid:
|
||||
|
||||
- A single-character emoji. For example, you can set ``icon="🚨"``
|
||||
or ``icon="🔥"``. 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.
|
||||
|
||||
Examples
|
||||
--------
|
||||
You can use the ``with`` notation to insert any element into an expander
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.bar_chart({"data": [1, 5, 2, 6, 2, 1]})
|
||||
>>>
|
||||
>>> with st.expander("See explanation"):
|
||||
... st.write('''
|
||||
... The chart above shows some numbers I picked for you.
|
||||
... I rolled actual dice for these, so they're *guaranteed* to
|
||||
... be random.
|
||||
... ''')
|
||||
... st.image("https://static.streamlit.io/examples/dice.jpg")
|
||||
|
||||
.. output ::
|
||||
https://doc-expander.streamlit.app/
|
||||
height: 750px
|
||||
|
||||
Or you can just call methods directly on the returned objects:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.bar_chart({"data": [1, 5, 2, 6, 2, 1]})
|
||||
>>>
|
||||
>>> expander = st.expander("See explanation")
|
||||
>>> expander.write('''
|
||||
... The chart above shows some numbers I picked for you.
|
||||
... I rolled actual dice for these, so they're *guaranteed* to
|
||||
... be random.
|
||||
... ''')
|
||||
>>> expander.image("https://static.streamlit.io/examples/dice.jpg")
|
||||
|
||||
.. output ::
|
||||
https://doc-expander.streamlit.app/
|
||||
height: 750px
|
||||
|
||||
"""
|
||||
if label is None:
|
||||
raise StreamlitAPIException("A label is required for an expander")
|
||||
|
||||
expandable_proto = BlockProto.Expandable()
|
||||
expandable_proto.expanded = expanded
|
||||
expandable_proto.label = label
|
||||
if icon is not None:
|
||||
expandable_proto.icon = validate_icon_or_emoji(icon)
|
||||
|
||||
block_proto = BlockProto()
|
||||
block_proto.allow_empty = False
|
||||
block_proto.expandable.CopyFrom(expandable_proto)
|
||||
|
||||
return self.dg._block(block_proto=block_proto)
|
||||
|
||||
@gather_metrics("popover")
|
||||
def popover(
|
||||
self,
|
||||
label: str,
|
||||
*,
|
||||
help: str | None = None,
|
||||
icon: str | None = None,
|
||||
disabled: bool = False,
|
||||
use_container_width: bool = False,
|
||||
) -> DeltaGenerator:
|
||||
r"""Insert a popover container.
|
||||
|
||||
Inserts a multi-element container as a popover. It consists of a button-like
|
||||
element and a container that opens when the button is clicked.
|
||||
|
||||
Opening and closing the popover will not trigger a rerun. Interacting
|
||||
with widgets inside of an open popover will rerun the app while keeping
|
||||
the popover open. Clicking outside of the popover will close it.
|
||||
|
||||
To add elements to the returned container, you can use the "with"
|
||||
notation (preferred) or just call methods directly on the returned object.
|
||||
See examples below.
|
||||
|
||||
.. warning::
|
||||
You may not put a popover inside another popover.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : str
|
||||
The label of the button that opens the popover container.
|
||||
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.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed when the popover button is hovered
|
||||
over. 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``.
|
||||
|
||||
icon : str
|
||||
An optional emoji or icon to display next to the button label. If ``icon``
|
||||
is ``None`` (default), no icon is displayed. If ``icon`` is a
|
||||
string, the following options are valid:
|
||||
|
||||
- A single-character emoji. For example, you can set ``icon="🚨"``
|
||||
or ``icon="🔥"``. 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.
|
||||
|
||||
disabled : bool
|
||||
An optional boolean that disables the popover button if set to
|
||||
``True``. The default is ``False``.
|
||||
|
||||
use_container_width : bool
|
||||
Whether to expand the button's width to fill its parent container.
|
||||
If ``use_container_width`` is ``False`` (default), Streamlit sizes
|
||||
the button to fit its contents. If ``use_container_width`` is
|
||||
``True``, the width of the button matches its parent container.
|
||||
|
||||
In both cases, if the contents of the button are wider than the
|
||||
parent container, the contents will line wrap.
|
||||
|
||||
The popover containter's minimimun width matches the width of its
|
||||
button. The popover container may be wider than its button to fit
|
||||
the container's contents.
|
||||
|
||||
Examples
|
||||
--------
|
||||
You can use the ``with`` notation to insert any element into a popover:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> with st.popover("Open popover"):
|
||||
>>> st.markdown("Hello World 👋")
|
||||
>>> name = st.text_input("What's your name?")
|
||||
>>>
|
||||
>>> st.write("Your name:", name)
|
||||
|
||||
.. output ::
|
||||
https://doc-popover.streamlit.app/
|
||||
height: 400px
|
||||
|
||||
Or you can just call methods directly on the returned objects:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> popover = st.popover("Filter items")
|
||||
>>> red = popover.checkbox("Show red items.", True)
|
||||
>>> blue = popover.checkbox("Show blue items.", True)
|
||||
>>>
|
||||
>>> if red:
|
||||
... st.write(":red[This is a red item.]")
|
||||
>>> if blue:
|
||||
... st.write(":blue[This is a blue item.]")
|
||||
|
||||
.. output ::
|
||||
https://doc-popover2.streamlit.app/
|
||||
height: 400px
|
||||
|
||||
"""
|
||||
if label is None:
|
||||
raise StreamlitAPIException("A label is required for a popover")
|
||||
|
||||
popover_proto = BlockProto.Popover()
|
||||
popover_proto.label = label
|
||||
popover_proto.use_container_width = use_container_width
|
||||
popover_proto.disabled = disabled
|
||||
if help:
|
||||
popover_proto.help = str(help)
|
||||
if icon is not None:
|
||||
popover_proto.icon = validate_icon_or_emoji(icon)
|
||||
|
||||
block_proto = BlockProto()
|
||||
block_proto.allow_empty = True
|
||||
block_proto.popover.CopyFrom(popover_proto)
|
||||
|
||||
return self.dg._block(block_proto=block_proto)
|
||||
|
||||
@gather_metrics("status")
|
||||
def status(
|
||||
self,
|
||||
label: str,
|
||||
*,
|
||||
expanded: bool = False,
|
||||
state: Literal["running", "complete", "error"] = "running",
|
||||
) -> StatusContainer:
|
||||
r"""Insert a status container to display output from long-running tasks.
|
||||
|
||||
Inserts a container into your app that is typically used to show the status and
|
||||
details of a process or task. The container can hold multiple elements and can
|
||||
be expanded or collapsed by the user similar to ``st.expander``.
|
||||
When collapsed, all that is visible is the status icon and label.
|
||||
|
||||
The label, state, and expanded state can all be updated by calling ``.update()``
|
||||
on the returned object. To add elements to the returned container, you can
|
||||
use ``with`` notation (preferred) or just call methods directly on the returned
|
||||
object.
|
||||
|
||||
By default, ``st.status()`` initializes in the "running" state. When called using
|
||||
``with`` notation, it automatically updates to the "complete" state at the end
|
||||
of the "with" block. See examples below for more details.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : str
|
||||
The initial label of the status container. 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.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
expanded : bool
|
||||
If True, initializes the status container in "expanded" state. Defaults to
|
||||
False (collapsed).
|
||||
|
||||
state : "running", "complete", or "error"
|
||||
The initial state of the status container which determines which icon is
|
||||
shown:
|
||||
|
||||
- ``running`` (default): A spinner icon is shown.
|
||||
|
||||
- ``complete``: A checkmark icon is shown.
|
||||
|
||||
- ``error``: An error icon is shown.
|
||||
|
||||
Returns
|
||||
-------
|
||||
StatusContainer
|
||||
A mutable status container that can hold multiple elements. The label, state,
|
||||
and expanded state can be updated after creation via ``.update()``.
|
||||
|
||||
Examples
|
||||
--------
|
||||
You can use the ``with`` notation to insert any element into an status container:
|
||||
|
||||
>>> import time
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> with st.status("Downloading data..."):
|
||||
... st.write("Searching for data...")
|
||||
... time.sleep(2)
|
||||
... st.write("Found URL.")
|
||||
... time.sleep(1)
|
||||
... st.write("Downloading data...")
|
||||
... time.sleep(1)
|
||||
>>>
|
||||
>>> st.button("Rerun")
|
||||
|
||||
.. output ::
|
||||
https://doc-status.streamlit.app/
|
||||
height: 300px
|
||||
|
||||
You can also use ``.update()`` on the container to change the label, state,
|
||||
or expanded state:
|
||||
|
||||
>>> import time
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> with st.status("Downloading data...", expanded=True) as status:
|
||||
... st.write("Searching for data...")
|
||||
... time.sleep(2)
|
||||
... st.write("Found URL.")
|
||||
... time.sleep(1)
|
||||
... st.write("Downloading data...")
|
||||
... time.sleep(1)
|
||||
... status.update(
|
||||
... label="Download complete!", state="complete", expanded=False
|
||||
... )
|
||||
>>>
|
||||
>>> st.button("Rerun")
|
||||
|
||||
.. output ::
|
||||
https://doc-status-update.streamlit.app/
|
||||
height: 300px
|
||||
|
||||
"""
|
||||
return get_dg_singleton_instance().status_container_cls._create(
|
||||
self.dg, label, expanded=expanded, state=state
|
||||
)
|
||||
|
||||
def _dialog(
|
||||
self,
|
||||
title: str,
|
||||
*,
|
||||
dismissible: bool = True,
|
||||
width: Literal["small", "large"] = "small",
|
||||
) -> Dialog:
|
||||
"""Inserts the dialog container.
|
||||
|
||||
Marked as internal because it is used by the dialog_decorator and is not supposed to be used directly.
|
||||
The dialog_decorator also has a more descriptive docstring since it is user-facing.
|
||||
"""
|
||||
return get_dg_singleton_instance().dialog_container_cls._create(
|
||||
self.dg, title, dismissible=dismissible, width=width
|
||||
)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
@@ -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.
|
||||
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 collections.abc import Collection
|
||||
from typing import Any, Callable, Union, cast
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit.errors import StreamlitInvalidColorError
|
||||
|
||||
# components go from 0.0 to 1.0
|
||||
# Supported by Pillow and pretty common.
|
||||
FloatRGBColorTuple: TypeAlias = tuple[float, float, float]
|
||||
FloatRGBAColorTuple: TypeAlias = tuple[float, float, float, float]
|
||||
|
||||
# components go from 0 to 255
|
||||
# DeckGL uses these.
|
||||
IntRGBColorTuple: TypeAlias = tuple[int, int, int]
|
||||
IntRGBAColorTuple: TypeAlias = tuple[int, int, int, int]
|
||||
|
||||
# components go from 0 to 255, except alpha goes from 0.0 to 1.0
|
||||
# CSS uses these.
|
||||
MixedRGBAColorTuple: TypeAlias = tuple[int, int, int, float]
|
||||
|
||||
Color4Tuple: TypeAlias = Union[
|
||||
FloatRGBAColorTuple,
|
||||
IntRGBAColorTuple,
|
||||
MixedRGBAColorTuple,
|
||||
]
|
||||
|
||||
Color3Tuple: TypeAlias = Union[
|
||||
FloatRGBColorTuple,
|
||||
IntRGBColorTuple,
|
||||
]
|
||||
|
||||
ColorTuple: TypeAlias = Union[Color4Tuple, Color3Tuple]
|
||||
|
||||
IntColorTuple = Union[IntRGBColorTuple, IntRGBAColorTuple]
|
||||
CSSColorStr = Union[IntRGBAColorTuple, MixedRGBAColorTuple]
|
||||
|
||||
ColorStr: TypeAlias = str
|
||||
|
||||
Color: TypeAlias = Union[ColorTuple, ColorStr]
|
||||
MaybeColor: TypeAlias = Union[str, Collection[Any]]
|
||||
|
||||
|
||||
def to_int_color_tuple(color: MaybeColor) -> IntColorTuple:
|
||||
"""Convert input into color tuple of type (int, int, int, int)."""
|
||||
color_tuple = _to_color_tuple(
|
||||
color,
|
||||
rgb_formatter=_int_formatter,
|
||||
alpha_formatter=_int_formatter,
|
||||
)
|
||||
return cast("IntColorTuple", color_tuple)
|
||||
|
||||
|
||||
def to_css_color(color: MaybeColor) -> Color:
|
||||
"""Convert input into a CSS-compatible color that Vega can use.
|
||||
|
||||
Inputs must be a hex string, rgb()/rgba() string, or a color tuple. Inputs may not be a CSS
|
||||
color name, other CSS color function (like "hsl(...)"), etc.
|
||||
|
||||
See tests for more info.
|
||||
"""
|
||||
if is_css_color_like(color):
|
||||
return cast("Color", color)
|
||||
|
||||
if is_color_tuple_like(color):
|
||||
ctuple = cast("ColorTuple", color)
|
||||
ctuple = _normalize_tuple(ctuple, _int_formatter, _float_formatter)
|
||||
if len(ctuple) == 3:
|
||||
return f"rgb({ctuple[0]}, {ctuple[1]}, {ctuple[2]})"
|
||||
elif len(ctuple) == 4:
|
||||
c4tuple = cast("MixedRGBAColorTuple", ctuple)
|
||||
return f"rgba({c4tuple[0]}, {c4tuple[1]}, {c4tuple[2]}, {c4tuple[3]})"
|
||||
|
||||
raise StreamlitInvalidColorError(color)
|
||||
|
||||
|
||||
def is_css_color_like(color: MaybeColor) -> bool:
|
||||
"""Check whether the input looks like something Vega can use.
|
||||
|
||||
This is meant to be lightweight, and not a definitive answer. The definitive solution is to try
|
||||
to convert and see if an error is thrown.
|
||||
|
||||
NOTE: We only accept hex colors and color tuples as user input. So do not use this function to
|
||||
validate user input! Instead use is_hex_color_like and is_color_tuple_like.
|
||||
"""
|
||||
return is_hex_color_like(color) or _is_cssrgb_color_like(color)
|
||||
|
||||
|
||||
def is_hex_color_like(color: MaybeColor) -> bool:
|
||||
"""Check whether the input looks like a hex color.
|
||||
|
||||
This is meant to be lightweight, and not a definitive answer. The definitive solution is to try
|
||||
to convert and see if an error is thrown.
|
||||
"""
|
||||
return (
|
||||
isinstance(color, str)
|
||||
and color.startswith("#")
|
||||
and color[1:].isalnum() # Alphanumeric
|
||||
and len(color) in {4, 5, 7, 9}
|
||||
)
|
||||
|
||||
|
||||
def _is_cssrgb_color_like(color: MaybeColor) -> bool:
|
||||
"""Check whether the input looks like a CSS rgb() or rgba() color string.
|
||||
|
||||
This is meant to be lightweight, and not a definitive answer. The definitive solution is to try
|
||||
to convert and see if an error is thrown.
|
||||
|
||||
NOTE: We only accept hex colors and color tuples as user input. So do not use this function to
|
||||
validate user input! Instead use is_hex_color_like and is_color_tuple_like.
|
||||
"""
|
||||
return isinstance(color, str) and color.startswith(("rgb(", "rgba("))
|
||||
|
||||
|
||||
def is_color_tuple_like(color: MaybeColor) -> bool:
|
||||
"""Check whether the input looks like a tuple color.
|
||||
|
||||
This is meant to be lightweight, and not a definitive answer. The definitive solution is to try
|
||||
to convert and see if an error is thrown.
|
||||
"""
|
||||
return (
|
||||
isinstance(color, (tuple, list))
|
||||
and len(color) in {3, 4}
|
||||
and all(isinstance(c, (int, float)) for c in color)
|
||||
)
|
||||
|
||||
|
||||
def is_color_like(color: MaybeColor) -> bool:
|
||||
"""A fairly lightweight check of whether the input is a color.
|
||||
|
||||
This isn't meant to be a definitive answer. The definitive solution is to
|
||||
try to convert and see if an error is thrown.
|
||||
"""
|
||||
return is_css_color_like(color) or is_color_tuple_like(color)
|
||||
|
||||
|
||||
# Wrote our own hex-to-tuple parser to avoid bringing in a dependency.
|
||||
def _to_color_tuple(
|
||||
color: MaybeColor,
|
||||
rgb_formatter: Callable[[float, MaybeColor], float],
|
||||
alpha_formatter: Callable[[float, MaybeColor], float],
|
||||
):
|
||||
"""Convert a potential color to a color tuple.
|
||||
|
||||
The exact type of color tuple this outputs is dictated by the formatter parameters.
|
||||
|
||||
The R, G, B components are transformed by rgb_formatter, and the alpha component is transformed
|
||||
by alpha_formatter.
|
||||
|
||||
For example, to output a (float, float, float, int) color tuple, set rgb_formatter
|
||||
to _float_formatter and alpha_formatter to _int_formatter.
|
||||
"""
|
||||
if is_hex_color_like(color):
|
||||
hex_len = len(color)
|
||||
color_hex = cast("str", color)
|
||||
|
||||
if hex_len == 4:
|
||||
r = 2 * color_hex[1]
|
||||
g = 2 * color_hex[2]
|
||||
b = 2 * color_hex[3]
|
||||
a = "ff"
|
||||
elif hex_len == 5:
|
||||
r = 2 * color_hex[1]
|
||||
g = 2 * color_hex[2]
|
||||
b = 2 * color_hex[3]
|
||||
a = 2 * color_hex[4]
|
||||
elif hex_len == 7:
|
||||
r = color_hex[1:3]
|
||||
g = color_hex[3:5]
|
||||
b = color_hex[5:7]
|
||||
a = "ff"
|
||||
elif hex_len == 9:
|
||||
r = color_hex[1:3]
|
||||
g = color_hex[3:5]
|
||||
b = color_hex[5:7]
|
||||
a = color_hex[7:9]
|
||||
else:
|
||||
raise StreamlitInvalidColorError(color)
|
||||
|
||||
try:
|
||||
color = int(r, 16), int(g, 16), int(b, 16), int(a, 16)
|
||||
except Exception as ex:
|
||||
raise StreamlitInvalidColorError(color) from ex
|
||||
|
||||
if is_color_tuple_like(color):
|
||||
color_tuple = cast("ColorTuple", color)
|
||||
return _normalize_tuple(color_tuple, rgb_formatter, alpha_formatter)
|
||||
|
||||
raise StreamlitInvalidColorError(color)
|
||||
|
||||
|
||||
def _normalize_tuple(
|
||||
color: ColorTuple,
|
||||
rgb_formatter: Callable[[float, MaybeColor], float],
|
||||
alpha_formatter: Callable[[float, MaybeColor], float],
|
||||
) -> ColorTuple:
|
||||
"""Parse color tuple using the specified color formatters.
|
||||
|
||||
The R, G, B components are transformed by rgb_formatter, and the alpha component is transformed
|
||||
by alpha_formatter.
|
||||
|
||||
For example, to output a (float, float, float, int) color tuple, set rgb_formatter
|
||||
to _float_formatter and alpha_formatter to _int_formatter.
|
||||
"""
|
||||
if len(color) == 3:
|
||||
r = rgb_formatter(color[0], color)
|
||||
g = rgb_formatter(color[1], color)
|
||||
b = rgb_formatter(color[2], color)
|
||||
return r, g, b
|
||||
|
||||
elif len(color) == 4:
|
||||
color_4tuple = cast("Color4Tuple", color)
|
||||
r = rgb_formatter(color_4tuple[0], color_4tuple)
|
||||
g = rgb_formatter(color_4tuple[1], color_4tuple)
|
||||
b = rgb_formatter(color_4tuple[2], color_4tuple)
|
||||
alpha = alpha_formatter(color_4tuple[3], color_4tuple)
|
||||
return r, g, b, alpha
|
||||
|
||||
raise StreamlitInvalidColorError(color)
|
||||
|
||||
|
||||
def _int_formatter(component: float, color: MaybeColor) -> int:
|
||||
"""Convert a color component (float or int) to an int from 0 to 255.
|
||||
|
||||
Anything too small will become 0, and anything too large will become 255.
|
||||
"""
|
||||
if isinstance(component, float):
|
||||
component = int(component * 255)
|
||||
|
||||
if isinstance(component, int):
|
||||
return min(255, max(component, 0))
|
||||
|
||||
raise StreamlitInvalidColorError(color)
|
||||
|
||||
|
||||
def _float_formatter(component: float, color: MaybeColor) -> float:
|
||||
"""Convert a color component (float or int) to a float from 0.0 to 1.0.
|
||||
|
||||
Anything too small will become 0.0, and anything too large will become 1.0.
|
||||
"""
|
||||
if isinstance(component, int):
|
||||
component = component / 255.0
|
||||
|
||||
if isinstance(component, float):
|
||||
return min(1.0, max(component, 0.0))
|
||||
|
||||
raise StreamlitInvalidColorError(color)
|
||||
@@ -0,0 +1,539 @@
|
||||
# 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 copy
|
||||
import json
|
||||
from collections.abc import Mapping
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Final, Literal, Union
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit.dataframe_util import DataFormat
|
||||
from streamlit.elements.lib.column_types import ColumnConfig, ColumnType
|
||||
from streamlit.elements.lib.dicttools import remove_none_values
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import pyarrow as pa
|
||||
from pandas import DataFrame, Index, Series
|
||||
|
||||
from streamlit.proto.Arrow_pb2 import Arrow as ArrowProto
|
||||
|
||||
|
||||
# The index identifier can be used to apply configuration options
|
||||
IndexIdentifierType = Literal["_index"]
|
||||
INDEX_IDENTIFIER: IndexIdentifierType = "_index"
|
||||
|
||||
# This is used as prefix for columns that are configured via the numerical position.
|
||||
# The integer value is converted into a string key with this prefix.
|
||||
# This needs to match with the prefix configured in the frontend.
|
||||
_NUMERICAL_POSITION_PREFIX = "_pos:"
|
||||
|
||||
|
||||
# The column data kind is used to describe the type of the data within the column.
|
||||
class ColumnDataKind(str, Enum):
|
||||
INTEGER = "integer"
|
||||
FLOAT = "float"
|
||||
DATE = "date"
|
||||
TIME = "time"
|
||||
DATETIME = "datetime"
|
||||
BOOLEAN = "boolean"
|
||||
STRING = "string"
|
||||
TIMEDELTA = "timedelta"
|
||||
PERIOD = "period"
|
||||
INTERVAL = "interval"
|
||||
BYTES = "bytes"
|
||||
DECIMAL = "decimal"
|
||||
COMPLEX = "complex"
|
||||
LIST = "list"
|
||||
DICT = "dict"
|
||||
EMPTY = "empty"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
# The dataframe schema is a mapping from the name of the column
|
||||
# in the underlying dataframe to the column data kind.
|
||||
# The index column uses `_index` as name.
|
||||
DataframeSchema: TypeAlias = dict[str, ColumnDataKind]
|
||||
|
||||
# This mapping contains all editable column types mapped to the data kinds
|
||||
# that the column type is compatible for editing.
|
||||
_EDITING_COMPATIBILITY_MAPPING: Final[dict[ColumnType, list[ColumnDataKind]]] = {
|
||||
"text": [ColumnDataKind.STRING, ColumnDataKind.EMPTY],
|
||||
"number": [
|
||||
ColumnDataKind.INTEGER,
|
||||
ColumnDataKind.FLOAT,
|
||||
ColumnDataKind.DECIMAL,
|
||||
ColumnDataKind.STRING,
|
||||
ColumnDataKind.TIMEDELTA,
|
||||
ColumnDataKind.EMPTY,
|
||||
],
|
||||
"checkbox": [
|
||||
ColumnDataKind.BOOLEAN,
|
||||
ColumnDataKind.STRING,
|
||||
ColumnDataKind.INTEGER,
|
||||
ColumnDataKind.EMPTY,
|
||||
],
|
||||
"selectbox": [
|
||||
ColumnDataKind.STRING,
|
||||
ColumnDataKind.BOOLEAN,
|
||||
ColumnDataKind.INTEGER,
|
||||
ColumnDataKind.FLOAT,
|
||||
ColumnDataKind.EMPTY,
|
||||
],
|
||||
"date": [ColumnDataKind.DATE, ColumnDataKind.DATETIME, ColumnDataKind.EMPTY],
|
||||
"time": [ColumnDataKind.TIME, ColumnDataKind.DATETIME, ColumnDataKind.EMPTY],
|
||||
"datetime": [
|
||||
ColumnDataKind.DATETIME,
|
||||
ColumnDataKind.DATE,
|
||||
ColumnDataKind.TIME,
|
||||
ColumnDataKind.EMPTY,
|
||||
],
|
||||
"link": [ColumnDataKind.STRING, ColumnDataKind.EMPTY],
|
||||
}
|
||||
|
||||
|
||||
def is_type_compatible(column_type: ColumnType, data_kind: ColumnDataKind) -> bool:
|
||||
"""Check if the column type is compatible with the underlying data kind.
|
||||
|
||||
This check only applies to editable column types (e.g. number or text).
|
||||
Non-editable column types (e.g. bar_chart or image) can be configured for
|
||||
all data kinds (this might change in the future).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
column_type : ColumnType
|
||||
The column type to check.
|
||||
|
||||
data_kind : ColumnDataKind
|
||||
The data kind to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if the column type is compatible with the data kind, False otherwise.
|
||||
"""
|
||||
|
||||
if column_type not in _EDITING_COMPATIBILITY_MAPPING:
|
||||
return True
|
||||
|
||||
return data_kind in _EDITING_COMPATIBILITY_MAPPING[column_type]
|
||||
|
||||
|
||||
def _determine_data_kind_via_arrow(field: pa.Field) -> ColumnDataKind:
|
||||
"""Determine the data kind via the arrow type information.
|
||||
|
||||
The column data kind refers to the shared data type of the values
|
||||
in the column (e.g. int, float, str, bool).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
field : pa.Field
|
||||
The arrow field from the arrow table schema.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ColumnDataKind
|
||||
The data kind of the field.
|
||||
"""
|
||||
import pyarrow as pa
|
||||
|
||||
field_type = field.type
|
||||
if pa.types.is_integer(field_type):
|
||||
return ColumnDataKind.INTEGER
|
||||
|
||||
if pa.types.is_floating(field_type):
|
||||
return ColumnDataKind.FLOAT
|
||||
|
||||
if pa.types.is_boolean(field_type):
|
||||
return ColumnDataKind.BOOLEAN
|
||||
|
||||
if pa.types.is_string(field_type):
|
||||
return ColumnDataKind.STRING
|
||||
|
||||
if pa.types.is_date(field_type):
|
||||
return ColumnDataKind.DATE
|
||||
|
||||
if pa.types.is_time(field_type):
|
||||
return ColumnDataKind.TIME
|
||||
|
||||
if pa.types.is_timestamp(field_type):
|
||||
return ColumnDataKind.DATETIME
|
||||
|
||||
if pa.types.is_duration(field_type):
|
||||
return ColumnDataKind.TIMEDELTA
|
||||
|
||||
if pa.types.is_list(field_type):
|
||||
return ColumnDataKind.LIST
|
||||
|
||||
if pa.types.is_decimal(field_type):
|
||||
return ColumnDataKind.DECIMAL
|
||||
|
||||
if pa.types.is_null(field_type):
|
||||
return ColumnDataKind.EMPTY
|
||||
|
||||
# Interval does not seem to work correctly:
|
||||
# if pa.types.is_interval(field_type):
|
||||
# return ColumnDataKind.INTERVAL
|
||||
|
||||
if pa.types.is_binary(field_type):
|
||||
return ColumnDataKind.BYTES
|
||||
|
||||
if pa.types.is_struct(field_type):
|
||||
return ColumnDataKind.DICT
|
||||
|
||||
return ColumnDataKind.UNKNOWN
|
||||
|
||||
|
||||
def _determine_data_kind_via_pandas_dtype(
|
||||
column: Series | Index,
|
||||
) -> ColumnDataKind:
|
||||
"""Determine the data kind by using the pandas dtype.
|
||||
|
||||
The column data kind refers to the shared data type of the values
|
||||
in the column (e.g. int, float, str, bool).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
column : pd.Series, pd.Index
|
||||
The column for which the data kind should be determined.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ColumnDataKind
|
||||
The data kind of the column.
|
||||
"""
|
||||
import pandas as pd
|
||||
|
||||
column_dtype = column.dtype
|
||||
if pd.api.types.is_bool_dtype(column_dtype):
|
||||
return ColumnDataKind.BOOLEAN
|
||||
|
||||
if pd.api.types.is_integer_dtype(column_dtype):
|
||||
return ColumnDataKind.INTEGER
|
||||
|
||||
if pd.api.types.is_float_dtype(column_dtype):
|
||||
return ColumnDataKind.FLOAT
|
||||
|
||||
if pd.api.types.is_datetime64_any_dtype(column_dtype):
|
||||
return ColumnDataKind.DATETIME
|
||||
|
||||
if pd.api.types.is_timedelta64_dtype(column_dtype):
|
||||
return ColumnDataKind.TIMEDELTA
|
||||
|
||||
if isinstance(column_dtype, pd.PeriodDtype):
|
||||
return ColumnDataKind.PERIOD
|
||||
|
||||
if isinstance(column_dtype, pd.IntervalDtype):
|
||||
return ColumnDataKind.INTERVAL
|
||||
|
||||
if pd.api.types.is_complex_dtype(column_dtype):
|
||||
return ColumnDataKind.COMPLEX
|
||||
|
||||
if pd.api.types.is_object_dtype(
|
||||
column_dtype
|
||||
) is False and pd.api.types.is_string_dtype(column_dtype):
|
||||
# The is_string_dtype
|
||||
return ColumnDataKind.STRING
|
||||
|
||||
return ColumnDataKind.UNKNOWN
|
||||
|
||||
|
||||
def _determine_data_kind_via_inferred_type(
|
||||
column: Series | Index,
|
||||
) -> ColumnDataKind:
|
||||
"""Determine the data kind by inferring it from the underlying data.
|
||||
|
||||
The column data kind refers to the shared data type of the values
|
||||
in the column (e.g. int, float, str, bool).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
column : pd.Series, pd.Index
|
||||
The column to determine the data kind for.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ColumnDataKind
|
||||
The data kind of the column.
|
||||
"""
|
||||
from pandas.api.types import infer_dtype
|
||||
|
||||
inferred_type = infer_dtype(column)
|
||||
|
||||
if inferred_type == "string":
|
||||
return ColumnDataKind.STRING
|
||||
|
||||
if inferred_type == "bytes":
|
||||
return ColumnDataKind.BYTES
|
||||
|
||||
if inferred_type in ["floating", "mixed-integer-float"]:
|
||||
return ColumnDataKind.FLOAT
|
||||
|
||||
if inferred_type == "integer":
|
||||
return ColumnDataKind.INTEGER
|
||||
|
||||
if inferred_type == "decimal":
|
||||
return ColumnDataKind.DECIMAL
|
||||
|
||||
if inferred_type == "complex":
|
||||
return ColumnDataKind.COMPLEX
|
||||
|
||||
if inferred_type == "boolean":
|
||||
return ColumnDataKind.BOOLEAN
|
||||
|
||||
if inferred_type in ["datetime64", "datetime"]:
|
||||
return ColumnDataKind.DATETIME
|
||||
|
||||
if inferred_type == "date":
|
||||
return ColumnDataKind.DATE
|
||||
|
||||
if inferred_type in ["timedelta64", "timedelta"]:
|
||||
return ColumnDataKind.TIMEDELTA
|
||||
|
||||
if inferred_type == "time":
|
||||
return ColumnDataKind.TIME
|
||||
|
||||
if inferred_type == "period":
|
||||
return ColumnDataKind.PERIOD
|
||||
|
||||
if inferred_type == "interval":
|
||||
return ColumnDataKind.INTERVAL
|
||||
|
||||
if inferred_type == "empty":
|
||||
return ColumnDataKind.EMPTY
|
||||
|
||||
# Unused types: mixed, unknown-array, categorical, mixed-integer
|
||||
|
||||
return ColumnDataKind.UNKNOWN
|
||||
|
||||
|
||||
def _determine_data_kind(
|
||||
column: Series | Index, field: pa.Field | None = None
|
||||
) -> ColumnDataKind:
|
||||
"""Determine the data kind of a column.
|
||||
|
||||
The column data kind refers to the shared data type of the values
|
||||
in the column (e.g. int, float, str, bool).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
column : pd.Series, pd.Index
|
||||
The column to determine the data kind for.
|
||||
field : pa.Field, optional
|
||||
The arrow field from the arrow table schema.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ColumnDataKind
|
||||
The data kind of the column.
|
||||
"""
|
||||
import pandas as pd
|
||||
|
||||
if isinstance(column.dtype, pd.CategoricalDtype):
|
||||
# Categorical columns can have different underlying data kinds
|
||||
# depending on the categories.
|
||||
return _determine_data_kind_via_inferred_type(column.dtype.categories)
|
||||
|
||||
if field is not None:
|
||||
data_kind = _determine_data_kind_via_arrow(field)
|
||||
if data_kind != ColumnDataKind.UNKNOWN:
|
||||
return data_kind
|
||||
|
||||
if column.dtype.name == "object":
|
||||
# If dtype is object, we need to infer the type from the column
|
||||
return _determine_data_kind_via_inferred_type(column)
|
||||
return _determine_data_kind_via_pandas_dtype(column)
|
||||
|
||||
|
||||
def determine_dataframe_schema(
|
||||
data_df: DataFrame, arrow_schema: pa.Schema
|
||||
) -> DataframeSchema:
|
||||
"""Determine the schema of a dataframe.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data_df : pd.DataFrame
|
||||
The dataframe to determine the schema of.
|
||||
arrow_schema : pa.Schema
|
||||
The Arrow schema of the dataframe.
|
||||
|
||||
Returns
|
||||
-------
|
||||
DataframeSchema
|
||||
A mapping that contains the detected data type for the index and columns.
|
||||
The key is the column name in the underlying dataframe or ``_index`` for index columns.
|
||||
"""
|
||||
|
||||
dataframe_schema: DataframeSchema = {}
|
||||
|
||||
# Add type of index:
|
||||
# TODO(lukasmasuch): We need to apply changes here to support multiindex.
|
||||
dataframe_schema[INDEX_IDENTIFIER] = _determine_data_kind(data_df.index)
|
||||
|
||||
# Add types for all columns:
|
||||
for i, column in enumerate(data_df.items()):
|
||||
column_name, column_data = column
|
||||
dataframe_schema[column_name] = _determine_data_kind(
|
||||
column_data, arrow_schema.field(i)
|
||||
)
|
||||
return dataframe_schema
|
||||
|
||||
|
||||
# A mapping of column names/IDs to column configs.
|
||||
ColumnConfigMapping: TypeAlias = dict[Union[IndexIdentifierType, str], ColumnConfig]
|
||||
ColumnConfigMappingInput: TypeAlias = Mapping[
|
||||
Union[IndexIdentifierType, str],
|
||||
Union[ColumnConfig, None, str],
|
||||
]
|
||||
|
||||
|
||||
def process_config_mapping(
|
||||
column_config: ColumnConfigMappingInput | None = None,
|
||||
) -> ColumnConfigMapping:
|
||||
"""Transforms a user-provided column config mapping into a valid column config mapping
|
||||
that can be used by the frontend.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
column_config: dict or None
|
||||
The user-provided column config mapping.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
The transformed column config mapping.
|
||||
"""
|
||||
if column_config is None:
|
||||
return {}
|
||||
|
||||
transformed_column_config: ColumnConfigMapping = {}
|
||||
for column, config in column_config.items():
|
||||
if config is None:
|
||||
transformed_column_config[column] = ColumnConfig(hidden=True)
|
||||
elif isinstance(config, str):
|
||||
transformed_column_config[column] = ColumnConfig(label=config)
|
||||
elif isinstance(config, dict):
|
||||
# Ensure that the column config objects are cloned
|
||||
# since we will apply in-place changes to it.
|
||||
transformed_column_config[column] = copy.deepcopy(config)
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
f"Invalid column config for column `{column}`. "
|
||||
f"Expected `None`, `str` or `dict`, but got `{type(config)}`."
|
||||
)
|
||||
return transformed_column_config
|
||||
|
||||
|
||||
def update_column_config(
|
||||
column_config_mapping: ColumnConfigMapping, column: str, column_config: ColumnConfig
|
||||
) -> None:
|
||||
"""Updates the column config value for a single column within the mapping.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
column_config_mapping : ColumnConfigMapping
|
||||
The column config mapping to update.
|
||||
|
||||
column : str
|
||||
The column to update the config value for.
|
||||
|
||||
column_config : ColumnConfig
|
||||
The column config to update.
|
||||
"""
|
||||
|
||||
if column not in column_config_mapping:
|
||||
column_config_mapping[column] = {}
|
||||
|
||||
column_config_mapping[column].update(column_config)
|
||||
|
||||
|
||||
def apply_data_specific_configs(
|
||||
columns_config: ColumnConfigMapping,
|
||||
data_format: DataFormat,
|
||||
) -> None:
|
||||
"""Apply data specific configurations to the provided dataframe.
|
||||
|
||||
This will apply inplace changes to the dataframe and the column configurations
|
||||
depending on the data format.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
columns_config : ColumnConfigMapping
|
||||
A mapping of column names/ids to column configurations.
|
||||
|
||||
data_format : DataFormat
|
||||
The format of the data.
|
||||
"""
|
||||
|
||||
# Pandas adds a range index as default to all datastructures
|
||||
# but for most of the non-pandas data objects it is unnecessary
|
||||
# to show this index to the user. Therefore, we will hide it as default.
|
||||
if data_format in [
|
||||
DataFormat.SET_OF_VALUES,
|
||||
DataFormat.TUPLE_OF_VALUES,
|
||||
DataFormat.LIST_OF_VALUES,
|
||||
DataFormat.NUMPY_LIST,
|
||||
DataFormat.NUMPY_MATRIX,
|
||||
DataFormat.LIST_OF_RECORDS,
|
||||
DataFormat.LIST_OF_ROWS,
|
||||
DataFormat.COLUMN_VALUE_MAPPING,
|
||||
# Dataframe-like objects that don't have an index:
|
||||
DataFormat.PANDAS_ARRAY,
|
||||
DataFormat.PANDAS_INDEX,
|
||||
DataFormat.POLARS_DATAFRAME,
|
||||
DataFormat.POLARS_SERIES,
|
||||
DataFormat.POLARS_LAZYFRAME,
|
||||
DataFormat.PYARROW_ARRAY,
|
||||
DataFormat.RAY_DATASET,
|
||||
]:
|
||||
update_column_config(columns_config, INDEX_IDENTIFIER, {"hidden": True})
|
||||
|
||||
|
||||
def _convert_column_config_to_json(column_config_mapping: ColumnConfigMapping) -> str:
|
||||
try:
|
||||
# Ignore all None values and prefix columns specified by numerical index:
|
||||
return json.dumps(
|
||||
{
|
||||
(
|
||||
f"{_NUMERICAL_POSITION_PREFIX}{str(k)}" if isinstance(k, int) else k
|
||||
): v
|
||||
for (k, v) in remove_none_values(column_config_mapping).items()
|
||||
},
|
||||
allow_nan=False,
|
||||
)
|
||||
except ValueError as ex:
|
||||
raise StreamlitAPIException(
|
||||
f"The provided column config cannot be serialized into JSON: {ex}"
|
||||
) from ex
|
||||
|
||||
|
||||
def marshall_column_config(
|
||||
proto: ArrowProto, column_config_mapping: ColumnConfigMapping
|
||||
) -> None:
|
||||
"""Marshall the column config into the Arrow proto.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
proto : ArrowProto
|
||||
The proto to marshall into.
|
||||
|
||||
column_config_mapping : ColumnConfigMapping
|
||||
The column config to marshall.
|
||||
"""
|
||||
|
||||
proto.columns = _convert_column_config_to_json(column_config_mapping)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,147 @@
|
||||
# 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 typing import TYPE_CHECKING, Literal, cast
|
||||
|
||||
from typing_extensions import Self, TypeAlias
|
||||
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.Block_pb2 import Block as BlockProto
|
||||
from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
|
||||
from streamlit.runtime.scriptrunner_utils.script_run_context import (
|
||||
enqueue_message,
|
||||
get_script_run_ctx,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types import TracebackType
|
||||
|
||||
from streamlit.cursor import Cursor
|
||||
|
||||
DialogWidth: TypeAlias = Literal["small", "large"]
|
||||
|
||||
|
||||
def _process_dialog_width_input(
|
||||
width: DialogWidth,
|
||||
) -> BlockProto.Dialog.DialogWidth.ValueType:
|
||||
"""Maps the user-provided literal to a value of the DialogWidth proto enum.
|
||||
|
||||
Returns the mapped enum field for "small" by default and otherwise the mapped type.
|
||||
"""
|
||||
if width == "large":
|
||||
return BlockProto.Dialog.DialogWidth.LARGE
|
||||
|
||||
return BlockProto.Dialog.DialogWidth.SMALL
|
||||
|
||||
|
||||
def _assert_first_dialog_to_be_opened(should_open: bool) -> None:
|
||||
"""Check whether a dialog has already been opened in the same script run.
|
||||
|
||||
Only one dialog is supposed to be opened. The check is implemented in a way
|
||||
that for a script run, the open function can only be called once.
|
||||
One dialog at a time is a product decision and not a technical one.
|
||||
|
||||
Raises
|
||||
------
|
||||
StreamlitAPIException
|
||||
Raised when a dialog has already been opened in the current script run.
|
||||
"""
|
||||
script_run_ctx = get_script_run_ctx()
|
||||
# We don't reset the ctx.has_dialog_opened when the flag is False because
|
||||
# it is reset in a new scriptrun anyways. If the execution model ever changes,
|
||||
# this might need to change.
|
||||
if should_open and script_run_ctx:
|
||||
if script_run_ctx.has_dialog_opened:
|
||||
raise StreamlitAPIException(
|
||||
"Only one dialog is allowed to be opened at the same time. Please make sure to not call a dialog-decorated function more than once in a script run."
|
||||
)
|
||||
script_run_ctx.has_dialog_opened = True
|
||||
|
||||
|
||||
class Dialog(DeltaGenerator):
|
||||
@staticmethod
|
||||
def _create(
|
||||
parent: DeltaGenerator,
|
||||
title: str,
|
||||
*,
|
||||
dismissible: bool = True,
|
||||
width: DialogWidth = "small",
|
||||
) -> Dialog:
|
||||
block_proto = BlockProto()
|
||||
block_proto.dialog.title = title
|
||||
block_proto.dialog.dismissible = dismissible
|
||||
block_proto.dialog.width = _process_dialog_width_input(width)
|
||||
|
||||
# We store the delta path here, because in _update we enqueue a new proto
|
||||
# message to update the open status. Without this, the dialog content is gone
|
||||
# when the _update message is sent
|
||||
delta_path: list[int] = (
|
||||
parent._active_dg._cursor.delta_path if parent._active_dg._cursor else []
|
||||
)
|
||||
dialog = cast("Dialog", parent._block(block_proto=block_proto, dg_type=Dialog))
|
||||
|
||||
dialog._delta_path = delta_path
|
||||
dialog._current_proto = block_proto
|
||||
return dialog
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
root_container: int | None,
|
||||
cursor: Cursor | None,
|
||||
parent: DeltaGenerator | None,
|
||||
block_type: str | None,
|
||||
):
|
||||
super().__init__(root_container, cursor, parent, block_type)
|
||||
|
||||
# Initialized in `_create()`:
|
||||
self._current_proto: BlockProto | None = None
|
||||
self._delta_path: list[int] | None = None
|
||||
|
||||
def _update(self, should_open: bool):
|
||||
"""Send an updated proto message to indicate the open-status for the dialog."""
|
||||
|
||||
assert self._current_proto is not None, "Dialog not correctly initialized!"
|
||||
assert self._delta_path is not None, "Dialog not correctly initialized!"
|
||||
_assert_first_dialog_to_be_opened(should_open)
|
||||
msg = ForwardMsg()
|
||||
msg.metadata.delta_path[:] = self._delta_path
|
||||
msg.delta.add_block.CopyFrom(self._current_proto)
|
||||
msg.delta.add_block.dialog.is_open = should_open
|
||||
self._current_proto = msg.delta.add_block
|
||||
|
||||
enqueue_message(msg)
|
||||
|
||||
def open(self) -> None:
|
||||
self._update(True)
|
||||
|
||||
def close(self) -> None:
|
||||
self._update(False)
|
||||
|
||||
def __enter__(self) -> Self: # type: ignore[override]
|
||||
# This is a little dubious: we're returning a different type than
|
||||
# our superclass' `__enter__` function. Maybe DeltaGenerator.__enter__
|
||||
# should always return `self`?
|
||||
super().__enter__()
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> Literal[False]:
|
||||
return super().__exit__(exc_type, exc_val, exc_tb)
|
||||
@@ -0,0 +1,154 @@
|
||||
# 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.
|
||||
|
||||
"""Tools for working with dicts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Mapping
|
||||
|
||||
|
||||
def _unflatten_single_dict(flat_dict: dict[Any, Any]) -> dict[Any, Any]:
|
||||
"""Convert a flat dict of key-value pairs to dict tree.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
_unflatten_single_dict({
|
||||
foo_bar_baz: 123,
|
||||
foo_bar_biz: 456,
|
||||
x_bonks: 'hi',
|
||||
})
|
||||
|
||||
# Returns:
|
||||
# {
|
||||
# foo: {
|
||||
# bar: {
|
||||
# baz: 123,
|
||||
# biz: 456,
|
||||
# },
|
||||
# },
|
||||
# x: {
|
||||
# bonks: 'hi'
|
||||
# }
|
||||
# }
|
||||
|
||||
Parameters
|
||||
----------
|
||||
flat_dict : dict
|
||||
A one-level dict where keys are fully-qualified paths separated by
|
||||
underscores.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
A tree made of dicts inside of dicts.
|
||||
|
||||
"""
|
||||
out: dict[str, Any] = {}
|
||||
for pathstr, v in flat_dict.items():
|
||||
path = pathstr.split("_")
|
||||
|
||||
prev_dict: dict[str, Any] | None = None
|
||||
curr_dict = out
|
||||
|
||||
for k in path:
|
||||
if k not in curr_dict:
|
||||
curr_dict[k] = {}
|
||||
prev_dict = curr_dict
|
||||
curr_dict = curr_dict[k]
|
||||
|
||||
if prev_dict is not None:
|
||||
prev_dict[k] = v
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def unflatten(
|
||||
flat_dict: dict[Any, Any], encodings: set[str] | None = None
|
||||
) -> dict[Any, Any]:
|
||||
"""Converts a flat dict of key-value pairs to a spec tree.
|
||||
|
||||
Example
|
||||
-------
|
||||
unflatten({
|
||||
foo_bar_baz: 123,
|
||||
foo_bar_biz: 456,
|
||||
x_bonks: 'hi',
|
||||
}, ['x'])
|
||||
|
||||
# Returns:
|
||||
# {
|
||||
# foo: {
|
||||
# bar: {
|
||||
# baz: 123,
|
||||
# biz: 456,
|
||||
# },
|
||||
# },
|
||||
# encoding: { # This gets added automatically
|
||||
# x: {
|
||||
# bonks: 'hi'
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
||||
Args
|
||||
----
|
||||
flat_dict: dict
|
||||
A flat dict where keys are fully-qualified paths separated by
|
||||
underscores.
|
||||
|
||||
encodings: set
|
||||
Key names that should be automatically moved into the 'encoding' key.
|
||||
|
||||
Returns
|
||||
-------
|
||||
A tree made of dicts inside of dicts.
|
||||
"""
|
||||
if encodings is None:
|
||||
encodings = set()
|
||||
|
||||
out_dict = _unflatten_single_dict(flat_dict)
|
||||
|
||||
for k, v in list(out_dict.items()):
|
||||
# Unflatten child dicts:
|
||||
if isinstance(v, dict):
|
||||
v = unflatten(v, encodings)
|
||||
elif hasattr(v, "__iter__"):
|
||||
for i, child in enumerate(v):
|
||||
if isinstance(child, dict):
|
||||
v[i] = unflatten(child, encodings)
|
||||
|
||||
# Move items into 'encoding' if needed:
|
||||
if k in encodings:
|
||||
if "encoding" not in out_dict:
|
||||
out_dict["encoding"] = {}
|
||||
out_dict["encoding"][k] = v
|
||||
out_dict.pop(k)
|
||||
|
||||
return out_dict
|
||||
|
||||
|
||||
def remove_none_values(input_dict: Mapping[Any, Any]) -> dict[Any, Any]:
|
||||
"""Remove all keys with None values from a dict."""
|
||||
new_dict = {}
|
||||
for key, val in input_dict.items():
|
||||
if isinstance(val, dict):
|
||||
val = remove_none_values(val)
|
||||
if val is not None:
|
||||
new_dict[key] = val
|
||||
return new_dict
|
||||
@@ -0,0 +1,37 @@
|
||||
# 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 typing import Any
|
||||
|
||||
|
||||
class AttributeDictionary(dict[Any, Any]):
|
||||
"""
|
||||
A dictionary subclass that supports attribute-style access.
|
||||
|
||||
This class extends the functionality of a standard dictionary to allow items to be accessed
|
||||
via attribute-style dot notation in addition to the traditional key-based access. If a dictionary
|
||||
item is accessed and is itself a dictionary, it is automatically wrapped in another `AttributeDictionary`,
|
||||
enabling recursive attribute-style access.
|
||||
"""
|
||||
|
||||
def __getattr__(self, key):
|
||||
try:
|
||||
item = self.__getitem__(key)
|
||||
return AttributeDictionary(item) if isinstance(item, dict) else item
|
||||
except KeyError as err:
|
||||
raise AttributeError(
|
||||
f"'{type(self).__name__}' object has no attribute '{key}'"
|
||||
) from err
|
||||
|
||||
__setattr__ = dict.__setitem__
|
||||
@@ -0,0 +1,66 @@
|
||||
# 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 os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
TYPE_PAIRS = [
|
||||
(".jpg", ".jpeg"),
|
||||
(".mpg", ".mpeg"),
|
||||
(".mp4", ".mpeg4"),
|
||||
(".tif", ".tiff"),
|
||||
(".htm", ".html"),
|
||||
]
|
||||
|
||||
|
||||
def normalize_upload_file_type(file_type: str | Sequence[str]) -> Sequence[str]:
|
||||
if isinstance(file_type, str):
|
||||
file_type = [file_type]
|
||||
|
||||
# May need a regex or a library to validate file types are valid
|
||||
# extensions.
|
||||
file_type = [
|
||||
file_type_entry if file_type_entry[0] == "." else f".{file_type_entry}"
|
||||
for file_type_entry in file_type
|
||||
]
|
||||
|
||||
file_type = [t.lower() for t in file_type]
|
||||
|
||||
for x, y in TYPE_PAIRS:
|
||||
if x in file_type and y not in file_type:
|
||||
file_type.append(y)
|
||||
if y in file_type and x not in file_type:
|
||||
file_type.append(x)
|
||||
|
||||
return file_type
|
||||
|
||||
|
||||
def enforce_filename_restriction(filename: str, allowed_types: Sequence[str]) -> None:
|
||||
"""Ensure the uploaded file's extension matches the allowed
|
||||
types set by the app developer. In theory, this should never happen, since we
|
||||
enforce file type check by extension on the frontend, but we check it on backend
|
||||
before returning file to the user to protect ourselves.
|
||||
"""
|
||||
extension = os.path.splitext(filename)[1].lower()
|
||||
if allowed_types and extension not in allowed_types:
|
||||
raise StreamlitAPIException(
|
||||
f"Invalid file extension: `{extension}`. Allowed: {allowed_types}"
|
||||
)
|
||||
@@ -0,0 +1,77 @@
|
||||
# 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 typing import TYPE_CHECKING, NamedTuple
|
||||
|
||||
from streamlit import runtime
|
||||
from streamlit.delta_generator_singletons import context_dg_stack
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
class FormData(NamedTuple):
|
||||
"""Form data stored on a DeltaGenerator."""
|
||||
|
||||
# The form's unique ID.
|
||||
form_id: str
|
||||
|
||||
|
||||
def _current_form(this_dg: DeltaGenerator) -> FormData | None:
|
||||
"""Find the FormData for the given DeltaGenerator.
|
||||
|
||||
Forms are blocks, and can have other blocks nested inside them.
|
||||
To find the current form, we walk up the dg_stack until we find
|
||||
a DeltaGenerator that has FormData.
|
||||
"""
|
||||
if not runtime.exists():
|
||||
return None
|
||||
|
||||
if this_dg._form_data is not None:
|
||||
return this_dg._form_data
|
||||
|
||||
if this_dg == this_dg._main_dg:
|
||||
# We were created via an `st.foo` call.
|
||||
# Walk up the dg_stack to see if we're nested inside a `with st.form` statement.
|
||||
for dg in reversed(context_dg_stack.get()):
|
||||
if dg._form_data is not None:
|
||||
return dg._form_data
|
||||
else:
|
||||
# We were created via an `dg.foo` call.
|
||||
# Take a look at our parent's form data to see if we're nested inside a form.
|
||||
parent = this_dg._parent
|
||||
if parent is not None and parent._form_data is not None:
|
||||
return parent._form_data
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def current_form_id(dg: DeltaGenerator) -> str:
|
||||
"""Return the form_id for the current form, or the empty string if we're
|
||||
not inside an `st.form` block.
|
||||
|
||||
(We return the empty string, instead of None, because this value is
|
||||
assigned to protobuf message fields, and None is not valid.)
|
||||
"""
|
||||
form_data = _current_form(dg)
|
||||
if form_data is None:
|
||||
return ""
|
||||
return form_data.form_id
|
||||
|
||||
|
||||
def is_in_form(dg: DeltaGenerator) -> bool:
|
||||
"""True if the DeltaGenerator is inside an st.form block."""
|
||||
return current_form_id(dg) != ""
|
||||
@@ -0,0 +1,444 @@
|
||||
# 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 io
|
||||
import os
|
||||
import re
|
||||
from collections.abc import Sequence
|
||||
from enum import IntEnum
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Final, Literal, Union, cast
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit import runtime, url_util
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.runtime import caching
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
import numpy.typing as npt
|
||||
from PIL import GifImagePlugin, Image, ImageFile
|
||||
|
||||
from streamlit.proto.Image_pb2 import ImageList as ImageListProto
|
||||
from streamlit.type_util import NumpyShape
|
||||
|
||||
PILImage: TypeAlias = Union[
|
||||
"ImageFile.ImageFile", "Image.Image", "GifImagePlugin.GifImageFile"
|
||||
]
|
||||
AtomicImage: TypeAlias = Union[
|
||||
PILImage, "npt.NDArray[Any]", io.BytesIO, str, Path, bytes
|
||||
]
|
||||
|
||||
Channels: TypeAlias = Literal["RGB", "BGR"]
|
||||
ImageFormat: TypeAlias = Literal["JPEG", "PNG", "GIF"]
|
||||
ImageFormatOrAuto: TypeAlias = Literal[ImageFormat, "auto"]
|
||||
ImageOrImageList: TypeAlias = Union[AtomicImage, Sequence[AtomicImage]]
|
||||
|
||||
# This constant is related to the frontend maximum content width specified
|
||||
# in App.jsx main container
|
||||
# 730 is the max width of element-container in the frontend, and 2x is for high
|
||||
# DPI.
|
||||
MAXIMUM_CONTENT_WIDTH: Final[int] = 2 * 730
|
||||
|
||||
|
||||
# @see Image.proto
|
||||
# @see WidthBehavior on the frontend
|
||||
class WidthBehavior(IntEnum):
|
||||
"""
|
||||
Special values that are recognized by the frontend and allow us to change the
|
||||
behavior of the displayed image.
|
||||
"""
|
||||
|
||||
ORIGINAL = -1
|
||||
COLUMN = -2
|
||||
AUTO = -3
|
||||
MIN_IMAGE_OR_CONTAINER = -4
|
||||
MAX_IMAGE_OR_CONTAINER = -5
|
||||
|
||||
|
||||
WidthBehavior.ORIGINAL.__doc__ = """Display the image at its original width"""
|
||||
WidthBehavior.COLUMN.__doc__ = (
|
||||
"""Display the image at the width of the column it's in."""
|
||||
)
|
||||
WidthBehavior.AUTO.__doc__ = """Display the image at its original width, unless it
|
||||
would exceed the width of its column in which case clamp it to
|
||||
its column width"""
|
||||
|
||||
|
||||
def _image_may_have_alpha_channel(image: PILImage) -> bool:
|
||||
return image.mode in ("RGBA", "LA", "P")
|
||||
|
||||
|
||||
def _image_is_gif(image: PILImage) -> bool:
|
||||
return image.format == "GIF"
|
||||
|
||||
|
||||
def _validate_image_format_string(
|
||||
image_data: bytes | PILImage, format: str
|
||||
) -> ImageFormat:
|
||||
"""Return either "JPEG", "PNG", or "GIF", based on the input `format` string.
|
||||
- If `format` is "JPEG" or "JPG" (or any capitalization thereof), return "JPEG"
|
||||
- If `format` is "PNG" (or any capitalization thereof), return "PNG"
|
||||
- For all other strings, return "PNG" if the image has an alpha channel,
|
||||
"GIF" if the image is a GIF, and "JPEG" otherwise.
|
||||
"""
|
||||
format = format.upper()
|
||||
if format in {"JPEG", "PNG"}:
|
||||
return cast("ImageFormat", format)
|
||||
|
||||
# We are forgiving on the spelling of JPEG
|
||||
if format == "JPG":
|
||||
return "JPEG"
|
||||
|
||||
pil_image: PILImage
|
||||
if isinstance(image_data, bytes):
|
||||
from PIL import Image
|
||||
|
||||
pil_image = Image.open(io.BytesIO(image_data))
|
||||
else:
|
||||
pil_image = image_data
|
||||
|
||||
if _image_is_gif(pil_image):
|
||||
return "GIF"
|
||||
|
||||
if _image_may_have_alpha_channel(pil_image):
|
||||
return "PNG"
|
||||
|
||||
return "JPEG"
|
||||
|
||||
|
||||
def _PIL_to_bytes(
|
||||
image: PILImage,
|
||||
format: ImageFormat = "JPEG",
|
||||
quality: int = 100,
|
||||
) -> bytes:
|
||||
"""Convert a PIL image to bytes."""
|
||||
tmp = io.BytesIO()
|
||||
|
||||
# User must have specified JPEG, so we must convert it
|
||||
if format == "JPEG" and _image_may_have_alpha_channel(image):
|
||||
image = image.convert("RGB")
|
||||
|
||||
image.save(tmp, format=format, quality=quality)
|
||||
|
||||
return tmp.getvalue()
|
||||
|
||||
|
||||
def _BytesIO_to_bytes(data: io.BytesIO) -> bytes:
|
||||
data.seek(0)
|
||||
return data.getvalue()
|
||||
|
||||
|
||||
def _np_array_to_bytes(array: npt.NDArray[Any], output_format: str = "JPEG") -> bytes:
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
img = Image.fromarray(array.astype(np.uint8))
|
||||
format = _validate_image_format_string(img, output_format)
|
||||
|
||||
return _PIL_to_bytes(img, format)
|
||||
|
||||
|
||||
def _verify_np_shape(array: npt.NDArray[Any]) -> npt.NDArray[Any]:
|
||||
shape: NumpyShape = array.shape
|
||||
if len(shape) not in (2, 3):
|
||||
raise StreamlitAPIException("Numpy shape has to be of length 2 or 3.")
|
||||
if len(shape) == 3 and shape[-1] not in (1, 3, 4):
|
||||
raise StreamlitAPIException(
|
||||
"Channel can only be 1, 3, or 4 got %d. Shape is %s"
|
||||
% (shape[-1], str(shape))
|
||||
)
|
||||
|
||||
# If there's only one channel, convert is to x, y
|
||||
if len(shape) == 3 and shape[-1] == 1:
|
||||
array = array[:, :, 0]
|
||||
|
||||
return array
|
||||
|
||||
|
||||
def _get_image_format_mimetype(image_format: ImageFormat) -> str:
|
||||
"""Get the mimetype string for the given ImageFormat."""
|
||||
return f"image/{image_format.lower()}"
|
||||
|
||||
|
||||
def _ensure_image_size_and_format(
|
||||
image_data: bytes, width: int, image_format: ImageFormat
|
||||
) -> bytes:
|
||||
"""Resize an image if it exceeds the given width, or if exceeds
|
||||
MAXIMUM_CONTENT_WIDTH. Ensure the image's format corresponds to the given
|
||||
ImageFormat. Return the (possibly resized and reformatted) image bytes.
|
||||
"""
|
||||
from PIL import Image
|
||||
|
||||
pil_image: PILImage = Image.open(io.BytesIO(image_data))
|
||||
actual_width, actual_height = pil_image.size
|
||||
|
||||
if width < 0 and actual_width > MAXIMUM_CONTENT_WIDTH:
|
||||
width = MAXIMUM_CONTENT_WIDTH
|
||||
|
||||
if width > 0 and actual_width > width:
|
||||
# We need to resize the image.
|
||||
new_height = int(1.0 * actual_height * width / actual_width)
|
||||
# pillow reexports Image.Resampling.BILINEAR as Image.BILINEAR for backwards
|
||||
# compatibility reasons, so we use the reexport to support older pillow
|
||||
# versions. The types don't seem to reflect this, though, hence the type: ignore
|
||||
# below.
|
||||
pil_image = pil_image.resize((width, new_height), resample=Image.BILINEAR) # type: ignore[attr-defined]
|
||||
return _PIL_to_bytes(pil_image, format=image_format, quality=90)
|
||||
|
||||
if pil_image.format != image_format:
|
||||
# We need to reformat the image.
|
||||
return _PIL_to_bytes(pil_image, format=image_format, quality=90)
|
||||
|
||||
# No resizing or reformatting necessary - return the original bytes.
|
||||
return image_data
|
||||
|
||||
|
||||
def _clip_image(image: npt.NDArray[Any], clamp: bool) -> npt.NDArray[Any]:
|
||||
import numpy as np
|
||||
|
||||
data = image
|
||||
if issubclass(image.dtype.type, np.floating):
|
||||
if clamp:
|
||||
data = np.clip(image, 0, 1.0)
|
||||
else:
|
||||
if np.amin(image) < 0.0 or np.amax(image) > 1.0:
|
||||
raise RuntimeError("Data is outside [0.0, 1.0] and clamp is not set.")
|
||||
data = data * 255
|
||||
else:
|
||||
if clamp:
|
||||
data = np.clip(image, 0, 255)
|
||||
else:
|
||||
if np.amin(image) < 0 or np.amax(image) > 255:
|
||||
raise RuntimeError("Data is outside [0, 255] and clamp is not set.")
|
||||
return data
|
||||
|
||||
|
||||
def image_to_url(
|
||||
image: AtomicImage,
|
||||
width: int,
|
||||
clamp: bool,
|
||||
channels: Channels,
|
||||
output_format: ImageFormatOrAuto,
|
||||
image_id: str,
|
||||
) -> str:
|
||||
"""Return a URL that an image can be served from.
|
||||
If `image` is already a URL, return it unmodified.
|
||||
Otherwise, add the image to the MediaFileManager and return the URL.
|
||||
(When running in "raw" mode, we won't actually load data into the
|
||||
MediaFileManager, and we'll return an empty URL).
|
||||
"""
|
||||
import numpy as np
|
||||
from PIL import Image, ImageFile
|
||||
|
||||
image_data: bytes
|
||||
|
||||
# Convert Path to string if necessary
|
||||
if isinstance(image, Path):
|
||||
image = str(image)
|
||||
|
||||
# Strings
|
||||
if isinstance(image, str):
|
||||
if not os.path.isfile(image) and url_util.is_url(
|
||||
image, allowed_schemas=("http", "https", "data")
|
||||
):
|
||||
# If it's a url, return it directly.
|
||||
return image
|
||||
|
||||
if image.endswith(".svg") and os.path.isfile(image):
|
||||
# Unpack local SVG image file to an SVG string
|
||||
with open(image) as textfile:
|
||||
image = textfile.read()
|
||||
|
||||
# Following regex allows svg image files to start either via a "<?xml...>" tag
|
||||
# eventually followed by a "<svg...>" tag or directly starting with a "<svg>" tag
|
||||
if re.search(r"(^\s?(<\?xml[\s\S]*<svg\s)|^\s?<svg\s|^\s?<svg>\s)", image):
|
||||
if "xmlns" not in image:
|
||||
# The xmlns attribute is required for SVGs to render in an img tag.
|
||||
# If it's not present, we add to the first SVG tag:
|
||||
image = image.replace(
|
||||
"<svg", '<svg xmlns="http://www.w3.org/2000/svg" ', 1
|
||||
)
|
||||
# Convert to base64 to prevent issues with encoding:
|
||||
import base64
|
||||
|
||||
image_b64_encoded = base64.b64encode(image.encode("utf-8")).decode("utf-8")
|
||||
# Return SVG as data URI:
|
||||
return f"data:image/svg+xml;base64,{image_b64_encoded}"
|
||||
|
||||
# Otherwise, try to open it as a file.
|
||||
try:
|
||||
with open(image, "rb") as f:
|
||||
image_data = f.read()
|
||||
except Exception:
|
||||
# When we aren't able to open the image file, we still pass the path to
|
||||
# the MediaFileManager - its storage backend may have access to files
|
||||
# that Streamlit does not.
|
||||
import mimetypes
|
||||
|
||||
mimetype, _ = mimetypes.guess_type(image)
|
||||
if mimetype is None:
|
||||
mimetype = "application/octet-stream"
|
||||
|
||||
url = runtime.get_instance().media_file_mgr.add(image, mimetype, image_id)
|
||||
caching.save_media_data(image, mimetype, image_id)
|
||||
return url
|
||||
|
||||
# PIL Images
|
||||
elif isinstance(image, (ImageFile.ImageFile, Image.Image)):
|
||||
format = _validate_image_format_string(image, output_format)
|
||||
image_data = _PIL_to_bytes(image, format)
|
||||
|
||||
# BytesIO
|
||||
# Note: This doesn't support SVG. We could convert to png (cairosvg.svg2png)
|
||||
# or just decode BytesIO to string and handle that way.
|
||||
elif isinstance(image, io.BytesIO):
|
||||
image_data = _BytesIO_to_bytes(image)
|
||||
|
||||
# Numpy Arrays (ie opencv)
|
||||
elif isinstance(image, np.ndarray):
|
||||
image = _clip_image(_verify_np_shape(image), clamp)
|
||||
|
||||
if channels == "BGR":
|
||||
if len(cast("NumpyShape", image.shape)) == 3:
|
||||
image = image[:, :, [2, 1, 0]]
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
'When using `channels="BGR"`, the input image should '
|
||||
"have exactly 3 color channels"
|
||||
)
|
||||
|
||||
image_data = _np_array_to_bytes(array=image, output_format=output_format)
|
||||
|
||||
# Raw bytes
|
||||
else:
|
||||
image_data = image
|
||||
|
||||
# Determine the image's format, resize it, and get its mimetype
|
||||
image_format = _validate_image_format_string(image_data, output_format)
|
||||
image_data = _ensure_image_size_and_format(image_data, width, image_format)
|
||||
mimetype = _get_image_format_mimetype(image_format)
|
||||
|
||||
if runtime.exists():
|
||||
url = runtime.get_instance().media_file_mgr.add(image_data, mimetype, image_id)
|
||||
caching.save_media_data(image_data, mimetype, image_id)
|
||||
return url
|
||||
else:
|
||||
# When running in "raw mode", we can't access the MediaFileManager.
|
||||
return ""
|
||||
|
||||
|
||||
def _4d_to_list_3d(array: npt.NDArray[Any]) -> list[npt.NDArray[Any]]:
|
||||
return [array[i, :, :, :] for i in range(array.shape[0])]
|
||||
|
||||
|
||||
def marshall_images(
|
||||
coordinates: str,
|
||||
image: ImageOrImageList,
|
||||
caption: str | npt.NDArray[Any] | list[str] | None,
|
||||
width: int | WidthBehavior,
|
||||
proto_imgs: ImageListProto,
|
||||
clamp: bool,
|
||||
channels: Channels = "RGB",
|
||||
output_format: ImageFormatOrAuto = "auto",
|
||||
) -> None:
|
||||
"""Fill an ImageListProto with a list of images and their captions.
|
||||
The images will be resized and reformatted as necessary.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
coordinates
|
||||
A string indentifying the images' location in the frontend.
|
||||
image
|
||||
The image or images to include in the ImageListProto.
|
||||
caption
|
||||
Image caption. If displaying multiple images, caption should be a
|
||||
list of captions (one for each image).
|
||||
width
|
||||
The desired width of the image or images. This parameter will be
|
||||
passed to the frontend.
|
||||
Positive values set the image width explicitly.
|
||||
Negative values has some special. For details, see: `WidthBehaviour`
|
||||
proto_imgs
|
||||
The ImageListProto to fill in.
|
||||
clamp
|
||||
Clamp image pixel values to a valid range ([0-255] per channel).
|
||||
This is only meaningful for byte array images; the parameter is
|
||||
ignored for image URLs. If this is not set, and an image has an
|
||||
out-of-range value, an error will be thrown.
|
||||
channels
|
||||
If image is an nd.array, this parameter denotes the format used to
|
||||
represent color information. Defaults to 'RGB', meaning
|
||||
`image[:, :, 0]` is the red channel, `image[:, :, 1]` is green, and
|
||||
`image[:, :, 2]` is blue. For images coming from libraries like
|
||||
OpenCV you should set this to 'BGR', instead.
|
||||
output_format
|
||||
This parameter specifies the format to use when transferring the
|
||||
image data. Photos should use the JPEG format for lossy compression
|
||||
while diagrams should use the PNG format for lossless compression.
|
||||
Defaults to 'auto' which identifies the compression type based
|
||||
on the type and format of the image argument.
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
channels = cast("Channels", channels.upper())
|
||||
|
||||
# Turn single image and caption into one element list.
|
||||
images: Sequence[AtomicImage]
|
||||
if isinstance(image, (list, set, tuple)):
|
||||
images = list(image)
|
||||
elif isinstance(image, np.ndarray) and len(cast("NumpyShape", image.shape)) == 4:
|
||||
images = _4d_to_list_3d(image)
|
||||
else:
|
||||
images = cast("Sequence[AtomicImage]", [image])
|
||||
|
||||
if isinstance(caption, list):
|
||||
captions: Sequence[str | None] = caption
|
||||
elif isinstance(caption, str):
|
||||
captions = [caption]
|
||||
elif (
|
||||
isinstance(caption, np.ndarray) and len(cast("NumpyShape", caption.shape)) == 1
|
||||
):
|
||||
captions = caption.tolist()
|
||||
elif caption is None:
|
||||
captions = [None] * len(images)
|
||||
else:
|
||||
captions = [str(caption)]
|
||||
|
||||
assert isinstance(captions, list), (
|
||||
"If image is a list then caption should be as well"
|
||||
)
|
||||
assert len(captions) == len(images), "Cannot pair %d captions with %d images." % (
|
||||
len(captions),
|
||||
len(images),
|
||||
)
|
||||
|
||||
proto_imgs.width = int(width)
|
||||
# Each image in an image list needs to be kept track of at its own coordinates.
|
||||
for coord_suffix, (image, caption) in enumerate(zip(images, captions)):
|
||||
proto_img = proto_imgs.imgs.add()
|
||||
if caption is not None:
|
||||
proto_img.caption = str(caption)
|
||||
|
||||
# We use the index of the image in the input image list to identify this image inside
|
||||
# MediaFileManager. For this, we just add the index to the image's "coordinates".
|
||||
image_id = "%s-%i" % (coordinates, coord_suffix)
|
||||
|
||||
proto_img.url = image_to_url(
|
||||
image, width, clamp, channels, output_format, image_id
|
||||
)
|
||||
@@ -0,0 +1,105 @@
|
||||
# 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
|
||||
|
||||
|
||||
class JSNumberBoundsException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class JSNumber:
|
||||
"""Utility class for exposing JavaScript Number constants."""
|
||||
|
||||
# The largest int that can be represented with perfect precision
|
||||
# in JavaScript.
|
||||
MAX_SAFE_INTEGER = (1 << 53) - 1
|
||||
|
||||
# The smallest int that can be represented with perfect precision
|
||||
# in JavaScript.
|
||||
MIN_SAFE_INTEGER = -((1 << 53) - 1)
|
||||
|
||||
# The largest float that can be represented in JavaScript.
|
||||
MAX_VALUE = 1.7976931348623157e308
|
||||
|
||||
# The closest number to zero that can be represented in JavaScript.
|
||||
MIN_VALUE = 5e-324
|
||||
|
||||
# The largest negative float that can be represented in JavaScript.
|
||||
MIN_NEGATIVE_VALUE = -MAX_VALUE
|
||||
|
||||
@classmethod
|
||||
def validate_int_bounds(cls, value: int, value_name: str | None = None) -> None:
|
||||
"""Validate that an int value can be represented with perfect precision
|
||||
by a JavaScript Number.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : int
|
||||
value_name : str or None
|
||||
The name of the value parameter. If specified, this will be used
|
||||
in any exception that is thrown.
|
||||
|
||||
Raises
|
||||
------
|
||||
JSNumberBoundsException
|
||||
Raised with a human-readable explanation if the value falls outside
|
||||
JavaScript int bounds.
|
||||
|
||||
"""
|
||||
if value_name is None:
|
||||
value_name = "value"
|
||||
|
||||
if value < cls.MIN_SAFE_INTEGER:
|
||||
raise JSNumberBoundsException(
|
||||
f"{value_name} ({value}) must be >= -((1 << 53) - 1)"
|
||||
)
|
||||
elif value > cls.MAX_SAFE_INTEGER:
|
||||
raise JSNumberBoundsException(
|
||||
f"{value_name} ({value}) must be <= (1 << 53) - 1"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def validate_float_bounds(cls, value: int | float, value_name: str | None) -> None:
|
||||
"""Validate that a float value can be represented by a JavaScript Number.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : float
|
||||
value_name : str or None
|
||||
The name of the value parameter. If specified, this will be used
|
||||
in any exception that is thrown.
|
||||
|
||||
Raises
|
||||
------
|
||||
JSNumberBoundsException
|
||||
Raised with a human-readable explanation if the value falls outside
|
||||
JavaScript float bounds.
|
||||
|
||||
"""
|
||||
if value_name is None:
|
||||
value_name = "value"
|
||||
|
||||
if not isinstance(value, (numbers.Integral, float)):
|
||||
raise JSNumberBoundsException(f"{value_name} ({value}) is not a float")
|
||||
elif value < cls.MIN_NEGATIVE_VALUE:
|
||||
raise JSNumberBoundsException(
|
||||
f"{value_name} ({value}) must be >= -1.797e+308"
|
||||
)
|
||||
elif value > cls.MAX_VALUE:
|
||||
raise JSNumberBoundsException(
|
||||
f"{value_name} ({value}) must be <= 1.797e+308"
|
||||
)
|
||||
@@ -0,0 +1,183 @@
|
||||
# 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 time
|
||||
from typing import TYPE_CHECKING, Literal, cast
|
||||
|
||||
from typing_extensions import Self, TypeAlias
|
||||
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.Block_pb2 import Block as BlockProto
|
||||
from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
|
||||
from streamlit.runtime.scriptrunner_utils.script_run_context import enqueue_message
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types import TracebackType
|
||||
|
||||
from streamlit.cursor import Cursor
|
||||
|
||||
States: TypeAlias = Literal["running", "complete", "error"]
|
||||
|
||||
|
||||
class StatusContainer(DeltaGenerator):
|
||||
@staticmethod
|
||||
def _create(
|
||||
parent: DeltaGenerator,
|
||||
label: str,
|
||||
expanded: bool = False,
|
||||
state: States = "running",
|
||||
) -> StatusContainer:
|
||||
expandable_proto = BlockProto.Expandable()
|
||||
expandable_proto.expanded = expanded
|
||||
expandable_proto.label = label or ""
|
||||
|
||||
if state == "running":
|
||||
expandable_proto.icon = "spinner"
|
||||
elif state == "complete":
|
||||
expandable_proto.icon = ":material/check:"
|
||||
elif state == "error":
|
||||
expandable_proto.icon = ":material/error:"
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
f"Unknown state ({state}). Must be one of 'running', 'complete', or 'error'."
|
||||
)
|
||||
|
||||
block_proto = BlockProto()
|
||||
block_proto.allow_empty = True
|
||||
block_proto.expandable.CopyFrom(expandable_proto)
|
||||
|
||||
delta_path: list[int] = (
|
||||
parent._active_dg._cursor.delta_path if parent._active_dg._cursor else []
|
||||
)
|
||||
|
||||
status_container = cast(
|
||||
"StatusContainer",
|
||||
parent._block(block_proto=block_proto, dg_type=StatusContainer),
|
||||
)
|
||||
|
||||
# Apply initial configuration
|
||||
status_container._delta_path = delta_path
|
||||
status_container._current_proto = block_proto
|
||||
status_container._current_state = state
|
||||
|
||||
# We need to sleep here for a very short time to prevent issues when
|
||||
# the status is updated too quickly. If an .update() directly follows the
|
||||
# the initialization, sometimes only the latest update is applied.
|
||||
# Adding a short timeout here allows the frontend to render the update before.
|
||||
time.sleep(0.05)
|
||||
|
||||
return status_container
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
root_container: int | None,
|
||||
cursor: Cursor | None,
|
||||
parent: DeltaGenerator | None,
|
||||
block_type: str | None,
|
||||
):
|
||||
super().__init__(root_container, cursor, parent, block_type)
|
||||
|
||||
# Initialized in `_create()`:
|
||||
self._current_proto: BlockProto | None = None
|
||||
self._current_state: States | None = None
|
||||
self._delta_path: list[int] | None = None
|
||||
|
||||
def update(
|
||||
self,
|
||||
*,
|
||||
label: str | None = None,
|
||||
expanded: bool | None = None,
|
||||
state: States | None = None,
|
||||
) -> None:
|
||||
"""Update the status container.
|
||||
|
||||
Only specified arguments are updated. Container contents and unspecified
|
||||
arguments remain unchanged.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : str or None
|
||||
A new label of the status container. If None, the label is not
|
||||
changed.
|
||||
|
||||
expanded : bool or None
|
||||
The new expanded state of the status container. If None,
|
||||
the expanded state is not changed.
|
||||
|
||||
state : "running", "complete", "error", or None
|
||||
The new state of the status container. This mainly changes the
|
||||
icon. If None, the state is not changed.
|
||||
"""
|
||||
assert self._current_proto is not None, "Status not correctly initialized!"
|
||||
assert self._delta_path is not None, "Status not correctly initialized!"
|
||||
|
||||
msg = ForwardMsg()
|
||||
msg.metadata.delta_path[:] = self._delta_path
|
||||
msg.delta.add_block.CopyFrom(self._current_proto)
|
||||
|
||||
if expanded is not None:
|
||||
msg.delta.add_block.expandable.expanded = expanded
|
||||
else:
|
||||
msg.delta.add_block.expandable.ClearField("expanded")
|
||||
|
||||
if label is not None:
|
||||
msg.delta.add_block.expandable.label = label
|
||||
|
||||
if state is not None:
|
||||
if state == "running":
|
||||
msg.delta.add_block.expandable.icon = "spinner"
|
||||
elif state == "complete":
|
||||
msg.delta.add_block.expandable.icon = ":material/check:"
|
||||
elif state == "error":
|
||||
msg.delta.add_block.expandable.icon = ":material/error:"
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
f"Unknown state ({state}). Must be one of 'running', 'complete', or 'error'."
|
||||
)
|
||||
self._current_state = state
|
||||
|
||||
self._current_proto = msg.delta.add_block
|
||||
enqueue_message(msg)
|
||||
|
||||
def __enter__(self) -> Self: # type: ignore[override]
|
||||
# This is a little dubious: we're returning a different type than
|
||||
# our superclass' `__enter__` function. Maybe DeltaGenerator.__enter__
|
||||
# should always return `self`?
|
||||
super().__enter__()
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> Literal[False]:
|
||||
# Only update if the current state is running
|
||||
if self._current_state == "running":
|
||||
# We need to sleep here for a very short time to prevent issues when
|
||||
# the status is updated too quickly. If an .update() is directly followed
|
||||
# by the exit of the context manager, sometimes only the last update
|
||||
# (to complete) is applied. Adding a short timeout here allows the frontend
|
||||
# to render the update before.
|
||||
time.sleep(0.05)
|
||||
if exc_type is not None:
|
||||
# If an exception was raised in the context,
|
||||
# we want to update the status to error.
|
||||
self.update(state="error")
|
||||
else:
|
||||
self.update(state="complete")
|
||||
return super().__exit__(exc_type, exc_val, exc_tb)
|
||||
@@ -0,0 +1,253 @@
|
||||
# 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 enum import Enum, EnumMeta
|
||||
from typing import TYPE_CHECKING, Any, Final, TypeVar, overload
|
||||
|
||||
from streamlit import config, logger
|
||||
from streamlit.dataframe_util import OptionSequence, convert_anything_to_list
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.runtime.state.common import RegisterWidgetResult
|
||||
from streamlit.type_util import (
|
||||
T,
|
||||
check_python_comparable,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable, Sequence
|
||||
|
||||
_LOGGER: Final = logger.get_logger(__name__)
|
||||
|
||||
_FLOAT_EQUALITY_EPSILON: Final[float] = 0.000000000005
|
||||
_Value = TypeVar("_Value")
|
||||
|
||||
|
||||
def index_(iterable: Iterable[_Value], x: _Value) -> int:
|
||||
"""Return zero-based index of the first item whose value is equal to x.
|
||||
Raises a ValueError if there is no such item.
|
||||
|
||||
We need a custom implementation instead of the built-in list .index() to
|
||||
be compatible with NumPy array and Pandas Series.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
iterable : list, tuple, numpy.ndarray, pandas.Series
|
||||
x : Any
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
"""
|
||||
for i, value in enumerate(iterable):
|
||||
if x == value:
|
||||
return i
|
||||
elif isinstance(value, float) and isinstance(x, float):
|
||||
if abs(x - value) < _FLOAT_EQUALITY_EPSILON:
|
||||
return i
|
||||
raise ValueError(f"{str(x)} is not in iterable")
|
||||
|
||||
|
||||
def check_and_convert_to_indices(
|
||||
opt: Sequence[Any], default_values: Sequence[Any] | Any | None
|
||||
) -> list[int] | None:
|
||||
"""Perform validation checks and return indices based on the default values."""
|
||||
if default_values is None:
|
||||
return None
|
||||
|
||||
default_values = convert_anything_to_list(default_values)
|
||||
|
||||
for value in default_values:
|
||||
if value not in opt:
|
||||
raise StreamlitAPIException(
|
||||
f"The default value '{value}' is not part of the options. "
|
||||
"Please make sure that every default values also exists in the options."
|
||||
)
|
||||
|
||||
return [opt.index(value) for value in default_values]
|
||||
|
||||
|
||||
def convert_to_sequence_and_check_comparable(options: OptionSequence[T]) -> Sequence[T]:
|
||||
indexable_options = convert_anything_to_list(options)
|
||||
check_python_comparable(indexable_options)
|
||||
return indexable_options
|
||||
|
||||
|
||||
def get_default_indices(
|
||||
indexable_options: Sequence[T], default: Sequence[Any] | Any | None = None
|
||||
) -> list[int]:
|
||||
default_indices = check_and_convert_to_indices(indexable_options, default)
|
||||
default_indices = default_indices if default_indices is not None else []
|
||||
return default_indices
|
||||
|
||||
|
||||
E1 = TypeVar("E1", bound=Enum)
|
||||
E2 = TypeVar("E2", bound=Enum)
|
||||
|
||||
_ALLOWED_ENUM_COERCION_CONFIG_SETTINGS = ("off", "nameOnly", "nameAndValue")
|
||||
|
||||
|
||||
def _coerce_enum(from_enum_value: E1, to_enum_class: type[E2]) -> E1 | E2:
|
||||
"""Attempt to coerce an Enum value to another EnumMeta.
|
||||
|
||||
An Enum value of EnumMeta E1 is considered coercable to EnumType E2
|
||||
if the EnumMeta __qualname__ match and the names of their members
|
||||
match as well. (This is configurable in streamlist configs)
|
||||
"""
|
||||
if not isinstance(from_enum_value, Enum):
|
||||
raise ValueError(
|
||||
f"Expected an Enum in the first argument. Got {type(from_enum_value)}"
|
||||
)
|
||||
if not isinstance(to_enum_class, EnumMeta):
|
||||
raise ValueError(
|
||||
f"Expected an EnumMeta/Type in the second argument. Got {type(to_enum_class)}"
|
||||
)
|
||||
if isinstance(from_enum_value, to_enum_class):
|
||||
return from_enum_value # Enum is already a member, no coersion necessary
|
||||
|
||||
coercion_type = config.get_option("runner.enumCoercion")
|
||||
if coercion_type not in _ALLOWED_ENUM_COERCION_CONFIG_SETTINGS:
|
||||
raise StreamlitAPIException(
|
||||
"Invalid value for config option runner.enumCoercion. "
|
||||
f"Expected one of {_ALLOWED_ENUM_COERCION_CONFIG_SETTINGS}, "
|
||||
f"but got '{coercion_type}'."
|
||||
)
|
||||
if coercion_type == "off":
|
||||
return from_enum_value # do not attempt to coerce
|
||||
|
||||
# We now know this is an Enum AND the user has configured coercion enabled.
|
||||
# Check if we do NOT meet the required conditions and log a failure message
|
||||
# if that is the case.
|
||||
from_enum_class = from_enum_value.__class__
|
||||
if (
|
||||
from_enum_class.__qualname__ != to_enum_class.__qualname__
|
||||
or (
|
||||
coercion_type == "nameOnly"
|
||||
and set(to_enum_class._member_names_) != set(from_enum_class._member_names_)
|
||||
)
|
||||
or (
|
||||
coercion_type == "nameAndValue"
|
||||
and set(to_enum_class._value2member_map_)
|
||||
!= set(from_enum_class._value2member_map_)
|
||||
)
|
||||
):
|
||||
_LOGGER.debug("Failed to coerce %s to class %s", from_enum_value, to_enum_class)
|
||||
return from_enum_value # do not attempt to coerce
|
||||
|
||||
# At this point we think the Enum is coercable, and we know
|
||||
# E1 and E2 have the same member names. We convert from E1 to E2 using _name_
|
||||
# (since user Enum subclasses can override the .name property in 3.11)
|
||||
_LOGGER.debug("Coerced %s to class %s", from_enum_value, to_enum_class)
|
||||
return to_enum_class[from_enum_value._name_]
|
||||
|
||||
|
||||
def _extract_common_class_from_iter(iterable: Iterable[Any]) -> Any:
|
||||
"""Return the common class of all elements in a iterable if they share one.
|
||||
Otherwise, return None.
|
||||
"""
|
||||
try:
|
||||
inner_iter = iter(iterable)
|
||||
first_class = type(next(inner_iter))
|
||||
except StopIteration:
|
||||
return None
|
||||
if all(type(item) is first_class for item in inner_iter):
|
||||
return first_class
|
||||
return None
|
||||
|
||||
|
||||
@overload
|
||||
def maybe_coerce_enum(
|
||||
register_widget_result: RegisterWidgetResult[Enum],
|
||||
options: type[Enum],
|
||||
opt_sequence: Sequence[Any],
|
||||
) -> RegisterWidgetResult[Enum]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def maybe_coerce_enum(
|
||||
register_widget_result: RegisterWidgetResult[T],
|
||||
options: OptionSequence[T],
|
||||
opt_sequence: Sequence[T],
|
||||
) -> RegisterWidgetResult[T]: ...
|
||||
|
||||
|
||||
def maybe_coerce_enum(register_widget_result, options, opt_sequence):
|
||||
"""Maybe Coerce a RegisterWidgetResult with an Enum member value to
|
||||
RegisterWidgetResult[option] if option is an EnumType, otherwise just return
|
||||
the original RegisterWidgetResult.
|
||||
"""
|
||||
|
||||
# If the value is not a Enum, return early
|
||||
if not isinstance(register_widget_result.value, Enum):
|
||||
return register_widget_result
|
||||
|
||||
coerce_class: EnumMeta | None
|
||||
if isinstance(options, EnumMeta):
|
||||
coerce_class = options
|
||||
else:
|
||||
coerce_class = _extract_common_class_from_iter(opt_sequence)
|
||||
if coerce_class is None:
|
||||
return register_widget_result
|
||||
|
||||
return RegisterWidgetResult(
|
||||
_coerce_enum(register_widget_result.value, coerce_class),
|
||||
register_widget_result.value_changed,
|
||||
)
|
||||
|
||||
|
||||
# slightly ugly typing because TypeVars with Generic Bounds are not supported
|
||||
# (https://github.com/python/typing/issues/548)
|
||||
@overload
|
||||
def maybe_coerce_enum_sequence(
|
||||
register_widget_result: RegisterWidgetResult[list[T]],
|
||||
options: OptionSequence[T],
|
||||
opt_sequence: Sequence[T],
|
||||
) -> RegisterWidgetResult[list[T]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def maybe_coerce_enum_sequence(
|
||||
register_widget_result: RegisterWidgetResult[tuple[T, T]],
|
||||
options: OptionSequence[T],
|
||||
opt_sequence: Sequence[T],
|
||||
) -> RegisterWidgetResult[tuple[T, T]]: ...
|
||||
|
||||
|
||||
def maybe_coerce_enum_sequence(register_widget_result, options, opt_sequence):
|
||||
"""Maybe Coerce a RegisterWidgetResult with a sequence of Enum members as value
|
||||
to RegisterWidgetResult[Sequence[option]] if option is an EnumType, otherwise just return
|
||||
the original RegisterWidgetResult.
|
||||
"""
|
||||
|
||||
# If not all widget values are Enums, return early
|
||||
if not all(isinstance(val, Enum) for val in register_widget_result.value):
|
||||
return register_widget_result
|
||||
|
||||
# Extract the class to coerce
|
||||
coerce_class: EnumMeta | None
|
||||
if isinstance(options, EnumMeta):
|
||||
coerce_class = options
|
||||
else:
|
||||
coerce_class = _extract_common_class_from_iter(opt_sequence)
|
||||
if coerce_class is None:
|
||||
return register_widget_result
|
||||
|
||||
# Return a new RegisterWidgetResult with the coerced enum values sequence
|
||||
return RegisterWidgetResult(
|
||||
type(register_widget_result.value)(
|
||||
_coerce_enum(val, coerce_class) for val in register_widget_result.value
|
||||
),
|
||||
register_widget_result.value_changed,
|
||||
)
|
||||
@@ -0,0 +1,274 @@
|
||||
# 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 Mapping
|
||||
from typing import TYPE_CHECKING, Any, TypeVar
|
||||
|
||||
from streamlit import dataframe_util
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pandas import DataFrame
|
||||
from pandas.io.formats.style import Styler
|
||||
|
||||
from streamlit.proto.Arrow_pb2 import Arrow as ArrowProto
|
||||
|
||||
|
||||
def marshall_styler(proto: ArrowProto, styler: Styler, default_uuid: str) -> None:
|
||||
"""Marshall pandas.Styler into an Arrow proto.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
proto : proto.Arrow
|
||||
Output. The protobuf for Streamlit Arrow proto.
|
||||
|
||||
styler : pandas.Styler
|
||||
Helps style a DataFrame or Series according to the data with HTML and CSS.
|
||||
|
||||
default_uuid : str
|
||||
If pandas.Styler uuid is not provided, this value will be used.
|
||||
|
||||
"""
|
||||
import pandas as pd
|
||||
|
||||
styler_data_df: pd.DataFrame = styler.data
|
||||
if styler_data_df.size > int(pd.options.styler.render.max_elements):
|
||||
raise StreamlitAPIException(
|
||||
f"The dataframe has `{styler_data_df.size}` cells, but the maximum number "
|
||||
"of cells allowed to be rendered by Pandas Styler is configured to "
|
||||
f"`{pd.options.styler.render.max_elements}`. To allow more cells to be "
|
||||
'styled, you can change the `"styler.render.max_elements"` config. For example: '
|
||||
f'`pd.set_option("styler.render.max_elements", {styler_data_df.size})`'
|
||||
)
|
||||
|
||||
# pandas.Styler uuid should be set before _compute is called.
|
||||
_marshall_uuid(proto, styler, default_uuid)
|
||||
|
||||
# We're using protected members of pandas.Styler to get styles,
|
||||
# which is not ideal and could break if the interface changes.
|
||||
styler._compute()
|
||||
|
||||
pandas_styles = styler._translate(False, False)
|
||||
|
||||
_marshall_caption(proto, styler)
|
||||
_marshall_styles(proto, styler, pandas_styles)
|
||||
_marshall_display_values(proto, styler_data_df, pandas_styles)
|
||||
|
||||
|
||||
def _marshall_uuid(proto: ArrowProto, styler: Styler, default_uuid: str) -> None:
|
||||
"""Marshall pandas.Styler uuid into an Arrow proto.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
proto : proto.Arrow
|
||||
Output. The protobuf for Streamlit Arrow proto.
|
||||
|
||||
styler : pandas.Styler
|
||||
Helps style a DataFrame or Series according to the data with HTML and CSS.
|
||||
|
||||
default_uuid : str
|
||||
If pandas.Styler uuid is not provided, this value will be used.
|
||||
|
||||
"""
|
||||
if styler.uuid is None:
|
||||
styler.set_uuid(default_uuid)
|
||||
|
||||
proto.styler.uuid = str(styler.uuid)
|
||||
|
||||
|
||||
def _marshall_caption(proto: ArrowProto, styler: Styler) -> None:
|
||||
"""Marshall pandas.Styler caption into an Arrow proto.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
proto : proto.Arrow
|
||||
Output. The protobuf for Streamlit Arrow proto.
|
||||
|
||||
styler : pandas.Styler
|
||||
Helps style a DataFrame or Series according to the data with HTML and CSS.
|
||||
|
||||
"""
|
||||
if styler.caption is not None:
|
||||
proto.styler.caption = styler.caption
|
||||
|
||||
|
||||
def _marshall_styles(
|
||||
proto: ArrowProto, styler: Styler, styles: Mapping[str, Any]
|
||||
) -> None:
|
||||
"""Marshall pandas.Styler styles into an Arrow proto.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
proto : proto.Arrow
|
||||
Output. The protobuf for Streamlit Arrow proto.
|
||||
|
||||
styler : pandas.Styler
|
||||
Helps style a DataFrame or Series according to the data with HTML and CSS.
|
||||
|
||||
styles : dict
|
||||
pandas.Styler translated styles.
|
||||
|
||||
"""
|
||||
css_rules = []
|
||||
|
||||
if "table_styles" in styles:
|
||||
table_styles = styles["table_styles"]
|
||||
table_styles = _trim_pandas_styles(table_styles)
|
||||
for style in table_styles:
|
||||
# styles in "table_styles" have a space
|
||||
# between the uuid and selector.
|
||||
rule = _pandas_style_to_css(
|
||||
"table_styles", style, styler.uuid, separator=" "
|
||||
)
|
||||
css_rules.append(rule)
|
||||
|
||||
if "cellstyle" in styles:
|
||||
cellstyle = styles["cellstyle"]
|
||||
cellstyle = _trim_pandas_styles(cellstyle)
|
||||
for style in cellstyle:
|
||||
rule = _pandas_style_to_css("cell_style", style, styler.uuid, separator="_")
|
||||
css_rules.append(rule)
|
||||
|
||||
if len(css_rules) > 0:
|
||||
proto.styler.styles = "\n".join(css_rules)
|
||||
|
||||
|
||||
M = TypeVar("M", bound=Mapping[str, Any])
|
||||
|
||||
|
||||
def _trim_pandas_styles(styles: list[M]) -> list[M]:
|
||||
"""Filter out empty styles.
|
||||
|
||||
Every cell will have a class, but the list of props
|
||||
may just be [['', '']].
|
||||
|
||||
Parameters
|
||||
----------
|
||||
styles : list
|
||||
pandas.Styler translated styles.
|
||||
|
||||
"""
|
||||
return [x for x in styles if any(any(y) for y in x["props"])]
|
||||
|
||||
|
||||
def _pandas_style_to_css(
|
||||
style_type: str,
|
||||
style: Mapping[str, Any],
|
||||
uuid: str,
|
||||
separator: str = "_",
|
||||
) -> str:
|
||||
"""Convert pandas.Styler translated style to CSS.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
style_type : str
|
||||
Either "table_styles" or "cell_style".
|
||||
|
||||
style : dict
|
||||
pandas.Styler translated style.
|
||||
|
||||
uuid : str
|
||||
pandas.Styler uuid.
|
||||
|
||||
separator : str
|
||||
A string separator used between table and cell selectors.
|
||||
|
||||
"""
|
||||
declarations = []
|
||||
for css_property, css_value in style["props"]:
|
||||
declaration = str(css_property).strip() + ": " + str(css_value).strip()
|
||||
declarations.append(declaration)
|
||||
|
||||
table_selector = f"#T_{uuid}"
|
||||
|
||||
# In pandas >= 1.1.0
|
||||
# translated_style["cellstyle"] has the following shape:
|
||||
# [
|
||||
# {
|
||||
# "props": [("color", " black"), ("background-color", "orange"), ("", "")],
|
||||
# "selectors": ["row0_col0"]
|
||||
# }
|
||||
# ...
|
||||
# ]
|
||||
if style_type == "table_styles":
|
||||
cell_selectors = [style["selector"]]
|
||||
else:
|
||||
cell_selectors = style["selectors"]
|
||||
|
||||
selectors = []
|
||||
for cell_selector in cell_selectors:
|
||||
selectors.append(table_selector + separator + cell_selector)
|
||||
selector = ", ".join(selectors)
|
||||
|
||||
declaration_block = "; ".join(declarations)
|
||||
rule_set = selector + " { " + declaration_block + " }"
|
||||
|
||||
return rule_set
|
||||
|
||||
|
||||
def _marshall_display_values(
|
||||
proto: ArrowProto, df: DataFrame, styles: Mapping[str, Any]
|
||||
) -> None:
|
||||
"""Marshall pandas.Styler display values into an Arrow proto.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
proto : proto.Arrow
|
||||
Output. The protobuf for Streamlit Arrow proto.
|
||||
|
||||
df : pandas.DataFrame
|
||||
A dataframe with original values.
|
||||
|
||||
styles : dict
|
||||
pandas.Styler translated styles.
|
||||
|
||||
"""
|
||||
new_df = _use_display_values(df, styles)
|
||||
proto.styler.display_values = dataframe_util.convert_pandas_df_to_arrow_bytes(
|
||||
new_df
|
||||
)
|
||||
|
||||
|
||||
def _use_display_values(df: DataFrame, styles: Mapping[str, Any]) -> DataFrame:
|
||||
"""Create a new pandas.DataFrame where display values are used instead of original ones.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
df : pandas.DataFrame
|
||||
A dataframe with original values.
|
||||
|
||||
styles : dict
|
||||
pandas.Styler translated styles.
|
||||
|
||||
"""
|
||||
import re
|
||||
|
||||
# If values in a column are not of the same type, Arrow
|
||||
# serialization would fail. Thus, we need to cast all values
|
||||
# of the dataframe to strings before assigning them display values.
|
||||
new_df = df.astype(str)
|
||||
|
||||
cell_selector_regex = re.compile(r"row(\d+)_col(\d+)")
|
||||
if "body" in styles:
|
||||
rows = styles["body"]
|
||||
for row in rows:
|
||||
for cell in row:
|
||||
if "id" in cell:
|
||||
if match := cell_selector_regex.match(cell["id"]):
|
||||
r, c = map(int, match.groups())
|
||||
new_df.iloc[r, c] = str(cell["display_value"])
|
||||
|
||||
return new_df
|
||||
@@ -0,0 +1,194 @@
|
||||
# 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 typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
from streamlit import config, errors, logger, runtime
|
||||
from streamlit.elements.lib.form_utils import is_in_form
|
||||
from streamlit.errors import (
|
||||
StreamlitAPIWarning,
|
||||
StreamlitFragmentWidgetsNotAllowedOutsideError,
|
||||
StreamlitInvalidFormCallbackError,
|
||||
StreamlitValueAssignmentNotAllowedError,
|
||||
)
|
||||
from streamlit.runtime.scriptrunner_utils.script_run_context import (
|
||||
get_script_run_ctx,
|
||||
in_cached_function,
|
||||
)
|
||||
from streamlit.runtime.state import WidgetCallback, get_session_state
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
_LOGGER: Final = logger.get_logger(__name__)
|
||||
|
||||
|
||||
def check_callback_rules(dg: DeltaGenerator, on_change: WidgetCallback | None) -> None:
|
||||
"""Ensures that widgets other than `st.form_submit_button` within a form don't have
|
||||
an on_change callback set.
|
||||
|
||||
Raises
|
||||
------
|
||||
StreamlitInvalidFormCallbackError:
|
||||
Raised when the described rule is violated.
|
||||
"""
|
||||
|
||||
if runtime.exists() and is_in_form(dg) and on_change is not None:
|
||||
raise StreamlitInvalidFormCallbackError()
|
||||
|
||||
|
||||
_shown_default_value_warning: bool = False
|
||||
|
||||
|
||||
def check_session_state_rules(
|
||||
default_value: Any, key: str | None, writes_allowed: bool = True
|
||||
) -> None:
|
||||
"""Ensures that no values are set for widgets with the given key when writing
|
||||
is not allowed.
|
||||
|
||||
Additionally, if `global.disableWidgetStateDuplicationWarning` is False a warning is
|
||||
shown when a widget has a default value but its value is also set via session state.
|
||||
|
||||
Raises
|
||||
------
|
||||
StreamlitAPIException:
|
||||
Raised when the described rule is violated.
|
||||
"""
|
||||
global _shown_default_value_warning
|
||||
|
||||
if key is None or not runtime.exists():
|
||||
return
|
||||
|
||||
session_state = get_session_state()
|
||||
if not session_state.is_new_state_value(key):
|
||||
return
|
||||
|
||||
if not writes_allowed:
|
||||
raise StreamlitValueAssignmentNotAllowedError(key=key)
|
||||
|
||||
if (
|
||||
default_value is not None
|
||||
and not _shown_default_value_warning
|
||||
and not config.get_option("global.disableWidgetStateDuplicationWarning")
|
||||
):
|
||||
from streamlit import warning
|
||||
|
||||
warning(
|
||||
f'The widget with key "{key}" was created with a default value but'
|
||||
" also had its value set via the Session State API."
|
||||
)
|
||||
_shown_default_value_warning = True
|
||||
|
||||
|
||||
class CachedWidgetWarning(StreamlitAPIWarning):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
"""
|
||||
Your script uses a widget command in a cached function
|
||||
(function decorated with `@st.cache_data` or `@st.cache_resource`).
|
||||
This code will only be called when we detect a cache "miss",
|
||||
which can lead to unexpected results.
|
||||
|
||||
To fix this, move all widget commands outside the cached function.
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def check_cache_replay_rules() -> None:
|
||||
"""Check if a widget is allowed to be used in the current context.
|
||||
More specifically, this checks if the current context is inside a
|
||||
cached function that disallows widget usage. If so, it raises a warning.
|
||||
|
||||
If there are other similar checks in the future, we could extend this
|
||||
function to check for those as well. And rename it to check_widget_usage_rules.
|
||||
"""
|
||||
if in_cached_function.get():
|
||||
from streamlit import exception
|
||||
|
||||
# We use an exception here to show a proper stack trace
|
||||
# that indicates to the user where the issue is.
|
||||
exception(CachedWidgetWarning())
|
||||
|
||||
|
||||
def check_fragment_path_policy(dg: DeltaGenerator):
|
||||
"""Ensures that the current widget is not written outside of the
|
||||
fragment's delta path.
|
||||
|
||||
Should be called by ever element that acts as a widget.
|
||||
We don't allow writing widgets from within a widget to the outside path
|
||||
because it can lead to unexpected behavior. For elements, this is okay
|
||||
because they do not trigger a re-run.
|
||||
"""
|
||||
|
||||
ctx = get_script_run_ctx()
|
||||
# Check is only relevant for fragments
|
||||
if ctx is None or ctx.current_fragment_id is None:
|
||||
return
|
||||
|
||||
current_fragment_delta_path = ctx.current_fragment_delta_path
|
||||
current_cursor = dg._active_dg._cursor
|
||||
if current_cursor is None:
|
||||
return
|
||||
|
||||
current_cursor_delta_path = current_cursor.delta_path
|
||||
|
||||
# the elements delta path cannot be smaller than the fragment's delta path if it is
|
||||
# inside of the fragment
|
||||
if len(current_cursor_delta_path) < len(current_fragment_delta_path):
|
||||
raise StreamlitFragmentWidgetsNotAllowedOutsideError()
|
||||
|
||||
# all path indices of the fragment-path must occur in the inner-elements delta path,
|
||||
# otherwise it is outside of the fragment container
|
||||
for index, path_index in enumerate(current_fragment_delta_path):
|
||||
if current_cursor_delta_path[index] != path_index:
|
||||
raise StreamlitFragmentWidgetsNotAllowedOutsideError()
|
||||
|
||||
|
||||
def check_widget_policies(
|
||||
dg: DeltaGenerator,
|
||||
key: str | None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
*,
|
||||
default_value: Sequence[Any] | Any | None = None,
|
||||
writes_allowed: bool = True,
|
||||
enable_check_callback_rules: bool = True,
|
||||
):
|
||||
"""Check all widget policies for the given DeltaGenerator."""
|
||||
check_fragment_path_policy(dg)
|
||||
check_cache_replay_rules()
|
||||
if enable_check_callback_rules:
|
||||
check_callback_rules(dg, on_change)
|
||||
check_session_state_rules(
|
||||
default_value=default_value, key=key, writes_allowed=writes_allowed
|
||||
)
|
||||
|
||||
|
||||
def maybe_raise_label_warnings(label: str | None, label_visibility: str | None):
|
||||
if not label:
|
||||
_LOGGER.warning(
|
||||
"`label` got an empty value. This is discouraged for accessibility "
|
||||
"reasons and may be disallowed in the future by raising an exception. "
|
||||
"Please provide a non-empty label and hide it with label_visibility "
|
||||
"if needed."
|
||||
)
|
||||
if label_visibility not in ("visible", "hidden", "collapsed"):
|
||||
raise errors.StreamlitAPIException(
|
||||
f"Unsupported label_visibility option '{label_visibility}'. "
|
||||
f"Valid values are 'visible', 'hidden' or 'collapsed'."
|
||||
)
|
||||
@@ -0,0 +1,207 @@
|
||||
# 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 contextlib
|
||||
|
||||
|
||||
def configure_streamlit_plotly_theme() -> None:
|
||||
"""Configure the Streamlit chart theme for Plotly.
|
||||
|
||||
The theme is only configured if Plotly is installed.
|
||||
"""
|
||||
# We do nothing if Plotly is not installed. This is expected since Plotly is an optional dependency.
|
||||
with contextlib.suppress(ImportError):
|
||||
import plotly.graph_objects as go
|
||||
import plotly.io as pio
|
||||
|
||||
# This is the streamlit theme for plotly where we pass in a template.data
|
||||
# and a template.layout.
|
||||
# Template.data is for changing specific graph properties in a general aspect
|
||||
# such as Contour plots or Waterfall plots.
|
||||
# Template.layout is for changing things such as the x axis and fonts and other
|
||||
# general layout properties for general graphs.
|
||||
# We pass in temporary colors to the frontend and the frontend will replace
|
||||
# those colors because we want to change colors based on the background color.
|
||||
# Start at #0000001 because developers may be likely to use #000000
|
||||
CATEGORY_0 = "#000001"
|
||||
CATEGORY_1 = "#000002"
|
||||
CATEGORY_2 = "#000003"
|
||||
CATEGORY_3 = "#000004"
|
||||
CATEGORY_4 = "#000005"
|
||||
CATEGORY_5 = "#000006"
|
||||
CATEGORY_6 = "#000007"
|
||||
CATEGORY_7 = "#000008"
|
||||
CATEGORY_8 = "#000009"
|
||||
CATEGORY_9 = "#000010"
|
||||
|
||||
SEQUENTIAL_0 = "#000011"
|
||||
SEQUENTIAL_1 = "#000012"
|
||||
SEQUENTIAL_2 = "#000013"
|
||||
SEQUENTIAL_3 = "#000014"
|
||||
SEQUENTIAL_4 = "#000015"
|
||||
SEQUENTIAL_5 = "#000016"
|
||||
SEQUENTIAL_6 = "#000017"
|
||||
SEQUENTIAL_7 = "#000018"
|
||||
SEQUENTIAL_8 = "#000019"
|
||||
SEQUENTIAL_9 = "#000020"
|
||||
|
||||
DIVERGING_0 = "#000021"
|
||||
DIVERGING_1 = "#000022"
|
||||
DIVERGING_2 = "#000023"
|
||||
DIVERGING_3 = "#000024"
|
||||
DIVERGING_4 = "#000025"
|
||||
DIVERGING_5 = "#000026"
|
||||
DIVERGING_6 = "#000027"
|
||||
DIVERGING_7 = "#000028"
|
||||
DIVERGING_8 = "#000029"
|
||||
DIVERGING_9 = "#000030"
|
||||
DIVERGING_10 = "#000031"
|
||||
|
||||
INCREASING = "#000032"
|
||||
DECREASING = "#000033"
|
||||
TOTAL = "#000034"
|
||||
|
||||
GRAY_70 = "#000036"
|
||||
GRAY_90 = "#000037"
|
||||
BG_COLOR = "#000038"
|
||||
FADED_TEXT_05 = "#000039"
|
||||
BG_MIX = "#000040"
|
||||
|
||||
# Plotly represents continuous colorscale through an array of pairs.
|
||||
# The pair's first index is the starting point and the next pair's first index is the end point.
|
||||
# The pair's second index is the starting color and the next pair's second index is the end color.
|
||||
# For more information, please refer to https://plotly.com/python/colorscales/
|
||||
|
||||
streamlit_colorscale = [
|
||||
[0.0, SEQUENTIAL_0],
|
||||
[0.1111111111111111, SEQUENTIAL_1],
|
||||
[0.2222222222222222, SEQUENTIAL_2],
|
||||
[0.3333333333333333, SEQUENTIAL_3],
|
||||
[0.4444444444444444, SEQUENTIAL_4],
|
||||
[0.5555555555555556, SEQUENTIAL_5],
|
||||
[0.6666666666666666, SEQUENTIAL_6],
|
||||
[0.7777777777777778, SEQUENTIAL_7],
|
||||
[0.8888888888888888, SEQUENTIAL_8],
|
||||
[1.0, SEQUENTIAL_9],
|
||||
]
|
||||
|
||||
pio.templates["streamlit"] = go.layout.Template(
|
||||
data=go.layout.template.Data(
|
||||
candlestick=[
|
||||
go.layout.template.data.Candlestick(
|
||||
decreasing=go.candlestick.Decreasing(
|
||||
line=go.candlestick.decreasing.Line(color=DECREASING)
|
||||
),
|
||||
increasing=go.candlestick.Increasing(
|
||||
line=go.candlestick.increasing.Line(color=INCREASING)
|
||||
),
|
||||
)
|
||||
],
|
||||
contour=[
|
||||
go.layout.template.data.Contour(colorscale=streamlit_colorscale)
|
||||
],
|
||||
contourcarpet=[
|
||||
go.layout.template.data.Contourcarpet(
|
||||
colorscale=streamlit_colorscale
|
||||
)
|
||||
],
|
||||
heatmap=[
|
||||
go.layout.template.data.Heatmap(colorscale=streamlit_colorscale)
|
||||
],
|
||||
histogram2d=[
|
||||
go.layout.template.data.Histogram2d(colorscale=streamlit_colorscale)
|
||||
],
|
||||
icicle=[
|
||||
go.layout.template.data.Icicle(
|
||||
textfont=go.icicle.Textfont(color="white")
|
||||
)
|
||||
],
|
||||
sankey=[
|
||||
go.layout.template.data.Sankey(
|
||||
textfont=go.sankey.Textfont(color=GRAY_70)
|
||||
)
|
||||
],
|
||||
scatter=[
|
||||
go.layout.template.data.Scatter(
|
||||
marker=go.scatter.Marker(line=go.scatter.marker.Line(width=0))
|
||||
)
|
||||
],
|
||||
table=[
|
||||
go.layout.template.data.Table(
|
||||
cells=go.table.Cells(
|
||||
fill=go.table.cells.Fill(color=BG_COLOR),
|
||||
font=go.table.cells.Font(color=GRAY_90),
|
||||
line=go.table.cells.Line(color=FADED_TEXT_05),
|
||||
),
|
||||
header=go.table.Header(
|
||||
font=go.table.header.Font(color=GRAY_70),
|
||||
line=go.table.header.Line(color=FADED_TEXT_05),
|
||||
fill=go.table.header.Fill(color=BG_MIX),
|
||||
),
|
||||
)
|
||||
],
|
||||
waterfall=[
|
||||
go.layout.template.data.Waterfall(
|
||||
increasing=go.waterfall.Increasing(
|
||||
marker=go.waterfall.increasing.Marker(color=INCREASING)
|
||||
),
|
||||
decreasing=go.waterfall.Decreasing(
|
||||
marker=go.waterfall.decreasing.Marker(color=DECREASING)
|
||||
),
|
||||
totals=go.waterfall.Totals(
|
||||
marker=go.waterfall.totals.Marker(color=TOTAL)
|
||||
),
|
||||
connector=go.waterfall.Connector(
|
||||
line=go.waterfall.connector.Line(color=GRAY_70, width=2)
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
layout=go.Layout(
|
||||
colorway=[
|
||||
CATEGORY_0,
|
||||
CATEGORY_1,
|
||||
CATEGORY_2,
|
||||
CATEGORY_3,
|
||||
CATEGORY_4,
|
||||
CATEGORY_5,
|
||||
CATEGORY_6,
|
||||
CATEGORY_7,
|
||||
CATEGORY_8,
|
||||
CATEGORY_9,
|
||||
],
|
||||
colorscale=go.layout.Colorscale(
|
||||
sequential=streamlit_colorscale,
|
||||
sequentialminus=streamlit_colorscale,
|
||||
diverging=[
|
||||
[0.0, DIVERGING_0],
|
||||
[0.1, DIVERGING_1],
|
||||
[0.2, DIVERGING_2],
|
||||
[0.3, DIVERGING_3],
|
||||
[0.4, DIVERGING_4],
|
||||
[0.5, DIVERGING_5],
|
||||
[0.6, DIVERGING_6],
|
||||
[0.7, DIVERGING_7],
|
||||
[0.8, DIVERGING_8],
|
||||
[0.9, DIVERGING_9],
|
||||
[1.0, DIVERGING_10],
|
||||
],
|
||||
),
|
||||
coloraxis=go.layout.Coloraxis(colorscale=streamlit_colorscale),
|
||||
),
|
||||
)
|
||||
|
||||
pio.templates.default = "streamlit"
|
||||
@@ -0,0 +1,178 @@
|
||||
# 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 io
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from streamlit import runtime
|
||||
from streamlit.runtime import caching
|
||||
from streamlit.util import calc_md5
|
||||
|
||||
# Regular expression to match the SRT timestamp format
|
||||
# It matches the
|
||||
# "hours:minutes:seconds,milliseconds --> hours:minutes:seconds,milliseconds" format
|
||||
SRT_VALIDATION_REGEX = r"\d{2}:\d{2}:\d{2},\d{3} --> \d{2}:\d{2}:\d{2},\d{3}"
|
||||
|
||||
SRT_CONVERSION_REGEX = r"(\d{2}:\d{2}:\d{2}),(\d{3})"
|
||||
|
||||
SUBTITLE_ALLOWED_FORMATS = (".srt", ".vtt")
|
||||
|
||||
|
||||
def _is_srt(stream: str | io.BytesIO | bytes) -> bool:
|
||||
# Handle raw bytes
|
||||
if isinstance(stream, bytes):
|
||||
stream = io.BytesIO(stream)
|
||||
|
||||
# Convert str to io.BytesIO if 'stream' is a string
|
||||
if isinstance(stream, str):
|
||||
stream = io.BytesIO(stream.encode("utf-8"))
|
||||
|
||||
# Set the stream position to the beginning in case it's been moved
|
||||
stream.seek(0)
|
||||
|
||||
# Read enough bytes to reliably check for SRT patterns
|
||||
# This might be adjusted, but 33 bytes should be enough to read the first numeric
|
||||
# line, the full timestamp line, and a bit of the next line
|
||||
header = stream.read(33)
|
||||
|
||||
try:
|
||||
header_str = header.decode("utf-8").strip() # Decode and strip whitespace
|
||||
except UnicodeDecodeError:
|
||||
# If it's not valid utf-8, it's probably not a valid SRT file
|
||||
return False
|
||||
|
||||
# Split the header into lines and process them
|
||||
lines = header_str.split("\n")
|
||||
|
||||
# Check for the pattern of an SRT file: digit(s), newline, timestamp
|
||||
if len(lines) >= 2 and lines[0].isdigit():
|
||||
match = re.search(SRT_VALIDATION_REGEX, lines[1])
|
||||
if match:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _srt_to_vtt(srt_data: str | bytes) -> bytes:
|
||||
"""
|
||||
Convert subtitles from SubRip (.srt) format to WebVTT (.vtt) format.
|
||||
This function accepts the content of the .srt file either as a string
|
||||
or as a BytesIO stream.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
srt_data : str or bytes
|
||||
The content of the .srt file as a string or a bytes stream.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bytes
|
||||
The content converted into .vtt format.
|
||||
"""
|
||||
|
||||
# If the input is a bytes stream, convert it to a string
|
||||
if isinstance(srt_data, bytes):
|
||||
# Decode the bytes to a UTF-8 string
|
||||
try:
|
||||
srt_data = srt_data.decode("utf-8")
|
||||
except UnicodeDecodeError as e:
|
||||
raise ValueError("Could not decode the input stream as UTF-8.") from e
|
||||
if not isinstance(srt_data, str):
|
||||
# If it's not a string by this point, something is wrong.
|
||||
raise TypeError(
|
||||
f"Input must be a string or a bytes stream, not {type(srt_data)}."
|
||||
)
|
||||
|
||||
# Replace SubRip timing with WebVTT timing
|
||||
vtt_data = re.sub(SRT_CONVERSION_REGEX, r"\1.\2", srt_data)
|
||||
|
||||
# Add WebVTT file header
|
||||
vtt_content = "WEBVTT\n\n" + vtt_data
|
||||
# Convert the vtt content to bytes
|
||||
vtt_content = vtt_content.strip().encode("utf-8")
|
||||
|
||||
return vtt_content
|
||||
|
||||
|
||||
def _handle_string_or_path_data(data_or_path: str | Path) -> bytes:
|
||||
"""Handles string data, either as a file path or raw content."""
|
||||
if os.path.isfile(data_or_path):
|
||||
path = Path(data_or_path)
|
||||
file_extension = path.suffix.lower()
|
||||
|
||||
if file_extension not in SUBTITLE_ALLOWED_FORMATS:
|
||||
raise ValueError(
|
||||
f"Incorrect subtitle format {file_extension}. Subtitles must be in "
|
||||
f"one of the following formats: {', '.join(SUBTITLE_ALLOWED_FORMATS)}"
|
||||
)
|
||||
with open(data_or_path, "rb") as file:
|
||||
content = file.read()
|
||||
return _srt_to_vtt(content) if file_extension == ".srt" else content
|
||||
elif isinstance(data_or_path, Path):
|
||||
raise ValueError(f"File {data_or_path} does not exist.")
|
||||
|
||||
content_string = data_or_path.strip()
|
||||
|
||||
if content_string.startswith("WEBVTT") or content_string == "":
|
||||
return content_string.encode("utf-8")
|
||||
elif _is_srt(content_string):
|
||||
return _srt_to_vtt(content_string)
|
||||
raise ValueError("The provided string neither matches valid VTT nor SRT format.")
|
||||
|
||||
|
||||
def _handle_stream_data(stream: io.BytesIO) -> bytes:
|
||||
"""Handles io.BytesIO data, converting SRT to VTT content if needed."""
|
||||
stream.seek(0)
|
||||
stream_data = stream.getvalue()
|
||||
return _srt_to_vtt(stream_data) if _is_srt(stream) else stream_data
|
||||
|
||||
|
||||
def _handle_bytes_data(data: bytes) -> bytes:
|
||||
"""Handles io.BytesIO data, converting SRT to VTT content if needed."""
|
||||
return _srt_to_vtt(data) if _is_srt(data) else data
|
||||
|
||||
|
||||
def process_subtitle_data(
|
||||
coordinates: str,
|
||||
data: str | bytes | Path | io.BytesIO,
|
||||
label: str,
|
||||
) -> str:
|
||||
# Determine the type of data and process accordingly
|
||||
if isinstance(data, (str, Path)):
|
||||
subtitle_data = _handle_string_or_path_data(data)
|
||||
elif isinstance(data, io.BytesIO):
|
||||
subtitle_data = _handle_stream_data(data)
|
||||
elif isinstance(data, bytes):
|
||||
subtitle_data = _handle_bytes_data(data)
|
||||
else:
|
||||
raise TypeError(f"Invalid binary data format for subtitle: {type(data)}.")
|
||||
|
||||
if runtime.exists():
|
||||
filename = calc_md5(label.encode())
|
||||
# Save the processed data and return the file URL
|
||||
file_url = runtime.get_instance().media_file_mgr.add(
|
||||
path_or_data=subtitle_data,
|
||||
mimetype="text/vtt",
|
||||
coordinates=coordinates,
|
||||
file_name=f"{filename}.vtt",
|
||||
)
|
||||
caching.save_media_data(subtitle_data, "text/vtt", coordinates)
|
||||
return file_url
|
||||
else:
|
||||
# When running in "raw mode", we can't access the MediaFileManager.
|
||||
return ""
|
||||
@@ -0,0 +1,248 @@
|
||||
# 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 hashlib
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Literal,
|
||||
Union,
|
||||
overload,
|
||||
)
|
||||
|
||||
from google.protobuf.message import Message
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit import config
|
||||
from streamlit.errors import StreamlitDuplicateElementId, StreamlitDuplicateElementKey
|
||||
from streamlit.proto.ChatInput_pb2 import ChatInput
|
||||
from streamlit.proto.LabelVisibilityMessage_pb2 import LabelVisibilityMessage
|
||||
from streamlit.runtime.scriptrunner_utils.script_run_context import (
|
||||
ScriptRunContext,
|
||||
get_script_run_ctx,
|
||||
)
|
||||
from streamlit.runtime.state.common import (
|
||||
GENERATED_ELEMENT_ID_PREFIX,
|
||||
TESTING_KEY,
|
||||
user_key_from_element_id,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from builtins import ellipsis
|
||||
from collections.abc import Iterable
|
||||
|
||||
|
||||
Key: TypeAlias = Union[str, int]
|
||||
|
||||
LabelVisibility: TypeAlias = Literal["visible", "hidden", "collapsed"]
|
||||
|
||||
PROTO_SCALAR_VALUE = Union[float, int, bool, str, bytes]
|
||||
SAFE_VALUES = Union[
|
||||
date,
|
||||
time,
|
||||
datetime,
|
||||
timedelta,
|
||||
None,
|
||||
"ellipsis",
|
||||
Message,
|
||||
PROTO_SCALAR_VALUE,
|
||||
]
|
||||
|
||||
|
||||
def get_label_visibility_proto_value(
|
||||
label_visibility_string: LabelVisibility,
|
||||
) -> LabelVisibilityMessage.LabelVisibilityOptions.ValueType:
|
||||
"""Returns one of LabelVisibilityMessage enum constants.py based on string value."""
|
||||
|
||||
if label_visibility_string == "visible":
|
||||
return LabelVisibilityMessage.LabelVisibilityOptions.VISIBLE
|
||||
elif label_visibility_string == "hidden":
|
||||
return LabelVisibilityMessage.LabelVisibilityOptions.HIDDEN
|
||||
elif label_visibility_string == "collapsed":
|
||||
return LabelVisibilityMessage.LabelVisibilityOptions.COLLAPSED
|
||||
|
||||
raise ValueError(f"Unknown label visibility value: {label_visibility_string}")
|
||||
|
||||
|
||||
def get_chat_input_accept_file_proto_value(
|
||||
accept_file_value: bool | Literal["multiple"],
|
||||
) -> ChatInput.AcceptFile.ValueType:
|
||||
"""Returns one of ChatInput.AcceptFile enum value based on string value."""
|
||||
|
||||
if accept_file_value is False:
|
||||
return ChatInput.AcceptFile.NONE
|
||||
elif accept_file_value is True:
|
||||
return ChatInput.AcceptFile.SINGLE
|
||||
elif accept_file_value == "multiple":
|
||||
return ChatInput.AcceptFile.MULTIPLE
|
||||
|
||||
raise ValueError(f"Unknown accept file value: {accept_file_value}")
|
||||
|
||||
|
||||
@overload
|
||||
def to_key(key: None) -> None: ...
|
||||
|
||||
|
||||
@overload
|
||||
def to_key(key: Key) -> str: ...
|
||||
|
||||
|
||||
def to_key(key: Key | None) -> str | None:
|
||||
return None if key is None else str(key)
|
||||
|
||||
|
||||
def _register_element_id(
|
||||
ctx: ScriptRunContext, element_type: str, element_id: str
|
||||
) -> None:
|
||||
"""Register the element ID and key for the given element.
|
||||
|
||||
If the element ID or key is not unique, an error is raised.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
element_type : str
|
||||
The type of the element to register.
|
||||
|
||||
element_id : str
|
||||
The ID of the element to register.
|
||||
|
||||
Raises
|
||||
------
|
||||
StreamlitDuplicateElementKey
|
||||
If the element key is not unique.
|
||||
|
||||
StreamlitDuplicateElementID
|
||||
If the element ID is not unique.
|
||||
|
||||
"""
|
||||
|
||||
if not element_id:
|
||||
return
|
||||
|
||||
if user_key := user_key_from_element_id(element_id):
|
||||
if user_key not in ctx.widget_user_keys_this_run:
|
||||
ctx.widget_user_keys_this_run.add(user_key)
|
||||
else:
|
||||
raise StreamlitDuplicateElementKey(user_key)
|
||||
|
||||
if element_id not in ctx.widget_ids_this_run:
|
||||
ctx.widget_ids_this_run.add(element_id)
|
||||
else:
|
||||
raise StreamlitDuplicateElementId(element_type)
|
||||
|
||||
|
||||
def _compute_element_id(
|
||||
element_type: str,
|
||||
user_key: str | None = None,
|
||||
**kwargs: SAFE_VALUES | Iterable[SAFE_VALUES],
|
||||
) -> str:
|
||||
"""Compute the ID for the given element.
|
||||
|
||||
This ID is stable: a given set of inputs to this function will always produce
|
||||
the same ID output. Only stable, deterministic values should be used to compute
|
||||
element IDs. Using nondeterministic values as inputs can cause the resulting
|
||||
element ID to change between runs.
|
||||
|
||||
The element ID includes the user_key so elements with identical arguments can
|
||||
use it to be distinct. The element ID includes an easily identified prefix, and the
|
||||
user_key as a suffix, to make it easy to identify it and know if a key maps to it.
|
||||
"""
|
||||
h = hashlib.new("md5", usedforsecurity=False)
|
||||
h.update(element_type.encode("utf-8"))
|
||||
if user_key:
|
||||
# Adding this to the hash isn't necessary for uniqueness since the
|
||||
# key is also appended to the ID as raw text. But since the hash and
|
||||
# the appending of the key are two slightly different aspects, it
|
||||
# still gets put into the hash.
|
||||
h.update(user_key.encode("utf-8"))
|
||||
# This will iterate in a consistent order when the provided arguments have
|
||||
# consistent order; dicts are always in insertion order.
|
||||
for k, v in kwargs.items():
|
||||
h.update(str(k).encode("utf-8"))
|
||||
h.update(str(v).encode("utf-8"))
|
||||
return f"{GENERATED_ELEMENT_ID_PREFIX}-{h.hexdigest()}-{user_key}"
|
||||
|
||||
|
||||
def compute_and_register_element_id(
|
||||
element_type: str,
|
||||
*,
|
||||
user_key: str | None,
|
||||
form_id: str | None,
|
||||
**kwargs: SAFE_VALUES | Iterable[SAFE_VALUES],
|
||||
) -> str:
|
||||
"""Compute and register the ID for the given element.
|
||||
|
||||
This ID is stable: a given set of inputs to this function will always produce
|
||||
the same ID output. Only stable, deterministic values should be used to compute
|
||||
element IDs. Using nondeterministic values as inputs can cause the resulting
|
||||
element ID to change between runs.
|
||||
|
||||
The element ID includes the user_key so elements with identical arguments can
|
||||
use it to be distinct. The element ID includes an easily identified prefix, and the
|
||||
user_key as a suffix, to make it easy to identify it and know if a key maps to it.
|
||||
|
||||
The element ID gets registered to make sure that only one ID and user-specified
|
||||
key exists at the same time. If there are duplicated IDs or keys, an error
|
||||
is raised.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
element_type : str
|
||||
The type (command name) of the element to register.
|
||||
|
||||
user_key : str | None
|
||||
The user-specified key for the element. `None` if no key is provided
|
||||
or if the element doesn't support a specifying a key.
|
||||
|
||||
form_id : str | None
|
||||
The ID of the form that the element belongs to. `None` or empty string
|
||||
if the element doesn't belong to a form or doesn't support forms.
|
||||
|
||||
kwargs : SAFE_VALUES | Iterable[SAFE_VALUES]
|
||||
The arguments to use to compute the element ID.
|
||||
The arguments must be stable, deterministic values.
|
||||
Some common parameters like key, disabled,
|
||||
format_func, label_visibility, args, kwargs, on_change, and
|
||||
the active_script_hash are not supposed to be added here
|
||||
"""
|
||||
ctx = get_script_run_ctx()
|
||||
|
||||
# If form_id is provided, add it to the kwargs.
|
||||
kwargs_to_use = {"form_id": form_id, **kwargs} if form_id else kwargs
|
||||
|
||||
if ctx:
|
||||
# Add the active script hash to give elements on different
|
||||
# pages unique IDs.
|
||||
kwargs_to_use["active_script_hash"] = ctx.active_script_hash
|
||||
|
||||
element_id = _compute_element_id(
|
||||
element_type,
|
||||
user_key,
|
||||
**kwargs_to_use,
|
||||
)
|
||||
|
||||
if ctx:
|
||||
_register_element_id(ctx, element_type, element_id)
|
||||
return element_id
|
||||
|
||||
|
||||
def save_for_app_testing(ctx: ScriptRunContext, k: str, v: Any):
|
||||
if config.get_option("global.appTest"):
|
||||
try:
|
||||
ctx.session_state[TESTING_KEY][k] = v
|
||||
except KeyError:
|
||||
ctx.session_state[TESTING_KEY] = {k: v}
|
||||
508
myenv/lib/python3.11/site-packages/streamlit/elements/map.py
Normal file
508
myenv/lib/python3.11/site-packages/streamlit/elements/map.py
Normal file
@@ -0,0 +1,508 @@
|
||||
# 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.
|
||||
|
||||
"""A wrapper for simple PyDeck scatter charts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
from typing import TYPE_CHECKING, Any, Final, cast
|
||||
|
||||
import streamlit.elements.deck_gl_json_chart as deck_gl_json_chart
|
||||
from streamlit import config, dataframe_util
|
||||
from streamlit.elements.lib.color_util import (
|
||||
Color,
|
||||
IntColorTuple,
|
||||
is_color_like,
|
||||
to_int_color_tuple,
|
||||
)
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.DeckGlJsonChart_pb2 import DeckGlJsonChart as DeckGlJsonChartProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Collection
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from streamlit.dataframe_util import Data
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
# Map used as the basis for st.map.
|
||||
_DEFAULT_MAP: Final[dict[str, Any]] = dict(deck_gl_json_chart.EMPTY_MAP)
|
||||
|
||||
# Other default parameters for st.map.
|
||||
_DEFAULT_LAT_COL_NAMES: Final = {"lat", "latitude", "LAT", "LATITUDE"}
|
||||
_DEFAULT_LON_COL_NAMES: Final = {"lon", "longitude", "LON", "LONGITUDE"}
|
||||
_DEFAULT_COLOR: Final = (200, 30, 0, 160)
|
||||
_DEFAULT_SIZE: Final = 100
|
||||
_DEFAULT_ZOOM_LEVEL: Final = 12
|
||||
_ZOOM_LEVELS: Final = [
|
||||
360,
|
||||
180,
|
||||
90,
|
||||
45,
|
||||
22.5,
|
||||
11.25,
|
||||
5.625,
|
||||
2.813,
|
||||
1.406,
|
||||
0.703,
|
||||
0.352,
|
||||
0.176,
|
||||
0.088,
|
||||
0.044,
|
||||
0.022,
|
||||
0.011,
|
||||
0.005,
|
||||
0.003,
|
||||
0.001,
|
||||
0.0005,
|
||||
0.00025,
|
||||
]
|
||||
|
||||
|
||||
class MapMixin:
|
||||
@gather_metrics("map")
|
||||
def map(
|
||||
self,
|
||||
data: Data = None,
|
||||
*,
|
||||
latitude: str | None = None,
|
||||
longitude: str | None = None,
|
||||
color: None | str | Color = None,
|
||||
size: None | str | float = None,
|
||||
zoom: int | None = None,
|
||||
use_container_width: bool = True,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
) -> DeltaGenerator:
|
||||
"""Display a map with a scatterplot overlaid onto it.
|
||||
|
||||
This is a wrapper around ``st.pydeck_chart`` to quickly create
|
||||
scatterplot charts on top of a map, with auto-centering and auto-zoom.
|
||||
|
||||
When using this command, Mapbox provides the map tiles to render map
|
||||
content. Note that Mapbox is a third-party product and Streamlit accepts
|
||||
no responsibility or liability of any kind for Mapbox or for any content
|
||||
or information made available by Mapbox.
|
||||
|
||||
Mapbox requires users to register and provide a token before users can
|
||||
request map tiles. Currently, Streamlit provides this token for you, but
|
||||
this could change at any time. We strongly recommend all users create and
|
||||
use their own personal Mapbox token to avoid any disruptions to their
|
||||
experience. You can do this with the ``mapbox.token`` config option. The
|
||||
use of Mapbox is governed by Mapbox's Terms of Use.
|
||||
|
||||
To get a token for yourself, create an account at https://mapbox.com.
|
||||
For more info on how to set config options, see
|
||||
https://docs.streamlit.io/develop/api-reference/configuration/config.toml.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : Anything supported by st.dataframe
|
||||
The data to be plotted.
|
||||
|
||||
latitude : str or None
|
||||
The name of the column containing the latitude coordinates of
|
||||
the datapoints in the chart.
|
||||
|
||||
If None, the latitude data will come from any column named 'lat',
|
||||
'latitude', 'LAT', or 'LATITUDE'.
|
||||
|
||||
longitude : str or None
|
||||
The name of the column containing the longitude coordinates of
|
||||
the datapoints in the chart.
|
||||
|
||||
If None, the longitude data will come from any column named 'lon',
|
||||
'longitude', 'LON', or 'LONGITUDE'.
|
||||
|
||||
color : str or tuple or None
|
||||
The color of the circles representing each datapoint.
|
||||
|
||||
Can be:
|
||||
|
||||
- None, to use the default color.
|
||||
- A hex string like "#ffaa00" or "#ffaa0088".
|
||||
- An RGB or RGBA tuple with the red, green, blue, and alpha
|
||||
components specified as ints from 0 to 255 or floats from 0.0 to
|
||||
1.0.
|
||||
- The name of the column to use for the color. Cells in this column
|
||||
should contain colors represented as a hex string or color tuple,
|
||||
as described above.
|
||||
|
||||
size : str or float or None
|
||||
The size of the circles representing each point, in meters.
|
||||
|
||||
This can be:
|
||||
|
||||
- None, to use the default size.
|
||||
- A number like 100, to specify a single size to use for all
|
||||
datapoints.
|
||||
- The name of the column to use for the size. This allows each
|
||||
datapoint to be represented by a circle of a different size.
|
||||
|
||||
zoom : int
|
||||
Zoom level as specified in
|
||||
https://wiki.openstreetmap.org/wiki/Zoom_levels.
|
||||
|
||||
use_container_width : bool
|
||||
Whether to override the map's native width with the width of
|
||||
the parent container. If ``use_container_width`` is ``True``
|
||||
(default), Streamlit sets the width of the map to match the width
|
||||
of the parent container. If ``use_container_width`` is ``False``,
|
||||
Streamlit sets the width of the chart to fit its contents according
|
||||
to the plotting library, up to the width of the parent container.
|
||||
|
||||
width : int or None
|
||||
Desired width of the chart expressed in pixels. If ``width`` is
|
||||
``None`` (default), Streamlit sets the width of the chart to fit
|
||||
its contents according to the plotting library, up to the width of
|
||||
the parent container. If ``width`` is greater than the width of the
|
||||
parent container, Streamlit sets the chart width to match the width
|
||||
of the parent container.
|
||||
|
||||
To use ``width``, you must set ``use_container_width=False``.
|
||||
|
||||
height : int or None
|
||||
Desired height of the chart expressed in pixels. If ``height`` is
|
||||
``None`` (default), Streamlit sets the height of the chart to fit
|
||||
its contents according to the plotting library.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> df = pd.DataFrame(
|
||||
... np.random.randn(1000, 2) / [50, 50] + [37.76, -122.4],
|
||||
... columns=["lat", "lon"],
|
||||
... )
|
||||
>>> st.map(df)
|
||||
|
||||
.. output::
|
||||
https://doc-map.streamlit.app/
|
||||
height: 600px
|
||||
|
||||
You can also customize the size and color of the datapoints:
|
||||
|
||||
>>> st.map(df, size=20, color="#0044ff")
|
||||
|
||||
And finally, you can choose different columns to use for the latitude
|
||||
and longitude components, as well as set size and color of each
|
||||
datapoint dynamically based on other columns:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> df = pd.DataFrame(
|
||||
... {
|
||||
... "col1": np.random.randn(1000) / 50 + 37.76,
|
||||
... "col2": np.random.randn(1000) / 50 + -122.4,
|
||||
... "col3": np.random.randn(1000) * 100,
|
||||
... "col4": np.random.rand(1000, 4).tolist(),
|
||||
... }
|
||||
... )
|
||||
>>>
|
||||
>>> st.map(df, latitude="col1", longitude="col2", size="col3", color="col4")
|
||||
|
||||
.. output::
|
||||
https://doc-map-color.streamlit.app/
|
||||
height: 600px
|
||||
|
||||
"""
|
||||
# This feature was turned off while we investigate why different
|
||||
# map styles cause DeckGL to crash.
|
||||
#
|
||||
# For reference, this was the docstring for map_style:
|
||||
#
|
||||
# map_style : str or None
|
||||
# One of Mapbox's map style URLs. A full list can be found here:
|
||||
# https://docs.mapbox.com/api/maps/styles/#mapbox-styles
|
||||
#
|
||||
# This feature requires a Mapbox token. See the top of these docs
|
||||
# for information on how to get one and set it up in Streamlit.
|
||||
#
|
||||
map_style = None
|
||||
map_proto = DeckGlJsonChartProto()
|
||||
deck_gl_json = to_deckgl_json(
|
||||
data, latitude, longitude, size, color, map_style, zoom
|
||||
)
|
||||
marshall(
|
||||
map_proto, deck_gl_json, use_container_width, width=width, height=height
|
||||
)
|
||||
return self.dg._enqueue("deck_gl_json_chart", map_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
def to_deckgl_json(
|
||||
data: Data,
|
||||
lat: str | None,
|
||||
lon: str | None,
|
||||
size: None | str | float,
|
||||
color: None | str | Collection[float],
|
||||
map_style: str | None,
|
||||
zoom: int | None,
|
||||
) -> str:
|
||||
if data is None:
|
||||
return json.dumps(_DEFAULT_MAP)
|
||||
|
||||
# TODO(harahu): iterables don't have the empty attribute. This is either
|
||||
# a bug, or the documented data type is too broad. One or the other
|
||||
# should be addressed
|
||||
if hasattr(data, "empty") and data.empty:
|
||||
return json.dumps(_DEFAULT_MAP)
|
||||
|
||||
df = dataframe_util.convert_anything_to_pandas_df(data)
|
||||
|
||||
lat_col_name = _get_lat_or_lon_col_name(df, "latitude", lat, _DEFAULT_LAT_COL_NAMES)
|
||||
lon_col_name = _get_lat_or_lon_col_name(
|
||||
df, "longitude", lon, _DEFAULT_LON_COL_NAMES
|
||||
)
|
||||
size_arg, size_col_name = _get_value_and_col_name(df, size, _DEFAULT_SIZE)
|
||||
color_arg, color_col_name = _get_value_and_col_name(df, color, _DEFAULT_COLOR)
|
||||
|
||||
# Drop columns we're not using.
|
||||
# (Sort for tests)
|
||||
used_columns = sorted(
|
||||
[
|
||||
c
|
||||
for c in {lat_col_name, lon_col_name, size_col_name, color_col_name}
|
||||
if c is not None
|
||||
]
|
||||
)
|
||||
df = df[used_columns]
|
||||
|
||||
color_arg = _convert_color_arg_or_column(df, color_arg, color_col_name)
|
||||
|
||||
zoom, center_lat, center_lon = _get_viewport_details(
|
||||
df, lat_col_name, lon_col_name, zoom
|
||||
)
|
||||
|
||||
default = copy.deepcopy(_DEFAULT_MAP)
|
||||
default["initialViewState"]["latitude"] = center_lat
|
||||
default["initialViewState"]["longitude"] = center_lon
|
||||
default["initialViewState"]["zoom"] = zoom
|
||||
default["layers"] = [
|
||||
{
|
||||
"@@type": "ScatterplotLayer",
|
||||
"getPosition": f"@@=[{lon_col_name}, {lat_col_name}]",
|
||||
"getRadius": size_arg,
|
||||
"radiusMinPixels": 3,
|
||||
"radiusUnits": "meters",
|
||||
"getFillColor": color_arg,
|
||||
"data": df.to_dict("records"),
|
||||
}
|
||||
]
|
||||
|
||||
if map_style:
|
||||
if not config.get_option("mapbox.token"):
|
||||
raise StreamlitAPIException(
|
||||
"You need a Mapbox token in order to select a map type. "
|
||||
"Refer to the docs for st.map for more information."
|
||||
)
|
||||
default["mapStyle"] = map_style
|
||||
|
||||
return json.dumps(default)
|
||||
|
||||
|
||||
def _get_lat_or_lon_col_name(
|
||||
data: DataFrame,
|
||||
human_readable_name: str,
|
||||
col_name_from_user: str | None,
|
||||
default_col_names: set[str],
|
||||
) -> str:
|
||||
"""Returns the column name to be used for latitude or longitude."""
|
||||
|
||||
if isinstance(col_name_from_user, str) and col_name_from_user in data.columns:
|
||||
col_name = col_name_from_user
|
||||
|
||||
else:
|
||||
# Try one of the default col_names:
|
||||
candidate_col_name = None
|
||||
|
||||
for c in default_col_names:
|
||||
if c in data.columns:
|
||||
candidate_col_name = c
|
||||
break
|
||||
|
||||
if candidate_col_name is None:
|
||||
formatted_allowed_col_name = ", ".join(map(repr, sorted(default_col_names)))
|
||||
formmated_col_names = ", ".join(map(repr, list(data.columns)))
|
||||
|
||||
raise StreamlitAPIException(
|
||||
f"Map data must contain a {human_readable_name} column named: "
|
||||
f"{formatted_allowed_col_name}. Existing columns: {formmated_col_names}"
|
||||
)
|
||||
else:
|
||||
col_name = candidate_col_name
|
||||
|
||||
# Check that the column is well-formed.
|
||||
# IMPLEMENTATION NOTE: We can't use isnull().values.any() because .values can return
|
||||
# ExtensionArrays, which don't have a .any() method.
|
||||
# (Read about ExtensionArrays here: # https://pandas.pydata.org/community/blog/extension-arrays.html)
|
||||
# However, after a performance test I found the solution below runs basically as
|
||||
# fast as .values.any().
|
||||
if any(data[col_name].isna().array):
|
||||
raise StreamlitAPIException(
|
||||
f"Column {col_name} is not allowed to contain null values, such "
|
||||
"as NaN, NaT, or None."
|
||||
)
|
||||
|
||||
return col_name
|
||||
|
||||
|
||||
def _get_value_and_col_name(
|
||||
data: DataFrame,
|
||||
value_or_name: Any,
|
||||
default_value: Any,
|
||||
) -> tuple[Any, str | None]:
|
||||
"""Take a value_or_name passed in by the Streamlit developer and return a PyDeck
|
||||
argument and column name for that property.
|
||||
|
||||
This is used for the size and color properties of the chart.
|
||||
|
||||
Example:
|
||||
- If the user passes size=None, this returns the default size value and no column.
|
||||
- If the user passes size=42, this returns 42 and no column.
|
||||
- If the user passes size="my_col_123", this returns "@@=my_col_123" and "my_col_123".
|
||||
"""
|
||||
|
||||
pydeck_arg: str | float
|
||||
|
||||
if isinstance(value_or_name, str) and value_or_name in data.columns:
|
||||
col_name = value_or_name
|
||||
pydeck_arg = f"@@={col_name}"
|
||||
else:
|
||||
col_name = None
|
||||
|
||||
if value_or_name is None:
|
||||
pydeck_arg = default_value
|
||||
else:
|
||||
pydeck_arg = value_or_name
|
||||
|
||||
return pydeck_arg, col_name
|
||||
|
||||
|
||||
def _convert_color_arg_or_column(
|
||||
data: DataFrame,
|
||||
color_arg: str | Color,
|
||||
color_col_name: str | None,
|
||||
) -> None | str | IntColorTuple:
|
||||
"""Converts color to a format accepted by PyDeck.
|
||||
|
||||
For example:
|
||||
- If color_arg is "#fff", then returns (255, 255, 255, 255).
|
||||
- If color_col_name is "my_col_123", then it converts everything in column my_col_123 to
|
||||
an accepted color format such as (0, 100, 200, 255).
|
||||
|
||||
NOTE: This function mutates the data argument.
|
||||
"""
|
||||
|
||||
color_arg_out: None | str | IntColorTuple = None
|
||||
|
||||
if color_col_name is not None:
|
||||
# Convert color column to the right format.
|
||||
if len(data[color_col_name]) > 0 and is_color_like(
|
||||
data[color_col_name].iloc[0]
|
||||
):
|
||||
# Use .loc[] to avoid a SettingWithCopyWarning in some cases.
|
||||
data.loc[:, color_col_name] = data.loc[:, color_col_name].map(
|
||||
to_int_color_tuple
|
||||
)
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
f'Column "{color_col_name}" does not appear to contain valid colors.'
|
||||
)
|
||||
|
||||
# This is guaranteed to be a str because of _get_value_and_col_name
|
||||
assert isinstance(color_arg, str)
|
||||
color_arg_out = color_arg
|
||||
|
||||
elif color_arg is not None:
|
||||
color_arg_out = to_int_color_tuple(color_arg)
|
||||
|
||||
return color_arg_out
|
||||
|
||||
|
||||
def _get_viewport_details(
|
||||
data: DataFrame, lat_col_name: str, lon_col_name: str, zoom: int | None
|
||||
) -> tuple[int, float, float]:
|
||||
"""Auto-set viewport when not fully specified by user."""
|
||||
min_lat = data[lat_col_name].min()
|
||||
max_lat = data[lat_col_name].max()
|
||||
min_lon = data[lon_col_name].min()
|
||||
max_lon = data[lon_col_name].max()
|
||||
center_lat = (max_lat + min_lat) / 2.0
|
||||
center_lon = (max_lon + min_lon) / 2.0
|
||||
range_lon = abs(max_lon - min_lon)
|
||||
range_lat = abs(max_lat - min_lat)
|
||||
|
||||
if zoom is None:
|
||||
if range_lon > range_lat:
|
||||
longitude_distance = range_lon
|
||||
else:
|
||||
longitude_distance = range_lat
|
||||
zoom = _get_zoom_level(longitude_distance)
|
||||
|
||||
return zoom, center_lat, center_lon
|
||||
|
||||
|
||||
def _get_zoom_level(distance: float) -> int:
|
||||
"""Get the zoom level for a given distance in degrees.
|
||||
|
||||
See https://wiki.openstreetmap.org/wiki/Zoom_levels for reference.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
distance : float
|
||||
How many degrees of longitude should fit in the map.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The zoom level, from 0 to 20.
|
||||
|
||||
"""
|
||||
for i in range(len(_ZOOM_LEVELS) - 1):
|
||||
if _ZOOM_LEVELS[i + 1] < distance <= _ZOOM_LEVELS[i]:
|
||||
return i
|
||||
|
||||
# For small number of points the default zoom level will be used.
|
||||
return _DEFAULT_ZOOM_LEVEL
|
||||
|
||||
|
||||
def marshall(
|
||||
pydeck_proto: DeckGlJsonChartProto,
|
||||
pydeck_json: str,
|
||||
use_container_width: bool,
|
||||
height: int | None = None,
|
||||
width: int | None = None,
|
||||
) -> None:
|
||||
pydeck_proto.json = pydeck_json
|
||||
pydeck_proto.use_container_width = use_container_width
|
||||
|
||||
if width:
|
||||
pydeck_proto.width = width
|
||||
if height:
|
||||
pydeck_proto.height = height
|
||||
|
||||
pydeck_proto.id = ""
|
||||
@@ -0,0 +1,388 @@
|
||||
# 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 typing import TYPE_CHECKING, Final, Literal, cast
|
||||
|
||||
from streamlit.proto.Markdown_pb2 import Markdown as MarkdownProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.string_util import clean_text, validate_icon_or_emoji
|
||||
from streamlit.type_util import SupportsStr, is_sympy_expression
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import sympy
|
||||
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
MARKDOWN_HORIZONTAL_RULE_EXPRESSION: Final = "---"
|
||||
|
||||
|
||||
class MarkdownMixin:
|
||||
@gather_metrics("markdown")
|
||||
def markdown(
|
||||
self,
|
||||
body: SupportsStr,
|
||||
unsafe_allow_html: bool = False,
|
||||
*, # keyword-only arguments:
|
||||
help: str | None = None,
|
||||
) -> DeltaGenerator:
|
||||
r"""Display string formatted as Markdown.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : any
|
||||
The text to display as GitHub-flavored Markdown. Syntax
|
||||
information can be found at: https://github.github.com/gfm.
|
||||
If anything other than a string is passed, it will be converted
|
||||
into a string behind the scenes using ``str(body)``.
|
||||
|
||||
This also supports:
|
||||
|
||||
- Emoji shortcodes, such as ``:+1:`` and ``:sunglasses:``.
|
||||
For a list of all supported codes,
|
||||
see https://share.streamlit.io/streamlit/emoji-shortcodes.
|
||||
|
||||
- Streamlit logo shortcode. Use ``:streamlit:`` to add a little
|
||||
Streamlit flair to your text.
|
||||
|
||||
- A limited set of typographical symbols. ``"<- -> <-> -- >= <= ~="``
|
||||
becomes "← → ↔ — ≥ ≤ ≈" when parsed as Markdown.
|
||||
|
||||
- Google Material Symbols (rounded style), using the syntax
|
||||
``:material/icon_name:``, where "icon_name" is the name of the
|
||||
icon in snake case. For a complete list of icons, see Google's
|
||||
`Material Symbols <https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded>`_
|
||||
font library.
|
||||
|
||||
- LaTeX expressions, by wrapping them in "$" or "$$" (the "$$"
|
||||
must be on their own lines). Supported LaTeX functions are listed
|
||||
at https://katex.org/docs/supported.html.
|
||||
|
||||
- Colored text and background colors for text, using the syntax
|
||||
``:color[text to be colored]`` and ``:color-background[text to be colored]``,
|
||||
respectively. ``color`` must be replaced with any of the following
|
||||
supported colors: blue, green, orange, red, violet, gray/grey,
|
||||
rainbow, or primary. For example, you can use
|
||||
``:orange[your text here]`` or ``:blue-background[your text here]``.
|
||||
If you use "primary" for color, Streamlit will use the default
|
||||
primary accent color unless you set the ``theme.primaryColor``
|
||||
configuration option.
|
||||
|
||||
- Colored badges, using the syntax ``:color-badge[text in the badge]``.
|
||||
``color`` must be replaced with any of the following supported
|
||||
colors: blue, green, orange, red, violet, gray/grey, or primary.
|
||||
For example, you can use ``:orange-badge[your text here]`` or
|
||||
``:blue-badge[your text here]``.
|
||||
|
||||
- Small text, using the syntax ``:small[text to show small]``.
|
||||
|
||||
|
||||
unsafe_allow_html : bool
|
||||
Whether to render HTML within ``body``. If this is ``False``
|
||||
(default), any HTML tags found in ``body`` will be escaped and
|
||||
therefore treated as raw text. If this is ``True``, any HTML
|
||||
expressions within ``body`` will be rendered.
|
||||
|
||||
Adding custom HTML to your app impacts safety, styling, and
|
||||
maintainability.
|
||||
|
||||
.. note::
|
||||
If you only want to insert HTML or CSS without Markdown text,
|
||||
we recommend using ``st.html`` instead.
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed next to the Markdown. 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``.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.markdown("*Streamlit* is **really** ***cool***.")
|
||||
>>> st.markdown('''
|
||||
... :red[Streamlit] :orange[can] :green[write] :blue[text] :violet[in]
|
||||
... :gray[pretty] :rainbow[colors] and :blue-background[highlight] text.''')
|
||||
>>> st.markdown("Here's a bouquet —\
|
||||
... :tulip::cherry_blossom::rose::hibiscus::sunflower::blossom:")
|
||||
>>>
|
||||
>>> multi = '''If you end a line with two spaces,
|
||||
... a soft return is used for the next line.
|
||||
...
|
||||
... Two (or more) newline characters in a row will result in a hard return.
|
||||
... '''
|
||||
>>> st.markdown(multi)
|
||||
|
||||
.. output::
|
||||
https://doc-markdown.streamlit.app/
|
||||
height: 350px
|
||||
|
||||
"""
|
||||
markdown_proto = MarkdownProto()
|
||||
|
||||
markdown_proto.body = clean_text(body)
|
||||
markdown_proto.allow_html = unsafe_allow_html
|
||||
markdown_proto.element_type = MarkdownProto.Type.NATIVE
|
||||
if help:
|
||||
markdown_proto.help = help
|
||||
|
||||
return self.dg._enqueue("markdown", markdown_proto)
|
||||
|
||||
@gather_metrics("caption")
|
||||
def caption(
|
||||
self,
|
||||
body: SupportsStr,
|
||||
unsafe_allow_html: bool = False,
|
||||
*, # keyword-only arguments:
|
||||
help: str | None = None,
|
||||
) -> DeltaGenerator:
|
||||
"""Display text in small font.
|
||||
|
||||
This should be used for captions, asides, footnotes, sidenotes, and
|
||||
other explanatory text.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : str
|
||||
The text to display as GitHub-flavored Markdown. Syntax
|
||||
information can be found at: https://github.github.com/gfm.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
unsafe_allow_html : bool
|
||||
Whether to render HTML within ``body``. If this is ``False``
|
||||
(default), any HTML tags found in ``body`` will be escaped and
|
||||
therefore treated as raw text. If this is ``True``, any HTML
|
||||
expressions within ``body`` will be rendered.
|
||||
|
||||
Adding custom HTML to your app impacts safety, styling, and
|
||||
maintainability.
|
||||
|
||||
.. note::
|
||||
If you only want to insert HTML or CSS without Markdown text,
|
||||
we recommend using ``st.html`` instead.
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed next to the caption. 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``.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.caption("This is a string that explains something above.")
|
||||
>>> st.caption("A caption with _italics_ :blue[colors] and emojis :sunglasses:")
|
||||
|
||||
"""
|
||||
caption_proto = MarkdownProto()
|
||||
caption_proto.body = clean_text(body)
|
||||
caption_proto.allow_html = unsafe_allow_html
|
||||
caption_proto.is_caption = True
|
||||
caption_proto.element_type = MarkdownProto.Type.CAPTION
|
||||
if help:
|
||||
caption_proto.help = help
|
||||
return self.dg._enqueue("markdown", caption_proto)
|
||||
|
||||
@gather_metrics("latex")
|
||||
def latex(
|
||||
self,
|
||||
body: SupportsStr | sympy.Expr,
|
||||
*, # keyword-only arguments:
|
||||
help: str | None = None,
|
||||
) -> DeltaGenerator:
|
||||
# This docstring needs to be "raw" because of the backslashes in the
|
||||
# example below.
|
||||
r"""Display mathematical expressions formatted as LaTeX.
|
||||
|
||||
Supported LaTeX functions are listed at
|
||||
https://katex.org/docs/supported.html.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : str or SymPy expression
|
||||
The string or SymPy expression to display as LaTeX. If str, it's
|
||||
a good idea to use raw Python strings since LaTeX uses backslashes
|
||||
a lot.
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed next to the LaTeX expression. 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``.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.latex(r'''
|
||||
... a + ar + a r^2 + a r^3 + \cdots + a r^{n-1} =
|
||||
... \sum_{k=0}^{n-1} ar^k =
|
||||
... a \left(\frac{1-r^{n}}{1-r}\right)
|
||||
... ''')
|
||||
|
||||
"""
|
||||
if is_sympy_expression(body):
|
||||
import sympy
|
||||
|
||||
body = sympy.latex(body)
|
||||
|
||||
latex_proto = MarkdownProto()
|
||||
latex_proto.body = "$$\n%s\n$$" % clean_text(body)
|
||||
latex_proto.element_type = MarkdownProto.Type.LATEX
|
||||
if help:
|
||||
latex_proto.help = help
|
||||
return self.dg._enqueue("markdown", latex_proto)
|
||||
|
||||
@gather_metrics("divider")
|
||||
def divider(self) -> DeltaGenerator:
|
||||
"""Display a horizontal rule.
|
||||
|
||||
.. note::
|
||||
You can achieve the same effect with st.write("---") or
|
||||
even just "---" in your script (via magic).
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.divider()
|
||||
|
||||
"""
|
||||
divider_proto = MarkdownProto()
|
||||
divider_proto.body = MARKDOWN_HORIZONTAL_RULE_EXPRESSION
|
||||
divider_proto.element_type = MarkdownProto.Type.DIVIDER
|
||||
return self.dg._enqueue("markdown", divider_proto)
|
||||
|
||||
@gather_metrics("badge")
|
||||
def badge(
|
||||
self,
|
||||
label: str,
|
||||
*, # keyword-only arguments:
|
||||
icon: str | None = None,
|
||||
color: Literal[
|
||||
"blue",
|
||||
"green",
|
||||
"orange",
|
||||
"red",
|
||||
"violet",
|
||||
"gray",
|
||||
"grey",
|
||||
"rainbow",
|
||||
"primary",
|
||||
] = "blue",
|
||||
) -> DeltaGenerator:
|
||||
"""Display a colored badge with an icon and label.
|
||||
|
||||
This is a thin wrapper around the color-badge Markdown directive.
|
||||
The following are equivalent:
|
||||
|
||||
- ``st.markdown(":blue-badge[Home]")``
|
||||
- ``st.badge("Home", color="blue")``
|
||||
|
||||
.. note::
|
||||
You can insert badges everywhere Streamlit supports Markdown by
|
||||
using the color-badge Markdown directive. See ``st.markdown`` for
|
||||
more information.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : str
|
||||
The label to display in the badge. The label can optionally contain
|
||||
GitHub-flavored Markdown of the following types: Bold, Italics,
|
||||
Strikethroughs, Inline Code.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives. Because this command escapes square
|
||||
brackets (``[ ]``) in this parameter, any directive requiring
|
||||
square brackets is not supported.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
icon : str or None
|
||||
An optional emoji or icon to display next to the badge label. If
|
||||
``icon`` is ``None`` (default), no icon is displayed. If ``icon``
|
||||
is a string, the following options are valid:
|
||||
|
||||
- A single-character emoji. For example, you can set ``icon="🚨"``
|
||||
or ``icon="🔥"``. 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.
|
||||
|
||||
color : str
|
||||
The color to use for the badge. This defaults to ``"blue"``.
|
||||
|
||||
This can be one of the following supported colors: blue, green,
|
||||
orange, red, violet, gray/grey, or primary. If you use
|
||||
``"primary"``, Streamlit will use the default primary accent color
|
||||
unless you set the ``theme.primaryColor`` configuration option.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Create standalone badges with ``st.badge`` (with or without icons). If
|
||||
you want to have multiple, side-by-side badges, you can use the
|
||||
Markdown directive in ``st.markdown``.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.badge("New")
|
||||
>>> st.badge("Success", icon=":material/check:", color="green")
|
||||
>>>
|
||||
>>> st.markdown(
|
||||
>>> ":violet-badge[:material/star: Favorite] :orange-badge[⚠️ Needs review] :gray-badge[Deprecated]"
|
||||
>>> )
|
||||
|
||||
.. output ::
|
||||
https://doc-badge.streamlit.app/
|
||||
height: 220px
|
||||
|
||||
"""
|
||||
if icon is not None:
|
||||
icon_str = validate_icon_or_emoji(icon) + " "
|
||||
else:
|
||||
icon_str = ""
|
||||
|
||||
# Escape [ and ] characters in the label to prevent breaking the directive syntax
|
||||
escaped_label = label.replace("[", "\\[").replace("]", "\\]")
|
||||
|
||||
badge_proto = MarkdownProto()
|
||||
badge_proto.body = f":{color}-badge[{icon_str}{escaped_label}]"
|
||||
badge_proto.element_type = MarkdownProto.Type.NATIVE
|
||||
return self.dg._enqueue("markdown", badge_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
794
myenv/lib/python3.11/site-packages/streamlit/elements/media.py
Normal file
794
myenv/lib/python3.11/site-packages/streamlit/elements/media.py
Normal file
@@ -0,0 +1,794 @@
|
||||
# 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 io
|
||||
import re
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Final, Union, cast
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit import runtime, type_util, url_util
|
||||
from streamlit.elements.lib.form_utils import current_form_id
|
||||
from streamlit.elements.lib.subtitle_utils import process_subtitle_data
|
||||
from streamlit.elements.lib.utils import compute_and_register_element_id
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.Audio_pb2 import Audio as AudioProto
|
||||
from streamlit.proto.Video_pb2 import Video as VideoProto
|
||||
from streamlit.runtime import caching
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.time_util import time_to_seconds
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from numpy import typing as npt
|
||||
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.type_util import NumpyShape
|
||||
|
||||
|
||||
MediaData: TypeAlias = Union[
|
||||
str,
|
||||
Path,
|
||||
bytes,
|
||||
io.BytesIO,
|
||||
io.RawIOBase,
|
||||
io.BufferedReader,
|
||||
"npt.NDArray[Any]",
|
||||
None,
|
||||
]
|
||||
|
||||
SubtitleData: TypeAlias = Union[
|
||||
str, Path, bytes, io.BytesIO, dict[str, Union[str, Path, bytes, io.BytesIO]], None
|
||||
]
|
||||
|
||||
MediaTime: TypeAlias = Union[int, float, timedelta, str]
|
||||
|
||||
TIMEDELTA_PARSE_ERROR_MESSAGE: Final = (
|
||||
"Failed to convert '{param_name}' to a timedelta. "
|
||||
"Please use a string in a format supported by "
|
||||
"[Pandas Timedelta constructor]"
|
||||
"(https://pandas.pydata.org/docs/reference/api/pandas.Timedelta.html), "
|
||||
'e.g. `"10s"`, `"15 seconds"`, or `"1h23s"`. Got: {param_value}'
|
||||
)
|
||||
|
||||
|
||||
class MediaMixin:
|
||||
@gather_metrics("audio")
|
||||
def audio(
|
||||
self,
|
||||
data: MediaData,
|
||||
format: str = "audio/wav",
|
||||
start_time: MediaTime = 0,
|
||||
*,
|
||||
sample_rate: int | None = None,
|
||||
end_time: MediaTime | None = None,
|
||||
loop: bool = False,
|
||||
autoplay: bool = False,
|
||||
) -> DeltaGenerator:
|
||||
"""Display an audio player.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : str, Path, bytes, BytesIO, numpy.ndarray, or file
|
||||
The audio to play. This can be one of the following:
|
||||
|
||||
- A URL (string) for a hosted audio file.
|
||||
- A path to a local audio file. The path can be a ``str``
|
||||
or ``Path`` object. Paths can be absolute or relative to the
|
||||
working directory (where you execute ``streamlit run``).
|
||||
- Raw audio data. Raw data formats must include all necessary file
|
||||
headers to match the file format specified via ``format``.
|
||||
|
||||
If ``data`` is a NumPy array, it must either be a 1D array of the
|
||||
waveform or a 2D array of shape (C, S) where C is the number of
|
||||
channels and S is the number of samples. See the default channel
|
||||
order at
|
||||
http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
|
||||
|
||||
format : str
|
||||
The MIME type for the audio file. This defaults to ``"audio/wav"``.
|
||||
For more information about MIME types, see
|
||||
https://www.iana.org/assignments/media-types/media-types.xhtml.
|
||||
|
||||
start_time: int, float, timedelta, str, or None
|
||||
The time from which the element should start playing. This can be
|
||||
one of the following:
|
||||
|
||||
- ``None`` (default): The element plays from the beginning.
|
||||
- An ``int`` or ``float`` specifying the time in seconds. ``float``
|
||||
values are rounded down to whole seconds.
|
||||
- A string specifying the time in a format supported by `Pandas'
|
||||
Timedelta constructor <https://pandas.pydata.org/docs/reference/api/pandas.Timedelta.html>`_,
|
||||
e.g. ``"2 minute"``, ``"20s"``, or ``"1m14s"``.
|
||||
- A ``timedelta`` object from `Python's built-in datetime library
|
||||
<https://docs.python.org/3/library/datetime.html#timedelta-objects>`_,
|
||||
e.g. ``timedelta(seconds=70)``.
|
||||
sample_rate: int or None
|
||||
The sample rate of the audio data in samples per second. This is
|
||||
only required if ``data`` is a NumPy array.
|
||||
end_time: int, float, timedelta, str, or None
|
||||
The time at which the element should stop playing. This can be
|
||||
one of the following:
|
||||
|
||||
- ``None`` (default): The element plays through to the end.
|
||||
- An ``int`` or ``float`` specifying the time in seconds. ``float``
|
||||
values are rounded down to whole seconds.
|
||||
- A string specifying the time in a format supported by `Pandas'
|
||||
Timedelta constructor <https://pandas.pydata.org/docs/reference/api/pandas.Timedelta.html>`_,
|
||||
e.g. ``"2 minute"``, ``"20s"``, or ``"1m14s"``.
|
||||
- A ``timedelta`` object from `Python's built-in datetime library
|
||||
<https://docs.python.org/3/library/datetime.html#timedelta-objects>`_,
|
||||
e.g. ``timedelta(seconds=70)``.
|
||||
loop: bool
|
||||
Whether the audio should loop playback.
|
||||
autoplay: bool
|
||||
Whether the audio file should start playing automatically. This is
|
||||
``False`` by default. Browsers will not autoplay audio files if the
|
||||
user has not interacted with the page by clicking somewhere.
|
||||
|
||||
Examples
|
||||
--------
|
||||
To display an audio player for a local file, specify the file's string
|
||||
path and format.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.audio("cat-purr.mp3", format="audio/mpeg", loop=True)
|
||||
|
||||
.. output::
|
||||
https://doc-audio-purr.streamlit.app/
|
||||
height: 250px
|
||||
|
||||
You can also pass ``bytes`` or ``numpy.ndarray`` objects to ``st.audio``.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> audio_file = open("myaudio.ogg", "rb")
|
||||
>>> audio_bytes = audio_file.read()
|
||||
>>>
|
||||
>>> st.audio(audio_bytes, format="audio/ogg")
|
||||
>>>
|
||||
>>> sample_rate = 44100 # 44100 samples per second
|
||||
>>> seconds = 2 # Note duration of 2 seconds
|
||||
>>> frequency_la = 440 # Our played note will be 440 Hz
|
||||
>>> # Generate array with seconds*sample_rate steps, ranging between 0 and seconds
|
||||
>>> t = np.linspace(0, seconds, seconds * sample_rate, False)
|
||||
>>> # Generate a 440 Hz sine wave
|
||||
>>> note_la = np.sin(frequency_la * t * 2 * np.pi)
|
||||
>>>
|
||||
>>> st.audio(note_la, sample_rate=sample_rate)
|
||||
|
||||
.. output::
|
||||
https://doc-audio.streamlit.app/
|
||||
height: 865px
|
||||
|
||||
"""
|
||||
start_time, end_time = _parse_start_time_end_time(start_time, end_time)
|
||||
|
||||
audio_proto = AudioProto()
|
||||
|
||||
is_data_numpy_array = type_util.is_type(data, "numpy.ndarray")
|
||||
|
||||
if is_data_numpy_array and sample_rate is None:
|
||||
raise StreamlitAPIException(
|
||||
"`sample_rate` must be specified when `data` is a numpy array."
|
||||
)
|
||||
if not is_data_numpy_array and sample_rate is not None:
|
||||
self.dg.warning(
|
||||
"Warning: `sample_rate` will be ignored since data is not a numpy "
|
||||
"array."
|
||||
)
|
||||
coordinates = self.dg._get_delta_path_str()
|
||||
marshall_audio(
|
||||
coordinates,
|
||||
audio_proto,
|
||||
data,
|
||||
format,
|
||||
start_time,
|
||||
sample_rate,
|
||||
end_time,
|
||||
loop,
|
||||
autoplay,
|
||||
form_id=current_form_id(self.dg),
|
||||
)
|
||||
return self.dg._enqueue("audio", audio_proto)
|
||||
|
||||
@gather_metrics("video")
|
||||
def video(
|
||||
self,
|
||||
data: MediaData,
|
||||
format: str = "video/mp4",
|
||||
start_time: MediaTime = 0,
|
||||
*, # keyword-only arguments:
|
||||
subtitles: SubtitleData = None,
|
||||
end_time: MediaTime | None = None,
|
||||
loop: bool = False,
|
||||
autoplay: bool = False,
|
||||
muted: bool = False,
|
||||
) -> DeltaGenerator:
|
||||
"""Display a video player.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : str, Path, bytes, io.BytesIO, numpy.ndarray, or file
|
||||
The video to play. This can be one of the following:
|
||||
|
||||
- A URL (string) for a hosted video file, including YouTube URLs.
|
||||
- A path to a local video file. The path can be a ``str``
|
||||
or ``Path`` object. Paths can be absolute or relative to the
|
||||
working directory (where you execute ``streamlit run``).
|
||||
- Raw video data. Raw data formats must include all necessary file
|
||||
headers to match the file format specified via ``format``.
|
||||
|
||||
format : str
|
||||
The MIME type for the video file. This defaults to ``"video/mp4"``.
|
||||
For more information about MIME types, see
|
||||
https://www.iana.org/assignments/media-types/media-types.xhtml.
|
||||
|
||||
start_time: int, float, timedelta, str, or None
|
||||
The time from which the element should start playing. This can be
|
||||
one of the following:
|
||||
|
||||
- ``None`` (default): The element plays from the beginning.
|
||||
- An ``int`` or ``float`` specifying the time in seconds. ``float``
|
||||
values are rounded down to whole seconds.
|
||||
- A string specifying the time in a format supported by `Pandas'
|
||||
Timedelta constructor <https://pandas.pydata.org/docs/reference/api/pandas.Timedelta.html>`_,
|
||||
e.g. ``"2 minute"``, ``"20s"``, or ``"1m14s"``.
|
||||
- A ``timedelta`` object from `Python's built-in datetime library
|
||||
<https://docs.python.org/3/library/datetime.html#timedelta-objects>`_,
|
||||
e.g. ``timedelta(seconds=70)``.
|
||||
subtitles: str, bytes, Path, io.BytesIO, or dict
|
||||
Optional subtitle data for the video, supporting several input types:
|
||||
|
||||
- ``None`` (default): No subtitles.
|
||||
|
||||
- A string, bytes, or Path: File path to a subtitle file in
|
||||
``.vtt`` or ``.srt`` formats, or the raw content of subtitles
|
||||
conforming to these formats. Paths can be absolute or relative to
|
||||
the working directory (where you execute ``streamlit run``).
|
||||
If providing raw content, the string must adhere to the WebVTT or
|
||||
SRT format specifications.
|
||||
|
||||
- io.BytesIO: A BytesIO stream that contains valid ``.vtt`` or ``.srt``
|
||||
formatted subtitle data.
|
||||
|
||||
- A dictionary: Pairs of labels and file paths or raw subtitle content in
|
||||
``.vtt`` or ``.srt`` formats to enable multiple subtitle tracks.
|
||||
The label will be shown in the video player. Example:
|
||||
``{"English": "path/to/english.vtt", "French": "path/to/french.srt"}``
|
||||
|
||||
When provided, subtitles are displayed by default. For multiple
|
||||
tracks, the first one is displayed by default. If you don't want any
|
||||
subtitles displayed by default, use an empty string for the value
|
||||
in a dictrionary's first pair: ``{"None": "", "English": "path/to/english.vtt"}``
|
||||
|
||||
Not supported for YouTube videos.
|
||||
end_time: int, float, timedelta, str, or None
|
||||
The time at which the element should stop playing. This can be
|
||||
one of the following:
|
||||
|
||||
- ``None`` (default): The element plays through to the end.
|
||||
- An ``int`` or ``float`` specifying the time in seconds. ``float``
|
||||
values are rounded down to whole seconds.
|
||||
- A string specifying the time in a format supported by `Pandas'
|
||||
Timedelta constructor <https://pandas.pydata.org/docs/reference/api/pandas.Timedelta.html>`_,
|
||||
e.g. ``"2 minute"``, ``"20s"``, or ``"1m14s"``.
|
||||
- A ``timedelta`` object from `Python's built-in datetime library
|
||||
<https://docs.python.org/3/library/datetime.html#timedelta-objects>`_,
|
||||
e.g. ``timedelta(seconds=70)``.
|
||||
loop: bool
|
||||
Whether the video should loop playback.
|
||||
autoplay: bool
|
||||
Whether the video should start playing automatically. This is
|
||||
``False`` by default. Browsers will not autoplay unmuted videos
|
||||
if the user has not interacted with the page by clicking somewhere.
|
||||
To enable autoplay without user interaction, you must also set
|
||||
``muted=True``.
|
||||
muted: bool
|
||||
Whether the video should play with the audio silenced. This is
|
||||
``False`` by default. Use this in conjunction with ``autoplay=True``
|
||||
to enable autoplay without user interaction.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> video_file = open("myvideo.mp4", "rb")
|
||||
>>> video_bytes = video_file.read()
|
||||
>>>
|
||||
>>> st.video(video_bytes)
|
||||
|
||||
.. output::
|
||||
https://doc-video.streamlit.app/
|
||||
height: 700px
|
||||
|
||||
When you include subtitles, they will be turned on by default. A viewer
|
||||
can turn off the subtitles (or captions) from the browser's default video
|
||||
control menu, usually located in the lower-right corner of the video.
|
||||
|
||||
Here is a simple VTT file (``subtitles.vtt``):
|
||||
|
||||
>>> WEBVTT
|
||||
>>>
|
||||
>>> 0:00:01.000 --> 0:00:02.000
|
||||
>>> Look!
|
||||
>>>
|
||||
>>> 0:00:03.000 --> 0:00:05.000
|
||||
>>> Look at the pretty stars!
|
||||
|
||||
If the above VTT file lives in the same directory as your app, you can
|
||||
add subtitles like so:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> VIDEO_URL = "https://example.com/not-youtube.mp4"
|
||||
>>> st.video(VIDEO_URL, subtitles="subtitles.vtt")
|
||||
|
||||
.. output::
|
||||
https://doc-video-subtitles.streamlit.app/
|
||||
height: 700px
|
||||
|
||||
See additional examples of supported subtitle input types in our
|
||||
`video subtitles feature demo <https://doc-video-subtitle-inputs.streamlit.app/>`_.
|
||||
|
||||
.. note::
|
||||
Some videos may not display if they are encoded using MP4V (which is an export option in OpenCV), as this codec is
|
||||
not widely supported by browsers. Converting your video to H.264 will allow the video to be displayed in Streamlit.
|
||||
See this `StackOverflow post <https://stackoverflow.com/a/49535220/2394542>`_ or this
|
||||
`Streamlit forum post <https://discuss.streamlit.io/t/st-video-doesnt-show-opencv-generated-mp4/3193/2>`_
|
||||
for more information.
|
||||
|
||||
"""
|
||||
start_time, end_time = _parse_start_time_end_time(start_time, end_time)
|
||||
|
||||
video_proto = VideoProto()
|
||||
coordinates = self.dg._get_delta_path_str()
|
||||
marshall_video(
|
||||
coordinates,
|
||||
video_proto,
|
||||
data,
|
||||
format,
|
||||
start_time,
|
||||
subtitles,
|
||||
end_time,
|
||||
loop,
|
||||
autoplay,
|
||||
muted,
|
||||
form_id=current_form_id(self.dg),
|
||||
)
|
||||
return self.dg._enqueue("video", video_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
# Regular expression from
|
||||
# https://gist.github.com/rodrigoborgesdeoliveira/987683cfbfcc8d800192da1e73adc486?permalink_comment_id=4645864#gistcomment-4645864
|
||||
# Covers any youtube URL (incl. shortlinks and embed links) and extracts its video code.
|
||||
YOUTUBE_RE: Final = r"^((https?://(?:www\.)?(?:m\.)?youtube\.com))/((?:oembed\?url=https?%3A//(?:www\.)youtube.com/watch\?(?:v%3D)(?P<video_id_1>[\w\-]{10,20})&format=json)|(?:attribution_link\?a=.*watch(?:%3Fv%3D|%3Fv%3D)(?P<video_id_2>[\w\-]{10,20}))(?:%26feature.*))|(https?:)?(\/\/)?((www\.|m\.)?youtube(-nocookie)?\.com\/((watch)?\?(app=desktop&)?(feature=\w*&)?v=|embed\/|v\/|e\/)|youtu\.be\/)(?P<video_id_3>[\w\-]{10,20})"
|
||||
|
||||
|
||||
def _reshape_youtube_url(url: str) -> str | None:
|
||||
"""Return whether URL is any kind of YouTube embed or watch link. If so,
|
||||
reshape URL into an embed link suitable for use in an iframe.
|
||||
|
||||
If not a YouTube URL, return None.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
url : str
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> print(_reshape_youtube_url("https://youtu.be/_T8LGqJtuGc"))
|
||||
|
||||
.. output::
|
||||
https://www.youtube.com/embed/_T8LGqJtuGc
|
||||
"""
|
||||
match = re.match(YOUTUBE_RE, url)
|
||||
if match:
|
||||
code = (
|
||||
match.group("video_id_1")
|
||||
or match.group("video_id_2")
|
||||
or match.group("video_id_3")
|
||||
)
|
||||
return f"https://www.youtube.com/embed/{code}"
|
||||
return None
|
||||
|
||||
|
||||
def _marshall_av_media(
|
||||
coordinates: str,
|
||||
proto: AudioProto | VideoProto,
|
||||
data: MediaData,
|
||||
mimetype: str,
|
||||
) -> None:
|
||||
"""Fill audio or video proto based on contents of data.
|
||||
|
||||
Given a string, check if it's a url; if so, send it out without modification.
|
||||
Otherwise assume strings are filenames and let any OS errors raise.
|
||||
|
||||
Load data either from file or through bytes-processing methods into a
|
||||
MediaFile object. Pack proto with generated Tornado-based URL.
|
||||
|
||||
(When running in "raw" mode, we won't actually load data into the
|
||||
MediaFileManager, and we'll return an empty URL.)
|
||||
"""
|
||||
# Audio and Video methods have already checked if this is a URL by this point.
|
||||
|
||||
if data is None:
|
||||
# Allow empty values so media players can be shown without media.
|
||||
return
|
||||
|
||||
data_or_filename: bytes | str
|
||||
if isinstance(data, (str, bytes)):
|
||||
# Pass strings and bytes through unchanged
|
||||
data_or_filename = data
|
||||
elif isinstance(data, Path):
|
||||
data_or_filename = str(data)
|
||||
elif isinstance(data, io.BytesIO):
|
||||
data.seek(0)
|
||||
data_or_filename = data.getvalue()
|
||||
elif isinstance(data, io.RawIOBase) or isinstance(data, io.BufferedReader):
|
||||
data.seek(0)
|
||||
read_data = data.read()
|
||||
if read_data is None:
|
||||
return
|
||||
else:
|
||||
data_or_filename = read_data
|
||||
elif type_util.is_type(data, "numpy.ndarray"):
|
||||
data_or_filename = data.tobytes()
|
||||
else:
|
||||
raise RuntimeError("Invalid binary data format: %s" % type(data))
|
||||
|
||||
if runtime.exists():
|
||||
file_url = runtime.get_instance().media_file_mgr.add(
|
||||
data_or_filename, mimetype, coordinates
|
||||
)
|
||||
caching.save_media_data(data_or_filename, mimetype, coordinates)
|
||||
else:
|
||||
# When running in "raw mode", we can't access the MediaFileManager.
|
||||
file_url = ""
|
||||
|
||||
proto.url = file_url
|
||||
|
||||
|
||||
def marshall_video(
|
||||
coordinates: str,
|
||||
proto: VideoProto,
|
||||
data: MediaData,
|
||||
mimetype: str = "video/mp4",
|
||||
start_time: int = 0,
|
||||
subtitles: SubtitleData = None,
|
||||
end_time: int | None = None,
|
||||
loop: bool = False,
|
||||
autoplay: bool = False,
|
||||
muted: bool = False,
|
||||
form_id: str | None = None,
|
||||
) -> None:
|
||||
"""Marshalls a video proto, using url processors as needed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
coordinates : str
|
||||
proto : the proto to fill. Must have a string field called "data".
|
||||
data : str, Path, bytes, BytesIO, numpy.ndarray, or file opened with
|
||||
io.open().
|
||||
Raw video data or a string with a URL pointing to the video
|
||||
to load. Includes support for YouTube URLs.
|
||||
If passing the raw data, this must include headers and any other
|
||||
bytes required in the actual file.
|
||||
mimetype : str
|
||||
The mime type for the video file. Defaults to 'video/mp4'.
|
||||
See https://tools.ietf.org/html/rfc4281 for more info.
|
||||
start_time : int
|
||||
The time from which this element should start playing. (default: 0)
|
||||
subtitles: str, dict, or io.BytesIO
|
||||
Optional subtitle data for the video, supporting several input types:
|
||||
- None (default): No subtitles.
|
||||
- A string: File path to a subtitle file in '.vtt' or '.srt' formats, or the raw content of subtitles conforming to these formats.
|
||||
If providing raw content, the string must adhere to the WebVTT or SRT format specifications.
|
||||
- A dictionary: Pairs of labels and file paths or raw subtitle content in '.vtt' or '.srt' formats.
|
||||
Enables multiple subtitle tracks. The label will be shown in the video player.
|
||||
Example: {'English': 'path/to/english.vtt', 'French': 'path/to/french.srt'}
|
||||
- io.BytesIO: A BytesIO stream that contains valid '.vtt' or '.srt' formatted subtitle data.
|
||||
When provided, subtitles are displayed by default. For multiple tracks, the first one is displayed by default.
|
||||
Not supported for YouTube videos.
|
||||
end_time: int
|
||||
The time at which this element should stop playing
|
||||
loop: bool
|
||||
Whether the video should loop playback.
|
||||
autoplay: bool
|
||||
Whether the video should start playing automatically.
|
||||
Browsers will not autoplay video files if the user has not interacted with
|
||||
the page yet, for example by clicking on the page while it loads.
|
||||
To enable autoplay without user interaction, you can set muted=True.
|
||||
Defaults to False.
|
||||
muted: bool
|
||||
Whether the video should play with the audio silenced. This can be used to
|
||||
enable autoplay without user interaction. Defaults to False.
|
||||
form_id: str | None
|
||||
The ID of the form that this element is placed in. Provide None if
|
||||
the element is not placed in a form.
|
||||
"""
|
||||
|
||||
if start_time < 0 or (end_time is not None and end_time <= start_time):
|
||||
raise StreamlitAPIException("Invalid start_time and end_time combination.")
|
||||
|
||||
proto.start_time = start_time
|
||||
proto.muted = muted
|
||||
|
||||
if end_time is not None:
|
||||
proto.end_time = end_time
|
||||
proto.loop = loop
|
||||
|
||||
# "type" distinguishes between YouTube and non-YouTube links
|
||||
proto.type = VideoProto.Type.NATIVE
|
||||
|
||||
if isinstance(data, Path):
|
||||
data = str(data) # Convert Path to string
|
||||
|
||||
if isinstance(data, str) and url_util.is_url(
|
||||
data, allowed_schemas=("http", "https", "data")
|
||||
):
|
||||
if youtube_url := _reshape_youtube_url(data):
|
||||
proto.url = youtube_url
|
||||
proto.type = VideoProto.Type.YOUTUBE_IFRAME
|
||||
if subtitles:
|
||||
raise StreamlitAPIException(
|
||||
"Subtitles are not supported for YouTube videos."
|
||||
)
|
||||
else:
|
||||
proto.url = data
|
||||
else:
|
||||
_marshall_av_media(coordinates, proto, data, mimetype)
|
||||
|
||||
if subtitles:
|
||||
subtitle_items: list[tuple[str, str | Path | bytes | io.BytesIO]] = []
|
||||
|
||||
# Single subtitle
|
||||
if isinstance(subtitles, (str, bytes, io.BytesIO, Path)):
|
||||
subtitle_items.append(("default", subtitles))
|
||||
# Multiple subtitles
|
||||
elif isinstance(subtitles, dict):
|
||||
subtitle_items.extend(subtitles.items())
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
f"Unsupported data type for subtitles: {type(subtitles)}. "
|
||||
f"Only str (file paths) and dict are supported."
|
||||
)
|
||||
|
||||
for label, subtitle_data in subtitle_items:
|
||||
sub = proto.subtitles.add()
|
||||
sub.label = label or ""
|
||||
|
||||
# Coordinates used in media_file_manager to identify the place of
|
||||
# element, in case of subtitle, we use same video coordinates
|
||||
# with suffix.
|
||||
# It is not aligned with common coordinates format, but in
|
||||
# media_file_manager we use it just as unique identifier, so it is fine.
|
||||
subtitle_coordinates = f"{coordinates}[subtitle{label}]"
|
||||
try:
|
||||
sub.url = process_subtitle_data(
|
||||
subtitle_coordinates, subtitle_data, label
|
||||
)
|
||||
except (TypeError, ValueError) as original_err:
|
||||
raise StreamlitAPIException(
|
||||
f"Failed to process the provided subtitle: {label}"
|
||||
) from original_err
|
||||
|
||||
if autoplay:
|
||||
proto.autoplay = autoplay
|
||||
proto.id = compute_and_register_element_id(
|
||||
"video",
|
||||
# video does not yet allow setting a user-defined key
|
||||
user_key=None,
|
||||
form_id=form_id,
|
||||
url=proto.url,
|
||||
mimetype=mimetype,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
loop=loop,
|
||||
autoplay=autoplay,
|
||||
muted=muted,
|
||||
)
|
||||
|
||||
|
||||
def _parse_start_time_end_time(
|
||||
start_time: MediaTime, end_time: MediaTime | None
|
||||
) -> tuple[int, int | None]:
|
||||
"""Parse start_time and end_time and return them as int."""
|
||||
|
||||
try:
|
||||
maybe_start_time = time_to_seconds(start_time, coerce_none_to_inf=False)
|
||||
if maybe_start_time is None:
|
||||
raise ValueError
|
||||
start_time = int(maybe_start_time)
|
||||
except (StreamlitAPIException, ValueError):
|
||||
error_msg = TIMEDELTA_PARSE_ERROR_MESSAGE.format(
|
||||
param_name="start_time", param_value=start_time
|
||||
)
|
||||
raise StreamlitAPIException(error_msg) from None
|
||||
|
||||
try:
|
||||
end_time = time_to_seconds(end_time, coerce_none_to_inf=False)
|
||||
if end_time is not None:
|
||||
end_time = int(end_time)
|
||||
except StreamlitAPIException:
|
||||
error_msg = TIMEDELTA_PARSE_ERROR_MESSAGE.format(
|
||||
param_name="end_time", param_value=end_time
|
||||
)
|
||||
raise StreamlitAPIException(error_msg) from None
|
||||
|
||||
return start_time, end_time
|
||||
|
||||
|
||||
def _validate_and_normalize(data: npt.NDArray[Any]) -> tuple[bytes, int]:
|
||||
"""Validates and normalizes numpy array data.
|
||||
We validate numpy array shape (should be 1d or 2d)
|
||||
We normalize input data to int16 [-32768, 32767] range.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : numpy array
|
||||
numpy array to be validated and normalized
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple of (bytes, int)
|
||||
(bytes, nchan)
|
||||
where
|
||||
- bytes : bytes of normalized numpy array converted to int16
|
||||
- nchan : number of channels for audio signal. 1 for mono, or 2 for stereo.
|
||||
"""
|
||||
# we import numpy here locally to import it only when needed (when numpy array given
|
||||
# to st.audio data)
|
||||
import numpy as np
|
||||
|
||||
transformed_data: npt.NDArray[Any] = np.array(data, dtype=float)
|
||||
|
||||
if len(cast("NumpyShape", transformed_data.shape)) == 1:
|
||||
nchan = 1
|
||||
elif len(transformed_data.shape) == 2:
|
||||
# In wave files,channels are interleaved. E.g.,
|
||||
# "L1R1L2R2..." for stereo. See
|
||||
# http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
|
||||
# for channel ordering
|
||||
nchan = transformed_data.shape[0]
|
||||
transformed_data = transformed_data.T.ravel()
|
||||
else:
|
||||
raise StreamlitAPIException("Numpy array audio input must be a 1D or 2D array.")
|
||||
|
||||
if transformed_data.size == 0:
|
||||
return transformed_data.astype(np.int16).tobytes(), nchan
|
||||
|
||||
max_abs_value: npt.NDArray[Any] = np.max(np.abs(transformed_data))
|
||||
# 16-bit samples are stored as 2's-complement signed integers,
|
||||
# ranging from -32768 to 32767.
|
||||
# scaled_data is PCM 16 bit numpy array, that's why we multiply [-1, 1] float
|
||||
# values to 32_767 == 2 ** 15 - 1.
|
||||
np_array = (transformed_data / max_abs_value) * 32767
|
||||
scaled_data = np_array.astype(np.int16)
|
||||
return scaled_data.tobytes(), nchan
|
||||
|
||||
|
||||
def _make_wav(data: npt.NDArray[Any], sample_rate: int) -> bytes:
|
||||
"""
|
||||
Transform a numpy array to a PCM bytestring.
|
||||
|
||||
We use code from IPython display module to convert numpy array to wave bytes
|
||||
https://github.com/ipython/ipython/blob/1015c392f3d50cf4ff3e9f29beede8c1abfdcb2a/IPython/lib/display.py#L146
|
||||
"""
|
||||
# we import wave here locally to import it only when needed (when numpy array given
|
||||
# to st.audio data)
|
||||
import wave
|
||||
|
||||
scaled, nchan = _validate_and_normalize(data)
|
||||
|
||||
with io.BytesIO() as fp, wave.open(fp, mode="wb") as waveobj:
|
||||
waveobj.setnchannels(nchan)
|
||||
waveobj.setframerate(sample_rate)
|
||||
waveobj.setsampwidth(2)
|
||||
waveobj.setcomptype("NONE", "NONE")
|
||||
waveobj.writeframes(scaled)
|
||||
return fp.getvalue()
|
||||
|
||||
|
||||
def _maybe_convert_to_wav_bytes(data: MediaData, sample_rate: int | None) -> MediaData:
|
||||
"""Convert data to wav bytes if the data type is numpy array."""
|
||||
if type_util.is_type(data, "numpy.ndarray") and sample_rate is not None:
|
||||
data = _make_wav(cast("npt.NDArray[Any]", data), sample_rate)
|
||||
return data
|
||||
|
||||
|
||||
def marshall_audio(
|
||||
coordinates: str,
|
||||
proto: AudioProto,
|
||||
data: MediaData,
|
||||
mimetype: str = "audio/wav",
|
||||
start_time: int = 0,
|
||||
sample_rate: int | None = None,
|
||||
end_time: int | None = None,
|
||||
loop: bool = False,
|
||||
autoplay: bool = False,
|
||||
form_id: str | None = None,
|
||||
) -> None:
|
||||
"""Marshalls an audio proto, using data and url processors as needed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
coordinates : str
|
||||
proto : The proto to fill. Must have a string field called "url".
|
||||
data : str, Path, bytes, BytesIO, numpy.ndarray, or file opened with
|
||||
io.open()
|
||||
Raw audio data or a string with a URL pointing to the file to load.
|
||||
If passing the raw data, this must include headers and any other bytes
|
||||
required in the actual file.
|
||||
mimetype : str
|
||||
The mime type for the audio file. Defaults to "audio/wav".
|
||||
See https://tools.ietf.org/html/rfc4281 for more info.
|
||||
start_time : int
|
||||
The time from which this element should start playing. (default: 0)
|
||||
sample_rate: int or None
|
||||
Optional param to provide sample_rate in case of numpy array
|
||||
end_time: int
|
||||
The time at which this element should stop playing
|
||||
loop: bool
|
||||
Whether the audio should loop playback.
|
||||
autoplay : bool
|
||||
Whether the audio should start playing automatically.
|
||||
Browsers will not autoplay audio files if the user has not interacted with the page yet.
|
||||
form_id: str | None
|
||||
The ID of the form that this element is placed in. Provide None if
|
||||
the element is not placed in a form.
|
||||
"""
|
||||
|
||||
proto.start_time = start_time
|
||||
if end_time is not None:
|
||||
proto.end_time = end_time
|
||||
proto.loop = loop
|
||||
|
||||
if isinstance(data, Path):
|
||||
data = str(data) # Convert Path to string
|
||||
|
||||
if isinstance(data, str) and url_util.is_url(
|
||||
data, allowed_schemas=("http", "https", "data")
|
||||
):
|
||||
proto.url = data
|
||||
else:
|
||||
data = _maybe_convert_to_wav_bytes(data, sample_rate)
|
||||
_marshall_av_media(coordinates, proto, data, mimetype)
|
||||
|
||||
if autoplay:
|
||||
proto.autoplay = autoplay
|
||||
proto.id = compute_and_register_element_id(
|
||||
"audio",
|
||||
user_key=None,
|
||||
form_id=form_id,
|
||||
url=proto.url,
|
||||
mimetype=mimetype,
|
||||
start_time=start_time,
|
||||
sample_rate=sample_rate,
|
||||
end_time=end_time,
|
||||
loop=loop,
|
||||
autoplay=autoplay,
|
||||
)
|
||||
300
myenv/lib/python3.11/site-packages/streamlit/elements/metric.py
Normal file
300
myenv/lib/python3.11/site-packages/streamlit/elements/metric.py
Normal file
@@ -0,0 +1,300 @@
|
||||
# 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, Literal, Union, cast
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit.elements.lib.policies import maybe_raise_label_warnings
|
||||
from streamlit.elements.lib.utils import (
|
||||
LabelVisibility,
|
||||
get_label_visibility_proto_value,
|
||||
)
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.Metric_pb2 import Metric as MetricProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.string_util import clean_text
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy as np
|
||||
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
Value: TypeAlias = Union["np.integer[Any]", "np.floating[Any]", float, int, str, None]
|
||||
Delta: TypeAlias = Union[float, int, str, None]
|
||||
DeltaColor: TypeAlias = Literal["normal", "inverse", "off"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MetricColorAndDirection:
|
||||
color: MetricProto.MetricColor.ValueType
|
||||
direction: MetricProto.MetricDirection.ValueType
|
||||
|
||||
|
||||
class MetricMixin:
|
||||
@gather_metrics("metric")
|
||||
def metric(
|
||||
self,
|
||||
label: str,
|
||||
value: Value,
|
||||
delta: Delta = None,
|
||||
delta_color: DeltaColor = "normal",
|
||||
help: str | None = None,
|
||||
label_visibility: LabelVisibility = "visible",
|
||||
border: bool = False,
|
||||
) -> DeltaGenerator:
|
||||
r"""Display a metric in big bold font, with an optional indicator of how the metric changed.
|
||||
|
||||
Tip: If you want to display a large number, it may be a good idea to
|
||||
shorten it using packages like `millify <https://github.com/azaitsev/millify>`_
|
||||
or `numerize <https://github.com/davidsa03/numerize>`_. E.g. ``1234`` can be
|
||||
displayed as ``1.2k`` using ``st.metric("Short number", millify(1234))``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : str
|
||||
The header or title for the metric. 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.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
value : int, float, str, or None
|
||||
Value of the metric. None is rendered as a long dash.
|
||||
|
||||
delta : int, float, str, or None
|
||||
Indicator of how the metric changed, rendered with an arrow below
|
||||
the metric. If delta is negative (int/float) or starts with a minus
|
||||
sign (str), the arrow points down and the text is red; else the
|
||||
arrow points up and the text is green. If None (default), no delta
|
||||
indicator is shown.
|
||||
|
||||
delta_color : "normal", "inverse", or "off"
|
||||
If "normal" (default), the delta indicator is shown as described
|
||||
above. If "inverse", it is red when positive and green when
|
||||
negative. This is useful when a negative change is considered
|
||||
good, e.g. if cost decreased. If "off", delta is shown in gray
|
||||
regardless of its value.
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed next to the metric 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``.
|
||||
|
||||
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.
|
||||
|
||||
border : bool
|
||||
Whether to show a border around the metric container. If this is
|
||||
``False`` (default), no border is shown. If this is ``True``, a
|
||||
border is shown.
|
||||
|
||||
Examples
|
||||
--------
|
||||
**Example 1: Show a metric**
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.metric(label="Temperature", value="70 °F", delta="1.2 °F")
|
||||
|
||||
.. output::
|
||||
https://doc-metric-example1.streamlit.app/
|
||||
height: 210px
|
||||
|
||||
**Example 2: Create a row of metrics**
|
||||
|
||||
``st.metric`` looks especially nice in combination with ``st.columns``.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> col1, col2, col3 = st.columns(3)
|
||||
>>> col1.metric("Temperature", "70 °F", "1.2 °F")
|
||||
>>> col2.metric("Wind", "9 mph", "-8%")
|
||||
>>> col3.metric("Humidity", "86%", "4%")
|
||||
|
||||
.. output::
|
||||
https://doc-metric-example2.streamlit.app/
|
||||
height: 210px
|
||||
|
||||
**Example 3: Modify the delta indicator**
|
||||
|
||||
The delta indicator color can also be inverted or turned off.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.metric(label="Gas price", value=4, delta=-0.5, delta_color="inverse")
|
||||
>>>
|
||||
>>> st.metric(
|
||||
... label="Active developers", value=123, delta=123, delta_color="off"
|
||||
... )
|
||||
|
||||
.. output::
|
||||
https://doc-metric-example3.streamlit.app/
|
||||
height: 320px
|
||||
|
||||
**Example 4: Create a grid of metric cards**
|
||||
|
||||
Add borders to your metrics to create a dashboard look.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> a, b = st.columns(2)
|
||||
>>> c, d = st.columns(2)
|
||||
>>>
|
||||
>>> a.metric("Temperature", "30°F", "-9°F", border=True)
|
||||
>>> b.metric("Wind", "4 mph", "2 mph", border=True)
|
||||
>>>
|
||||
>>> c.metric("Humidity", "77%", "5%", border=True)
|
||||
>>> d.metric("Pressure", "30.34 inHg", "-2 inHg", border=True)
|
||||
|
||||
.. output::
|
||||
https://doc-metric-example4.streamlit.app/
|
||||
height: 350px
|
||||
|
||||
"""
|
||||
maybe_raise_label_warnings(label, label_visibility)
|
||||
|
||||
metric_proto = MetricProto()
|
||||
metric_proto.body = _parse_value(value)
|
||||
metric_proto.label = _parse_label(label)
|
||||
metric_proto.delta = _parse_delta(delta)
|
||||
metric_proto.show_border = border
|
||||
if help is not None:
|
||||
metric_proto.help = dedent(help)
|
||||
|
||||
color_and_direction = _determine_delta_color_and_direction(
|
||||
cast("DeltaColor", clean_text(delta_color)), delta
|
||||
)
|
||||
metric_proto.color = color_and_direction.color
|
||||
metric_proto.direction = color_and_direction.direction
|
||||
metric_proto.label_visibility.value = get_label_visibility_proto_value(
|
||||
label_visibility
|
||||
)
|
||||
|
||||
return self.dg._enqueue("metric", metric_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
def _parse_label(label: str) -> str:
|
||||
if not isinstance(label, str):
|
||||
raise TypeError(
|
||||
f"'{str(label)}' is of type {str(type(label))}, which is not an accepted type."
|
||||
" label only accepts: str. Please convert the label to an accepted type."
|
||||
)
|
||||
return label
|
||||
|
||||
|
||||
def _parse_value(value: Value) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
if isinstance(value, int) or isinstance(value, float) or isinstance(value, str):
|
||||
return str(value)
|
||||
elif hasattr(value, "item"):
|
||||
# Add support for numpy values (e.g. int16, float64, etc.)
|
||||
try:
|
||||
# Item could also be just a variable, so we use try, except
|
||||
if isinstance(value.item(), float) or isinstance(value.item(), int):
|
||||
return str(value.item())
|
||||
except Exception:
|
||||
# If the numpy item is not a valid value, the TypeError below will be raised.
|
||||
pass
|
||||
|
||||
raise TypeError(
|
||||
f"'{str(value)}' is of type {str(type(value))}, which is not an accepted type."
|
||||
" value only accepts: int, float, str, or None."
|
||||
" Please convert the value to an accepted type."
|
||||
)
|
||||
|
||||
|
||||
def _parse_delta(delta: Delta) -> str:
|
||||
if delta is None or delta == "":
|
||||
return ""
|
||||
if isinstance(delta, str):
|
||||
return dedent(delta)
|
||||
elif isinstance(delta, int) or isinstance(delta, float):
|
||||
return str(delta)
|
||||
else:
|
||||
raise TypeError(
|
||||
f"'{str(delta)}' is of type {str(type(delta))}, which is not an accepted type."
|
||||
" delta only accepts: int, float, str, or None."
|
||||
" Please convert the value to an accepted type."
|
||||
)
|
||||
|
||||
|
||||
def _determine_delta_color_and_direction(
|
||||
delta_color: DeltaColor,
|
||||
delta: Delta,
|
||||
) -> MetricColorAndDirection:
|
||||
if delta_color not in {"normal", "inverse", "off"}:
|
||||
raise StreamlitAPIException(
|
||||
f"'{str(delta_color)}' is not an accepted value. delta_color only accepts: "
|
||||
"'normal', 'inverse', or 'off'"
|
||||
)
|
||||
|
||||
if delta is None or delta == "":
|
||||
return MetricColorAndDirection(
|
||||
color=MetricProto.MetricColor.GRAY,
|
||||
direction=MetricProto.MetricDirection.NONE,
|
||||
)
|
||||
|
||||
if _is_negative_delta(delta):
|
||||
if delta_color == "normal":
|
||||
cd_color = MetricProto.MetricColor.RED
|
||||
elif delta_color == "inverse":
|
||||
cd_color = MetricProto.MetricColor.GREEN
|
||||
else:
|
||||
cd_color = MetricProto.MetricColor.GRAY
|
||||
cd_direction = MetricProto.MetricDirection.DOWN
|
||||
else:
|
||||
if delta_color == "normal":
|
||||
cd_color = MetricProto.MetricColor.GREEN
|
||||
elif delta_color == "inverse":
|
||||
cd_color = MetricProto.MetricColor.RED
|
||||
else:
|
||||
cd_color = MetricProto.MetricColor.GRAY
|
||||
cd_direction = MetricProto.MetricDirection.UP
|
||||
|
||||
return MetricColorAndDirection(
|
||||
color=cd_color,
|
||||
direction=cd_direction,
|
||||
)
|
||||
|
||||
|
||||
def _is_negative_delta(delta: Delta) -> bool:
|
||||
return dedent(str(delta)).startswith("-")
|
||||
@@ -0,0 +1,546 @@
|
||||
# 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.
|
||||
|
||||
"""Streamlit support for Plotly charts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Final,
|
||||
Literal,
|
||||
TypedDict,
|
||||
Union,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit import type_util
|
||||
from streamlit.deprecation_util import show_deprecation_warning
|
||||
from streamlit.elements.lib.event_utils import AttributeDictionary
|
||||
from streamlit.elements.lib.form_utils import current_form_id
|
||||
from streamlit.elements.lib.policies import check_widget_policies
|
||||
from streamlit.elements.lib.streamlit_plotly_theme import (
|
||||
configure_streamlit_plotly_theme,
|
||||
)
|
||||
from streamlit.elements.lib.utils import Key, compute_and_register_element_id, to_key
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.PlotlyChart_pb2 import PlotlyChart as PlotlyChartProto
|
||||
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 WidgetCallback, register_widget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable
|
||||
|
||||
import matplotlib as mpl
|
||||
import plotly.graph_objs as go
|
||||
from plotly.basedatatypes import BaseFigure
|
||||
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
# We need to configure the Plotly theme before any Plotly figures are created:
|
||||
configure_streamlit_plotly_theme()
|
||||
|
||||
_AtomicFigureOrData: TypeAlias = Union[
|
||||
"go.Figure",
|
||||
"go.Data",
|
||||
]
|
||||
FigureOrData: TypeAlias = Union[
|
||||
_AtomicFigureOrData,
|
||||
list[_AtomicFigureOrData],
|
||||
# It is kind of hard to figure out exactly what kind of dict is supported
|
||||
# here, as plotly hasn't embraced typing yet. This version is chosen to
|
||||
# align with the docstring.
|
||||
dict[str, _AtomicFigureOrData],
|
||||
"BaseFigure",
|
||||
"mpl.figure.Figure",
|
||||
]
|
||||
|
||||
SelectionMode: TypeAlias = Literal["lasso", "points", "box"]
|
||||
_SELECTION_MODES: Final[set[SelectionMode]] = {"lasso", "points", "box"}
|
||||
|
||||
|
||||
class PlotlySelectionState(TypedDict, total=False):
|
||||
"""
|
||||
The schema for the Plotly chart selection state.
|
||||
|
||||
The selection state is stored in a dictionary-like object that supports both
|
||||
key and attribute notation. Selection states cannot be programmatically
|
||||
changed or set through Session State.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
points : list[dict[str, Any]]
|
||||
The selected data points in the chart, including the data points
|
||||
selected by the box and lasso mode. The data includes the values
|
||||
associated to each point and a point index used to populate
|
||||
``point_indices``. If additional information has been assigned to your
|
||||
points, such as size or legend group, this is also included.
|
||||
|
||||
point_indices : list[int]
|
||||
The numerical indices of all selected data points in the chart. The
|
||||
details of each identified point are included in ``points``.
|
||||
|
||||
box : list[dict[str, Any]]
|
||||
The metadata related to the box selection. This includes the
|
||||
coordinates of the selected area.
|
||||
|
||||
lasso : list[dict[str, Any]]
|
||||
The metadata related to the lasso selection. This includes the
|
||||
coordinates of the selected area.
|
||||
|
||||
Example
|
||||
-------
|
||||
When working with more complicated graphs, the ``points`` attribute
|
||||
displays additional information. Try selecting points in the following
|
||||
example:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import plotly.express as px
|
||||
>>>
|
||||
>>> df = px.data.iris()
|
||||
>>> fig = px.scatter(
|
||||
... df,
|
||||
... x="sepal_width",
|
||||
... y="sepal_length",
|
||||
... color="species",
|
||||
... size="petal_length",
|
||||
... hover_data=["petal_width"],
|
||||
... )
|
||||
>>>
|
||||
>>> event = st.plotly_chart(fig, key="iris", on_select="rerun")
|
||||
>>>
|
||||
>>> event.selection
|
||||
|
||||
.. output::
|
||||
https://doc-chart-events-plotly-selection-state.streamlit.app
|
||||
height: 600px
|
||||
|
||||
This is an example of the selection state when selecting a single point:
|
||||
|
||||
>>> {
|
||||
>>> "points": [
|
||||
>>> {
|
||||
>>> "curve_number": 2,
|
||||
>>> "point_number": 9,
|
||||
>>> "point_index": 9,
|
||||
>>> "x": 3.6,
|
||||
>>> "y": 7.2,
|
||||
>>> "customdata": [
|
||||
>>> 2.5
|
||||
>>> ],
|
||||
>>> "marker_size": 6.1,
|
||||
>>> "legendgroup": "virginica"
|
||||
>>> }
|
||||
>>> ],
|
||||
>>> "point_indices": [
|
||||
>>> 9
|
||||
>>> ],
|
||||
>>> "box": [],
|
||||
>>> "lasso": []
|
||||
>>> }
|
||||
|
||||
"""
|
||||
|
||||
points: list[dict[str, Any]]
|
||||
point_indices: list[int]
|
||||
box: list[dict[str, Any]]
|
||||
lasso: list[dict[str, Any]]
|
||||
|
||||
|
||||
class PlotlyState(TypedDict, total=False):
|
||||
"""
|
||||
The schema for the Plotly chart event state.
|
||||
|
||||
The event state is stored in a dictionary-like object that supports both
|
||||
key and attribute notation. Event states cannot be programmatically
|
||||
changed or set through Session State.
|
||||
|
||||
Only selection events are supported at this time.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
selection : dict
|
||||
The state of the ``on_select`` event. This attribute returns a
|
||||
dictionary-like object that supports both key and attribute notation.
|
||||
The attributes are described by the ``PlotlySelectionState`` dictionary
|
||||
schema.
|
||||
|
||||
Example
|
||||
-------
|
||||
Try selecting points by any of the three available methods (direct click,
|
||||
box, or lasso). The current selection state is available through Session
|
||||
State or as the output of the chart function.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import plotly.express as px
|
||||
>>>
|
||||
>>> df = px.data.iris() # iris is a pandas DataFrame
|
||||
>>> fig = px.scatter(df, x="sepal_width", y="sepal_length")
|
||||
>>>
|
||||
>>> event = st.plotly_chart(fig, key="iris", on_select="rerun")
|
||||
>>>
|
||||
>>> event
|
||||
|
||||
.. output::
|
||||
https://doc-chart-events-plotly-state.streamlit.app
|
||||
height: 600px
|
||||
|
||||
"""
|
||||
|
||||
selection: PlotlySelectionState
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlotlyChartSelectionSerde:
|
||||
"""PlotlyChartSelectionSerde is used to serialize and deserialize the Plotly Chart
|
||||
selection state.
|
||||
"""
|
||||
|
||||
def deserialize(self, ui_value: str | None, widget_id: str = "") -> PlotlyState:
|
||||
empty_selection_state: PlotlyState = {
|
||||
"selection": {
|
||||
"points": [],
|
||||
"point_indices": [],
|
||||
"box": [],
|
||||
"lasso": [],
|
||||
},
|
||||
}
|
||||
|
||||
selection_state = (
|
||||
empty_selection_state
|
||||
if ui_value is None
|
||||
else cast("PlotlyState", AttributeDictionary(json.loads(ui_value)))
|
||||
)
|
||||
|
||||
if "selection" not in selection_state:
|
||||
selection_state = empty_selection_state
|
||||
|
||||
return cast("PlotlyState", AttributeDictionary(selection_state))
|
||||
|
||||
def serialize(self, selection_state: PlotlyState) -> str:
|
||||
return json.dumps(selection_state, default=str)
|
||||
|
||||
|
||||
def parse_selection_mode(
|
||||
selection_mode: SelectionMode | Iterable[SelectionMode],
|
||||
) -> set[PlotlyChartProto.SelectionMode.ValueType]:
|
||||
"""Parse and check the user provided selection modes."""
|
||||
if isinstance(selection_mode, str):
|
||||
# Only a single selection mode was passed
|
||||
selection_mode_set = {selection_mode}
|
||||
else:
|
||||
# Multiple selection modes were passed
|
||||
selection_mode_set = set(selection_mode)
|
||||
|
||||
if not selection_mode_set.issubset(_SELECTION_MODES):
|
||||
raise StreamlitAPIException(
|
||||
f"Invalid selection mode: {selection_mode}. "
|
||||
f"Valid options are: {_SELECTION_MODES}"
|
||||
)
|
||||
|
||||
parsed_selection_modes = []
|
||||
for selection_mode in selection_mode_set:
|
||||
if selection_mode == "points":
|
||||
parsed_selection_modes.append(PlotlyChartProto.SelectionMode.POINTS)
|
||||
elif selection_mode == "lasso":
|
||||
parsed_selection_modes.append(PlotlyChartProto.SelectionMode.LASSO)
|
||||
elif selection_mode == "box":
|
||||
parsed_selection_modes.append(PlotlyChartProto.SelectionMode.BOX)
|
||||
return set(parsed_selection_modes)
|
||||
|
||||
|
||||
class PlotlyMixin:
|
||||
@overload
|
||||
def plotly_chart(
|
||||
self,
|
||||
figure_or_data: FigureOrData,
|
||||
use_container_width: bool = True,
|
||||
*,
|
||||
theme: Literal["streamlit"] | None = "streamlit",
|
||||
key: Key | None = None,
|
||||
on_select: Literal["ignore"], # No default value here to make it work with mypy
|
||||
selection_mode: SelectionMode | Iterable[SelectionMode] = (
|
||||
"points",
|
||||
"box",
|
||||
"lasso",
|
||||
),
|
||||
**kwargs: Any,
|
||||
) -> DeltaGenerator: ...
|
||||
|
||||
@overload
|
||||
def plotly_chart(
|
||||
self,
|
||||
figure_or_data: FigureOrData,
|
||||
use_container_width: bool = True,
|
||||
*,
|
||||
theme: Literal["streamlit"] | None = "streamlit",
|
||||
key: Key | None = None,
|
||||
on_select: Literal["rerun"] | WidgetCallback = "rerun",
|
||||
selection_mode: SelectionMode | Iterable[SelectionMode] = (
|
||||
"points",
|
||||
"box",
|
||||
"lasso",
|
||||
),
|
||||
**kwargs: Any,
|
||||
) -> PlotlyState: ...
|
||||
|
||||
@gather_metrics("plotly_chart")
|
||||
def plotly_chart(
|
||||
self,
|
||||
figure_or_data: FigureOrData,
|
||||
use_container_width: bool = True,
|
||||
*,
|
||||
theme: Literal["streamlit"] | None = "streamlit",
|
||||
key: Key | None = None,
|
||||
on_select: Literal["rerun", "ignore"] | WidgetCallback = "ignore",
|
||||
selection_mode: SelectionMode | Iterable[SelectionMode] = (
|
||||
"points",
|
||||
"box",
|
||||
"lasso",
|
||||
),
|
||||
**kwargs: Any,
|
||||
) -> DeltaGenerator | PlotlyState:
|
||||
"""Display an interactive Plotly chart.
|
||||
|
||||
`Plotly <https://plot.ly/python>`_ is a charting library for Python.
|
||||
The arguments to this function closely follow the ones for Plotly's
|
||||
``plot()`` function.
|
||||
|
||||
To show Plotly charts in Streamlit, call ``st.plotly_chart`` wherever
|
||||
you would call Plotly's ``py.plot`` or ``py.iplot``.
|
||||
|
||||
.. Important::
|
||||
You must install ``plotly`` to use this command. Your app's
|
||||
performance may be enhanced by installing ``orjson`` as well.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
figure_or_data : plotly.graph_objs.Figure, plotly.graph_objs.Data,\
|
||||
or dict/list of plotly.graph_objs.Figure/Data
|
||||
|
||||
The Plotly ``Figure`` or ``Data`` object to render. See
|
||||
https://plot.ly/python/ for examples of graph descriptions.
|
||||
|
||||
use_container_width : bool
|
||||
Whether to override the figure's native width with the width of
|
||||
the parent container. If ``use_container_width`` is ``True`` (default),
|
||||
Streamlit sets the width of the figure to match the width of the parent
|
||||
container. If ``use_container_width`` is ``False``, Streamlit sets the
|
||||
width of the chart to fit its contents according to the plotting library,
|
||||
up to the width of the parent container.
|
||||
|
||||
theme : "streamlit" or None
|
||||
The theme of the chart. If ``theme`` is ``"streamlit"`` (default),
|
||||
Streamlit uses its own design default. If ``theme`` is ``None``,
|
||||
Streamlit falls back to the default behavior of the library.
|
||||
|
||||
key : str
|
||||
An optional string to use for giving this element a stable
|
||||
identity. If ``key`` is ``None`` (default), this element's identity
|
||||
will be determined based on the values of the other parameters.
|
||||
|
||||
Additionally, if selections are activated and ``key`` is provided,
|
||||
Streamlit will register the key in Session State to store the
|
||||
selection state. The selection state is read-only.
|
||||
|
||||
on_select : "ignore" or "rerun" or callable
|
||||
How the figure should respond to user selection events. This
|
||||
controls whether or not the figure behaves like an input widget.
|
||||
``on_select`` can be one of the following:
|
||||
|
||||
- ``"ignore"`` (default): Streamlit will not react to any selection
|
||||
events in the chart. The figure will not behave like an input
|
||||
widget.
|
||||
|
||||
- ``"rerun"``: Streamlit will rerun the app when the user selects
|
||||
data in the chart. In this case, ``st.plotly_chart`` will return
|
||||
the selection data as a dictionary.
|
||||
|
||||
- A ``callable``: Streamlit will rerun the app and execute the
|
||||
``callable`` as a callback function before the rest of the app.
|
||||
In this case, ``st.plotly_chart`` will return the selection data
|
||||
as a dictionary.
|
||||
|
||||
selection_mode : "points", "box", "lasso" or an Iterable of these
|
||||
The selection mode of the chart. This can be one of the following:
|
||||
|
||||
- ``"points"``: The chart will allow selections based on individual
|
||||
data points.
|
||||
- ``"box"``: The chart will allow selections based on rectangular
|
||||
areas.
|
||||
- ``"lasso"``: The chart will allow selections based on freeform
|
||||
areas.
|
||||
- An ``Iterable`` of the above options: The chart will allow
|
||||
selections based on the modes specified.
|
||||
|
||||
All selections modes are activated by default.
|
||||
|
||||
**kwargs
|
||||
Any argument accepted by Plotly's ``plot()`` function.
|
||||
|
||||
Returns
|
||||
-------
|
||||
element or dict
|
||||
If ``on_select`` is ``"ignore"`` (default), this command returns an
|
||||
internal placeholder for the chart element. Otherwise, this command
|
||||
returns a dictionary-like object that supports both key and
|
||||
attribute notation. The attributes are described by the
|
||||
``PlotlyState`` dictionary schema.
|
||||
|
||||
Example
|
||||
-------
|
||||
The example below comes straight from the examples at
|
||||
https://plot.ly/python. Note that ``plotly.figure_factory`` requires
|
||||
``scipy`` to run.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import numpy as np
|
||||
>>> import plotly.figure_factory as ff
|
||||
>>>
|
||||
>>> # Add histogram data
|
||||
>>> x1 = np.random.randn(200) - 2
|
||||
>>> x2 = np.random.randn(200)
|
||||
>>> x3 = np.random.randn(200) + 2
|
||||
>>>
|
||||
>>> # Group data together
|
||||
>>> hist_data = [x1, x2, x3]
|
||||
>>>
|
||||
>>> group_labels = ['Group 1', 'Group 2', 'Group 3']
|
||||
>>>
|
||||
>>> # Create distplot with custom bin_size
|
||||
>>> fig = ff.create_distplot(
|
||||
... hist_data, group_labels, bin_size=[.1, .25, .5])
|
||||
>>>
|
||||
>>> # Plot!
|
||||
>>> st.plotly_chart(fig)
|
||||
|
||||
.. output::
|
||||
https://doc-plotly-chart.streamlit.app/
|
||||
height: 550px
|
||||
|
||||
"""
|
||||
import plotly.io
|
||||
import plotly.tools
|
||||
|
||||
# NOTE: "figure_or_data" is the name used in Plotly's .plot() method
|
||||
# for their main parameter. I don't like the name, but it's best to
|
||||
# keep it in sync with what Plotly calls it.
|
||||
|
||||
if "sharing" in kwargs:
|
||||
show_deprecation_warning(
|
||||
"The `sharing` parameter has been deprecated and will be removed "
|
||||
"in a future release. Plotly charts will always be rendered using "
|
||||
"Streamlit's offline mode."
|
||||
)
|
||||
|
||||
if theme not in ["streamlit", None]:
|
||||
raise StreamlitAPIException(
|
||||
f'You set theme="{theme}" while Streamlit charts only support '
|
||||
"theme=”streamlit” or theme=None to fallback to the default "
|
||||
"library theme."
|
||||
)
|
||||
|
||||
if on_select not in ["ignore", "rerun"] and not callable(on_select):
|
||||
raise StreamlitAPIException(
|
||||
f"You have passed {on_select} to `on_select`. But only 'ignore', "
|
||||
"'rerun', or a callable is supported."
|
||||
)
|
||||
|
||||
key = to_key(key)
|
||||
is_selection_activated = on_select != "ignore"
|
||||
|
||||
if is_selection_activated:
|
||||
# Run some checks that are only relevant when selections are activated
|
||||
|
||||
is_callback = callable(on_select)
|
||||
check_widget_policies(
|
||||
self.dg,
|
||||
key,
|
||||
on_change=cast("WidgetCallback", on_select) if is_callback else None,
|
||||
default_value=None,
|
||||
writes_allowed=False,
|
||||
enable_check_callback_rules=is_callback,
|
||||
)
|
||||
|
||||
if type_util.is_type(figure_or_data, "matplotlib.figure.Figure"):
|
||||
# Convert matplotlib figure to plotly figure:
|
||||
figure = plotly.tools.mpl_to_plotly(figure_or_data)
|
||||
else:
|
||||
figure = plotly.tools.return_figure_from_figure_or_data(
|
||||
figure_or_data, validate_figure=True
|
||||
)
|
||||
|
||||
plotly_chart_proto = PlotlyChartProto()
|
||||
plotly_chart_proto.use_container_width = use_container_width
|
||||
plotly_chart_proto.theme = theme or ""
|
||||
plotly_chart_proto.form_id = current_form_id(self.dg)
|
||||
|
||||
config = dict(kwargs.get("config", {}))
|
||||
# Copy over some kwargs to config dict. Plotly does the same in plot().
|
||||
config.setdefault("showLink", kwargs.get("show_link", False))
|
||||
config.setdefault("linkText", kwargs.get("link_text", False))
|
||||
|
||||
plotly_chart_proto.spec = plotly.io.to_json(figure, validate=False)
|
||||
plotly_chart_proto.config = json.dumps(config)
|
||||
|
||||
ctx = get_script_run_ctx()
|
||||
|
||||
# We are computing the widget id for all plotly uses
|
||||
# to also allow non-widget Plotly charts to keep their state
|
||||
# when the frontend component gets unmounted and remounted.
|
||||
plotly_chart_proto.id = compute_and_register_element_id(
|
||||
"plotly_chart",
|
||||
user_key=key,
|
||||
form_id=plotly_chart_proto.form_id,
|
||||
plotly_spec=plotly_chart_proto.spec,
|
||||
plotly_config=plotly_chart_proto.config,
|
||||
selection_mode=selection_mode,
|
||||
is_selection_activated=is_selection_activated,
|
||||
theme=theme,
|
||||
use_container_width=use_container_width,
|
||||
)
|
||||
|
||||
if is_selection_activated:
|
||||
# Selections are activated, treat plotly chart as a widget:
|
||||
plotly_chart_proto.selection_mode.extend(
|
||||
parse_selection_mode(selection_mode)
|
||||
)
|
||||
|
||||
serde = PlotlyChartSelectionSerde()
|
||||
|
||||
widget_state = register_widget(
|
||||
plotly_chart_proto.id,
|
||||
on_change_handler=on_select if callable(on_select) else None,
|
||||
deserializer=serde.deserialize,
|
||||
serializer=serde.serialize,
|
||||
ctx=ctx,
|
||||
value_type="string_value",
|
||||
)
|
||||
|
||||
self.dg._enqueue("plotly_chart", plotly_chart_proto)
|
||||
return cast("PlotlyState", widget_state.value)
|
||||
else:
|
||||
return self.dg._enqueue("plotly_chart", plotly_chart_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
@@ -0,0 +1,156 @@
|
||||
# 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 math
|
||||
from typing import TYPE_CHECKING, Union, cast
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.Progress_pb2 import Progress as ProgressProto
|
||||
from streamlit.string_util import clean_text
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
# Currently, equates to just float, but we can't use `numbers.Real` due to
|
||||
# https://github.com/python/mypy/issues/3186
|
||||
FloatOrInt: TypeAlias = Union[int, float]
|
||||
|
||||
|
||||
def _check_float_between(value: float, low: float = 0.0, high: float = 1.0) -> bool:
|
||||
"""
|
||||
Checks given value is 'between' the bounds of [low, high],
|
||||
considering close values around bounds are acceptable input.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This check is required for handling values that are slightly above or below the
|
||||
acceptable range, for example -0.0000000000021, 1.0000000000000013.
|
||||
These values are little off the conventional 0.0 <= x <= 1.0 condition
|
||||
due to floating point operations, but should still be considered acceptable input.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : float
|
||||
low : float
|
||||
high : float
|
||||
|
||||
"""
|
||||
return (
|
||||
(low <= value <= high)
|
||||
or math.isclose(value, low, rel_tol=1e-9, abs_tol=1e-9)
|
||||
or math.isclose(value, high, rel_tol=1e-9, abs_tol=1e-9)
|
||||
)
|
||||
|
||||
|
||||
def _get_value(value):
|
||||
if isinstance(value, int):
|
||||
if 0 <= value <= 100:
|
||||
return value
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
"Progress Value has invalid value [0, 100]: %d" % value
|
||||
)
|
||||
|
||||
elif isinstance(value, float):
|
||||
if _check_float_between(value, low=0.0, high=1.0):
|
||||
return int(value * 100)
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
"Progress Value has invalid value [0.0, 1.0]: %f" % value
|
||||
)
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
"Progress Value has invalid type: %s" % type(value).__name__
|
||||
)
|
||||
|
||||
|
||||
def _get_text(text: str | None) -> str | None:
|
||||
if text is None:
|
||||
return None
|
||||
if isinstance(text, str):
|
||||
return clean_text(text)
|
||||
raise StreamlitAPIException(
|
||||
f"Progress Text is of type {str(type(text))}, which is not an accepted type."
|
||||
"Text only accepts: str. Please convert the text to an accepted type."
|
||||
)
|
||||
|
||||
|
||||
class ProgressMixin:
|
||||
def progress(self, value: FloatOrInt, text: str | None = None) -> DeltaGenerator:
|
||||
r"""Display a progress bar.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : int or float
|
||||
0 <= value <= 100 for int
|
||||
|
||||
0.0 <= value <= 1.0 for float
|
||||
|
||||
text : str or None
|
||||
A message to display above the progress bar. The text 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.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
Example
|
||||
-------
|
||||
Here is an example of a progress bar increasing over time and disappearing when it reaches completion:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import time
|
||||
>>>
|
||||
>>> progress_text = "Operation in progress. Please wait."
|
||||
>>> my_bar = st.progress(0, text=progress_text)
|
||||
>>>
|
||||
>>> for percent_complete in range(100):
|
||||
... time.sleep(0.01)
|
||||
... my_bar.progress(percent_complete + 1, text=progress_text)
|
||||
>>> time.sleep(1)
|
||||
>>> my_bar.empty()
|
||||
>>>
|
||||
>>> st.button("Rerun")
|
||||
|
||||
.. output::
|
||||
https://doc-status-progress.streamlit.app/
|
||||
height: 220px
|
||||
|
||||
"""
|
||||
# TODO: standardize numerical type checking across st.* functions.
|
||||
progress_proto = ProgressProto()
|
||||
progress_proto.value = _get_value(value)
|
||||
text = _get_text(text)
|
||||
if text is not None:
|
||||
progress_proto.text = text
|
||||
return self.dg._enqueue("progress", progress_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
194
myenv/lib/python3.11/site-packages/streamlit/elements/pyplot.py
Normal file
194
myenv/lib/python3.11/site-packages/streamlit/elements/pyplot.py
Normal file
@@ -0,0 +1,194 @@
|
||||
# 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.
|
||||
|
||||
"""Streamlit support for Matplotlib PyPlot charts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from streamlit.deprecation_util import show_deprecation_warning
|
||||
from streamlit.elements.lib.image_utils import WidthBehavior, marshall_images
|
||||
from streamlit.proto.Image_pb2 import ImageList as ImageListProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from matplotlib.figure import Figure
|
||||
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
class PyplotMixin:
|
||||
@gather_metrics("pyplot")
|
||||
def pyplot(
|
||||
self,
|
||||
fig: Figure | None = None,
|
||||
clear_figure: bool | None = None,
|
||||
use_container_width: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> DeltaGenerator:
|
||||
"""Display a matplotlib.pyplot figure.
|
||||
|
||||
.. Important::
|
||||
You must install ``matplotlib`` to use this command.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fig : Matplotlib Figure
|
||||
The Matplotlib ``Figure`` object to render. See
|
||||
https://matplotlib.org/stable/gallery/index.html for examples.
|
||||
|
||||
.. note::
|
||||
When this argument isn't specified, this function will render the global
|
||||
Matplotlib figure object. However, this feature is deprecated and
|
||||
will be removed in a later version.
|
||||
|
||||
clear_figure : bool
|
||||
If True, the figure will be cleared after being rendered.
|
||||
If False, the figure will not be cleared after being rendered.
|
||||
If left unspecified, we pick a default based on the value of ``fig``.
|
||||
|
||||
- If ``fig`` is set, defaults to ``False``.
|
||||
|
||||
- If ``fig`` is not set, defaults to ``True``. This simulates Jupyter's
|
||||
approach to matplotlib rendering.
|
||||
|
||||
use_container_width : bool
|
||||
Whether to override the figure's native width with the width of
|
||||
the parent container. If ``use_container_width`` is ``True``
|
||||
(default), Streamlit sets the width of the figure to match the
|
||||
width of the parent container. If ``use_container_width`` is
|
||||
``False``, Streamlit sets the width of the chart to fit its
|
||||
contents according to the plotting library, up to the width of the
|
||||
parent container.
|
||||
|
||||
**kwargs : any
|
||||
Arguments to pass to Matplotlib's savefig function.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>> import matplotlib.pyplot as plt
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> arr = np.random.normal(1, 1, size=100)
|
||||
>>> fig, ax = plt.subplots()
|
||||
>>> ax.hist(arr, bins=20)
|
||||
>>>
|
||||
>>> st.pyplot(fig)
|
||||
|
||||
.. output::
|
||||
https://doc-pyplot.streamlit.app/
|
||||
height: 630px
|
||||
|
||||
Matplotlib supports several types of "backends". If you're getting an
|
||||
error using Matplotlib with Streamlit, try setting your backend to "TkAgg"::
|
||||
|
||||
echo "backend: TkAgg" >> ~/.matplotlib/matplotlibrc
|
||||
|
||||
For more information, see https://matplotlib.org/faq/usage_faq.html.
|
||||
|
||||
"""
|
||||
|
||||
if not fig:
|
||||
show_deprecation_warning("""
|
||||
Calling `st.pyplot()` without providing a figure argument has been deprecated
|
||||
and will be removed in a later version as it requires the use of Matplotlib's
|
||||
global figure object, which is not thread-safe.
|
||||
|
||||
To future-proof this code, you should pass in a figure as shown below:
|
||||
|
||||
```python
|
||||
fig, ax = plt.subplots()
|
||||
ax.scatter([1, 2, 3], [1, 2, 3])
|
||||
# other plotting actions...
|
||||
st.pyplot(fig)
|
||||
```
|
||||
|
||||
If you have a specific use case that requires this functionality, please let us
|
||||
know via [issue on Github](https://github.com/streamlit/streamlit/issues).
|
||||
""")
|
||||
|
||||
image_list_proto = ImageListProto()
|
||||
marshall(
|
||||
self.dg._get_delta_path_str(),
|
||||
image_list_proto,
|
||||
fig,
|
||||
clear_figure,
|
||||
use_container_width,
|
||||
**kwargs,
|
||||
)
|
||||
return self.dg._enqueue("imgs", image_list_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
def marshall(
|
||||
coordinates: str,
|
||||
image_list_proto: ImageListProto,
|
||||
fig: Figure | None = None,
|
||||
clear_figure: bool | None = True,
|
||||
use_container_width: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
try:
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
plt.ioff()
|
||||
except ImportError:
|
||||
raise ImportError("pyplot() command requires matplotlib")
|
||||
|
||||
# You can call .savefig() on a Figure object or directly on the pyplot
|
||||
# module, in which case you're doing it to the latest Figure.
|
||||
if not fig:
|
||||
if clear_figure is None:
|
||||
clear_figure = True
|
||||
|
||||
fig = cast("Figure", plt)
|
||||
|
||||
# Normally, dpi is set to 'figure', and the figure's dpi is set to 100.
|
||||
# So here we pick double of that to make things look good in a high
|
||||
# DPI display.
|
||||
options = {"bbox_inches": "tight", "dpi": 200, "format": "png"}
|
||||
|
||||
# If some options are passed in from kwargs then replace the values in
|
||||
# options with the ones from kwargs
|
||||
options = {a: kwargs.get(a, b) for a, b in options.items()}
|
||||
# Merge options back into kwargs.
|
||||
kwargs.update(options)
|
||||
|
||||
image = io.BytesIO()
|
||||
fig.savefig(image, **kwargs)
|
||||
image_width = (
|
||||
WidthBehavior.COLUMN if use_container_width else WidthBehavior.ORIGINAL
|
||||
)
|
||||
marshall_images(
|
||||
coordinates=coordinates,
|
||||
image=image,
|
||||
caption=None,
|
||||
width=image_width,
|
||||
proto_imgs=image_list_proto,
|
||||
clamp=False,
|
||||
channels="RGB",
|
||||
output_format="PNG",
|
||||
)
|
||||
|
||||
# Clear the figure after rendering it. This means that subsequent
|
||||
# plt calls will be starting fresh.
|
||||
if clear_figure:
|
||||
fig.clf()
|
||||
@@ -0,0 +1,47 @@
|
||||
# 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 typing import TYPE_CHECKING, cast
|
||||
|
||||
from streamlit.proto.Snow_pb2 import Snow as SnowProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
class SnowMixin:
|
||||
@gather_metrics("snow")
|
||||
def snow(self) -> DeltaGenerator:
|
||||
"""Draw celebratory snowfall.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.snow()
|
||||
|
||||
...then watch your app and get ready for a cool celebration!
|
||||
|
||||
"""
|
||||
snow_proto = SnowProto()
|
||||
snow_proto.show = True
|
||||
return self.dg._enqueue("snow", snow_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
113
myenv/lib/python3.11/site-packages/streamlit/elements/spinner.py
Normal file
113
myenv/lib/python3.11/site-packages/streamlit/elements/spinner.py
Normal file
@@ -0,0 +1,113 @@
|
||||
# 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 contextlib
|
||||
import threading
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import streamlit as st
|
||||
from streamlit.runtime.scriptrunner import add_script_run_ctx
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def spinner(
|
||||
text: str = "In progress...",
|
||||
*,
|
||||
show_time: bool = False,
|
||||
_cache: bool = False,
|
||||
) -> Iterator[None]:
|
||||
"""Display a loading spinner while executing a block of code.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
text : str
|
||||
The text to display next to the spinner. This defaults to
|
||||
``"In progress..."``.
|
||||
|
||||
The text can optionally contain GitHub-flavored Markdown. Syntax
|
||||
information can be found at: https://github.github.com/gfm.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional, supported
|
||||
Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
show_time : bool
|
||||
Whether to show the elapsed time next to the spinner text. If this is
|
||||
``False`` (default), no time is displayed. If this is ``True``,
|
||||
elapsed time is displayed with a precision of 0.1 seconds. The time
|
||||
format is not configurable.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>> import time
|
||||
>>>
|
||||
>>> with st.spinner("Wait for it...", show_time=True):
|
||||
>>> time.sleep(5)
|
||||
>>> st.success("Done!")
|
||||
>>> st.button("Rerun")
|
||||
|
||||
.. output ::
|
||||
https://doc-spinner.streamlit.app/
|
||||
height: 210px
|
||||
|
||||
"""
|
||||
from streamlit.proto.Spinner_pb2 import Spinner as SpinnerProto
|
||||
from streamlit.string_util import clean_text
|
||||
|
||||
message = st.empty()
|
||||
|
||||
# Set the message 0.5 seconds in the future to avoid annoying
|
||||
# flickering if this spinner runs too quickly.
|
||||
DELAY_SECS = 0.5
|
||||
display_message = True
|
||||
display_message_lock = threading.Lock()
|
||||
|
||||
try:
|
||||
|
||||
def set_message():
|
||||
with display_message_lock:
|
||||
if display_message:
|
||||
spinner_proto = SpinnerProto()
|
||||
spinner_proto.text = clean_text(text)
|
||||
spinner_proto.cache = _cache
|
||||
spinner_proto.show_time = show_time
|
||||
message._enqueue("spinner", spinner_proto)
|
||||
|
||||
add_script_run_ctx(threading.Timer(DELAY_SECS, set_message)).start()
|
||||
|
||||
# Yield control back to the context.
|
||||
yield
|
||||
finally:
|
||||
if display_message_lock:
|
||||
with display_message_lock:
|
||||
display_message = False
|
||||
if "chat_message" in set(message._active_dg._ancestor_block_types):
|
||||
# Temporary stale element fix:
|
||||
# For chat messages, we are resetting the spinner placeholder to an
|
||||
# empty container instead of an empty placeholder (st.empty) to have
|
||||
# it removed from the delta path. Empty containers are ignored in the
|
||||
# frontend since they are configured with allow_empty=False. This
|
||||
# prevents issues with stale elements caused by the spinner being
|
||||
# rendered only in some situations (e.g. for caching).
|
||||
message.container()
|
||||
else:
|
||||
message.empty()
|
||||
@@ -0,0 +1,76 @@
|
||||
# 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 typing import TYPE_CHECKING, cast
|
||||
|
||||
from streamlit.proto.Text_pb2 import Text as TextProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.string_util import clean_text
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.type_util import SupportsStr
|
||||
|
||||
|
||||
class TextMixin:
|
||||
@gather_metrics("text")
|
||||
def text(
|
||||
self,
|
||||
body: SupportsStr,
|
||||
*, # keyword-only arguments:
|
||||
help: str | None = None,
|
||||
) -> DeltaGenerator:
|
||||
r"""Write text without Markdown or HTML parsing.
|
||||
|
||||
For monospace text, use |st.code|_.
|
||||
|
||||
.. |st.code| replace:: ``st.code``
|
||||
.. _st.code: https://docs.streamlit.io/develop/api-reference/text/st.code
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : str
|
||||
The string to display.
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed next to the text. 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``.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.text("This is text\n[and more text](that's not a Markdown link).")
|
||||
|
||||
.. output ::
|
||||
https://doc-text.streamlit.app/
|
||||
height: 220px
|
||||
|
||||
"""
|
||||
text_proto = TextProto()
|
||||
text_proto.body = clean_text(body)
|
||||
if help:
|
||||
text_proto.help = help
|
||||
return self.dg._enqueue("text", text_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
@@ -0,0 +1,98 @@
|
||||
# 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 typing import TYPE_CHECKING, cast
|
||||
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.Toast_pb2 import Toast as ToastProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.string_util import clean_text, validate_icon_or_emoji
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.type_util import SupportsStr
|
||||
|
||||
|
||||
def validate_text(toast_text: SupportsStr) -> SupportsStr:
|
||||
if str(toast_text) == "":
|
||||
raise StreamlitAPIException(
|
||||
"Toast body cannot be blank - please provide a message."
|
||||
)
|
||||
else:
|
||||
return toast_text
|
||||
|
||||
|
||||
class ToastMixin:
|
||||
@gather_metrics("toast")
|
||||
def toast(
|
||||
self,
|
||||
body: SupportsStr,
|
||||
*, # keyword-only args:
|
||||
icon: str | None = None,
|
||||
) -> DeltaGenerator:
|
||||
"""Display a short message, known as a notification "toast".
|
||||
The toast appears in the app's bottom-right corner and disappears after four seconds.
|
||||
|
||||
.. warning::
|
||||
``st.toast`` is not compatible with Streamlit's `caching \
|
||||
<https://docs.streamlit.io/develop/concepts/architecture/caching>`_ and
|
||||
cannot be called within a cached function.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : str
|
||||
The string to display as GitHub-flavored Markdown. Syntax
|
||||
information can be found at: https://github.github.com/gfm.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
icon : str, None
|
||||
An optional emoji or icon to display next to the alert. If ``icon``
|
||||
is ``None`` (default), no icon is displayed. If ``icon`` is a
|
||||
string, the following options are valid:
|
||||
|
||||
- A single-character emoji. For example, you can set ``icon="🚨"``
|
||||
or ``icon="🔥"``. 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.
|
||||
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.toast('Your edited image was saved!', icon='😍')
|
||||
"""
|
||||
toast_proto = ToastProto()
|
||||
toast_proto.body = clean_text(validate_text(body))
|
||||
toast_proto.icon = validate_icon_or_emoji(icon)
|
||||
return self.dg._enqueue("toast", toast_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
1985
myenv/lib/python3.11/site-packages/streamlit/elements/vega_charts.py
Normal file
1985
myenv/lib/python3.11/site-packages/streamlit/elements/vega_charts.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
573
myenv/lib/python3.11/site-packages/streamlit/elements/write.py
Normal file
573
myenv/lib/python3.11/site-packages/streamlit/elements/write.py
Normal file
@@ -0,0 +1,573 @@
|
||||
# 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 dataclasses
|
||||
import inspect
|
||||
import types
|
||||
from collections import ChainMap, UserDict, UserList
|
||||
from collections.abc import (
|
||||
AsyncGenerator,
|
||||
Generator,
|
||||
ItemsView,
|
||||
Iterable,
|
||||
KeysView,
|
||||
ValuesView,
|
||||
)
|
||||
from io import StringIO
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Final,
|
||||
cast,
|
||||
)
|
||||
|
||||
from streamlit import dataframe_util, type_util
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.logger import get_logger
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.string_util import (
|
||||
is_mem_address_str,
|
||||
max_char_sequence,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
# Special methods:
|
||||
HELP_TYPES: Final[tuple[type[Any], ...]] = (
|
||||
types.BuiltinFunctionType,
|
||||
types.BuiltinMethodType,
|
||||
types.FunctionType,
|
||||
types.MethodType,
|
||||
types.ModuleType,
|
||||
)
|
||||
|
||||
_LOGGER: Final = get_logger(__name__)
|
||||
|
||||
_TEXT_CURSOR: Final = " ▏"
|
||||
|
||||
|
||||
class StreamingOutput(list[Any]):
|
||||
pass
|
||||
|
||||
|
||||
class WriteMixin:
|
||||
@gather_metrics("write_stream")
|
||||
def write_stream(
|
||||
self,
|
||||
stream: Callable[..., Any]
|
||||
| Generator[Any, Any, Any]
|
||||
| Iterable[Any]
|
||||
| AsyncGenerator[Any, Any],
|
||||
) -> list[Any] | str:
|
||||
"""Stream a generator, iterable, or stream-like sequence to the app.
|
||||
|
||||
``st.write_stream`` iterates through the given sequences and writes all
|
||||
chunks to the app. String chunks will be written using a typewriter effect.
|
||||
Other data types will be written using ``st.write``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
stream : Callable, Generator, Iterable, OpenAI Stream, or LangChain Stream
|
||||
The generator or iterable to stream.
|
||||
|
||||
If you pass an async generator, Streamlit will internally convert
|
||||
it to a sync generator.
|
||||
|
||||
.. note::
|
||||
To use additional LLM libraries, you can create a wrapper to
|
||||
manually define a generator function and include custom output
|
||||
parsing.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str or list
|
||||
The full response. If the streamed output only contains text, this
|
||||
is a string. Otherwise, this is a list of all the streamed objects.
|
||||
The return value is fully compatible as input for ``st.write``.
|
||||
|
||||
Example
|
||||
-------
|
||||
You can pass an OpenAI stream as shown in our tutorial, `Build a \
|
||||
basic LLM chat app <https://docs.streamlit.io/develop/tutorials/llms\
|
||||
/build-conversational-apps#build-a-chatgpt-like-app>`_. Alternatively,
|
||||
you can pass a generic generator function as input:
|
||||
|
||||
>>> import time
|
||||
>>> import numpy as np
|
||||
>>> import pandas as pd
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> _LOREM_IPSUM = \"\"\"
|
||||
>>> Lorem ipsum dolor sit amet, **consectetur adipiscing** elit, sed do eiusmod tempor
|
||||
>>> incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis
|
||||
>>> nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
>>> \"\"\"
|
||||
>>>
|
||||
>>>
|
||||
>>> def stream_data():
|
||||
>>> for word in _LOREM_IPSUM.split(" "):
|
||||
>>> yield word + " "
|
||||
>>> time.sleep(0.02)
|
||||
>>>
|
||||
>>> yield pd.DataFrame(
|
||||
>>> np.random.randn(5, 10),
|
||||
>>> columns=["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"],
|
||||
>>> )
|
||||
>>>
|
||||
>>> for word in _LOREM_IPSUM.split(" "):
|
||||
>>> yield word + " "
|
||||
>>> time.sleep(0.02)
|
||||
>>>
|
||||
>>>
|
||||
>>> if st.button("Stream data"):
|
||||
>>> st.write_stream(stream_data)
|
||||
|
||||
.. output::
|
||||
https://doc-write-stream-data.streamlit.app/
|
||||
height: 550px
|
||||
|
||||
"""
|
||||
|
||||
# Just apply some basic checks for common iterable types that should
|
||||
# not be passed in here.
|
||||
if isinstance(stream, str) or dataframe_util.is_dataframe_like(stream):
|
||||
raise StreamlitAPIException(
|
||||
"`st.write_stream` expects a generator or stream-like object as input "
|
||||
f"not {type(stream)}. Please use `st.write` instead for "
|
||||
"this data type."
|
||||
)
|
||||
|
||||
stream_container: DeltaGenerator | None = None
|
||||
streamed_response: str = ""
|
||||
written_content: list[Any] = StreamingOutput()
|
||||
|
||||
def flush_stream_response():
|
||||
"""Write the full response to the app."""
|
||||
nonlocal streamed_response
|
||||
nonlocal stream_container
|
||||
|
||||
if streamed_response and stream_container:
|
||||
# Replace the stream_container element the full response
|
||||
stream_container.markdown(streamed_response)
|
||||
written_content.append(streamed_response)
|
||||
stream_container = None
|
||||
streamed_response = ""
|
||||
|
||||
# Make sure we have a generator and not just a generator function.
|
||||
if inspect.isgeneratorfunction(stream) or inspect.isasyncgenfunction(stream):
|
||||
stream = stream()
|
||||
|
||||
# If the stream is an async generator, convert it to a sync generator:
|
||||
if inspect.isasyncgen(stream):
|
||||
stream = type_util.async_generator_to_sync(stream)
|
||||
|
||||
try:
|
||||
iter(stream) # type: ignore
|
||||
except TypeError as exc:
|
||||
raise StreamlitAPIException(
|
||||
f"The provided input (type: {type(stream)}) cannot be iterated. "
|
||||
"Please make sure that it is a generator, generator function or iterable."
|
||||
) from exc
|
||||
|
||||
# Iterate through the generator and write each chunk to the app
|
||||
# with a type writer effect.
|
||||
for chunk in stream: # type: ignore
|
||||
if type_util.is_openai_chunk(chunk):
|
||||
# Try to convert OpenAI chat completion chunk to a string:
|
||||
try:
|
||||
if len(chunk.choices) == 0 or chunk.choices[0].delta is None:
|
||||
# The choices list can be empty. E.g. when using the
|
||||
# AzureOpenAI client, the first chunk will always be empty.
|
||||
chunk = ""
|
||||
else:
|
||||
chunk = chunk.choices[0].delta.content or ""
|
||||
except AttributeError as err:
|
||||
raise StreamlitAPIException(
|
||||
"Failed to parse the OpenAI ChatCompletionChunk. "
|
||||
"The most likely cause is a change of the chunk object structure "
|
||||
"due to a recent OpenAI update. You might be able to fix this "
|
||||
"by downgrading the OpenAI library or upgrading Streamlit. Also, "
|
||||
"please report this issue to: https://github.com/streamlit/streamlit/issues."
|
||||
) from err
|
||||
|
||||
if type_util.is_type(chunk, "langchain_core.messages.ai.AIMessageChunk"):
|
||||
# Try to convert LangChain message chunk to a string:
|
||||
try:
|
||||
chunk = chunk.content or ""
|
||||
except AttributeError as err:
|
||||
raise StreamlitAPIException(
|
||||
"Failed to parse the LangChain AIMessageChunk. "
|
||||
"The most likely cause is a change of the chunk object structure "
|
||||
"due to a recent LangChain update. You might be able to fix this "
|
||||
"by downgrading the OpenAI library or upgrading Streamlit. Also, "
|
||||
"please report this issue to: https://github.com/streamlit/streamlit/issues."
|
||||
) from err
|
||||
|
||||
if isinstance(chunk, str):
|
||||
if not chunk:
|
||||
# Empty strings can be ignored
|
||||
continue
|
||||
|
||||
first_text = False
|
||||
if not stream_container:
|
||||
stream_container = self.dg.empty()
|
||||
first_text = True
|
||||
streamed_response += chunk
|
||||
# Only add the streaming symbol on the second text chunk
|
||||
stream_container.markdown(
|
||||
streamed_response + ("" if first_text else _TEXT_CURSOR),
|
||||
)
|
||||
elif callable(chunk):
|
||||
flush_stream_response()
|
||||
chunk()
|
||||
else:
|
||||
flush_stream_response()
|
||||
self.write(chunk)
|
||||
written_content.append(chunk)
|
||||
|
||||
flush_stream_response()
|
||||
|
||||
if not written_content:
|
||||
# If nothing was streamed, return an empty string.
|
||||
return ""
|
||||
elif len(written_content) == 1 and isinstance(written_content[0], str):
|
||||
# If the output only contains a single string, return it as a string
|
||||
return written_content[0]
|
||||
|
||||
# Otherwise return it as a list of write-compatible objects
|
||||
return written_content
|
||||
|
||||
@gather_metrics("write")
|
||||
def write(self, *args: Any, unsafe_allow_html: bool = False, **kwargs) -> None:
|
||||
"""Displays arguments in the app.
|
||||
|
||||
This is the Swiss Army knife of Streamlit commands: it does different
|
||||
things depending on what you throw at it. Unlike other Streamlit
|
||||
commands, ``st.write()`` has some unique properties:
|
||||
|
||||
- You can pass in multiple arguments, all of which will be displayed.
|
||||
- Its behavior depends on the input type(s).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*args : any
|
||||
One or many objects to display in the app.
|
||||
|
||||
.. list-table:: Each type of argument is handled as follows:
|
||||
:header-rows: 1
|
||||
|
||||
* - Type
|
||||
- Handling
|
||||
* - ``str``
|
||||
- Uses ``st.markdown()``.
|
||||
* - dataframe-like, ``dict``, or ``list``
|
||||
- Uses ``st.dataframe()``.
|
||||
* - ``Exception``
|
||||
- Uses ``st.exception()``.
|
||||
* - function, module, or class
|
||||
- Uses ``st.help()``.
|
||||
* - ``DeltaGenerator``
|
||||
- Uses ``st.help()``.
|
||||
* - Altair chart
|
||||
- Uses ``st.altair_chart()``.
|
||||
* - Bokeh figure
|
||||
- Uses ``st.bokeh_chart()``.
|
||||
* - Graphviz graph
|
||||
- Uses ``st.graphviz_chart()``.
|
||||
* - Keras model
|
||||
- Converts model and uses ``st.graphviz_chart()``.
|
||||
* - Matplotlib figure
|
||||
- Uses ``st.pyplot()``.
|
||||
* - Plotly figure
|
||||
- Uses ``st.plotly_chart()``.
|
||||
* - ``PIL.Image``
|
||||
- Uses ``st.image()``.
|
||||
* - generator or stream (like ``openai.Stream``)
|
||||
- Uses ``st.write_stream()``.
|
||||
* - SymPy expression
|
||||
- Uses ``st.latex()``.
|
||||
* - An object with ``._repr_html()``
|
||||
- Uses ``st.html()``.
|
||||
* - Database cursor
|
||||
- Displays DB API 2.0 cursor results in a table.
|
||||
* - Any
|
||||
- Displays ``str(arg)`` as inline code.
|
||||
|
||||
unsafe_allow_html : bool
|
||||
Whether to render HTML within ``*args``. This only applies to
|
||||
strings or objects falling back on ``_repr_html_()``. If this is
|
||||
``False`` (default), any HTML tags found in ``body`` will be
|
||||
escaped and therefore treated as raw text. If this is ``True``, any
|
||||
HTML expressions within ``body`` will be rendered.
|
||||
|
||||
Adding custom HTML to your app impacts safety, styling, and
|
||||
maintainability.
|
||||
|
||||
.. note::
|
||||
If you only want to insert HTML or CSS without Markdown text,
|
||||
we recommend using ``st.html`` instead.
|
||||
|
||||
**kwargs : any
|
||||
Keyword arguments. Not used.
|
||||
|
||||
.. deprecated::
|
||||
``**kwargs`` is deprecated and will be removed in a later version.
|
||||
Use other, more specific Streamlit commands to pass additional
|
||||
keyword arguments.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
|
||||
Examples
|
||||
--------
|
||||
Its basic use case is to draw Markdown-formatted text, whenever the
|
||||
input is a string:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.write("Hello, *World!* :sunglasses:")
|
||||
|
||||
.. output::
|
||||
https://doc-write1.streamlit.app/
|
||||
height: 150px
|
||||
|
||||
As mentioned earlier, ``st.write()`` also accepts other data formats, such as
|
||||
numbers, data frames, styled data frames, and assorted objects:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>>
|
||||
>>> st.write(1234)
|
||||
>>> st.write(
|
||||
... pd.DataFrame(
|
||||
... {
|
||||
... "first column": [1, 2, 3, 4],
|
||||
... "second column": [10, 20, 30, 40],
|
||||
... }
|
||||
... )
|
||||
... )
|
||||
|
||||
.. output::
|
||||
https://doc-write2.streamlit.app/
|
||||
height: 350px
|
||||
|
||||
Finally, you can pass in multiple arguments to do things like:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.write("1 + 1 = ", 2)
|
||||
>>> st.write("Below is a DataFrame:", data_frame, "Above is a dataframe.")
|
||||
|
||||
.. output::
|
||||
https://doc-write3.streamlit.app/
|
||||
height: 410px
|
||||
|
||||
Oh, one more thing: ``st.write`` accepts chart objects too! For example:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>> import numpy as np
|
||||
>>> import altair as alt
|
||||
>>>
|
||||
>>> df = pd.DataFrame(np.random.randn(200, 3), columns=["a", "b", "c"])
|
||||
>>> c = (
|
||||
... alt.Chart(df)
|
||||
... .mark_circle()
|
||||
... .encode(x="a", y="b", size="c", color="c", tooltip=["a", "b", "c"])
|
||||
... )
|
||||
>>>
|
||||
>>> st.write(c)
|
||||
|
||||
.. output::
|
||||
https://doc-vega-lite-chart.streamlit.app/
|
||||
height: 300px
|
||||
|
||||
"""
|
||||
if kwargs:
|
||||
_LOGGER.warning(
|
||||
'Invalid arguments were passed to "st.write" function. Support for '
|
||||
"passing such unknown keywords arguments will be dropped in future. "
|
||||
"Invalid arguments were: %s",
|
||||
kwargs,
|
||||
)
|
||||
|
||||
string_buffer: list[str] = []
|
||||
|
||||
# This bans some valid cases like: e = st.empty(); e.write("a", "b").
|
||||
# BUT: 1) such cases are rare, 2) this rule is easy to understand,
|
||||
# and 3) this rule should be removed once we have st.container()
|
||||
if not self.dg._is_top_level and len(args) > 1:
|
||||
raise StreamlitAPIException(
|
||||
"Cannot replace a single element with multiple elements.\n\n"
|
||||
"The `write()` method only supports multiple elements when "
|
||||
"inserting elements rather than replacing. That is, only "
|
||||
"when called as `st.write()` or `st.sidebar.write()`."
|
||||
)
|
||||
|
||||
def flush_buffer():
|
||||
if string_buffer:
|
||||
text_content = " ".join(string_buffer)
|
||||
# The usage of empty here prevents
|
||||
# some grey out effects:
|
||||
text_container = self.dg.empty()
|
||||
text_container.markdown(
|
||||
text_content,
|
||||
unsafe_allow_html=unsafe_allow_html,
|
||||
)
|
||||
string_buffer[:] = []
|
||||
|
||||
for arg in args:
|
||||
# Order matters!
|
||||
if isinstance(arg, str):
|
||||
string_buffer.append(arg)
|
||||
elif isinstance(arg, StreamingOutput):
|
||||
flush_buffer()
|
||||
for item in arg:
|
||||
if callable(item):
|
||||
flush_buffer()
|
||||
item()
|
||||
else:
|
||||
self.write(item, unsafe_allow_html=unsafe_allow_html)
|
||||
elif isinstance(arg, Exception):
|
||||
flush_buffer()
|
||||
self.dg.exception(arg)
|
||||
elif type_util.is_delta_generator(arg):
|
||||
flush_buffer()
|
||||
self.dg.help(arg)
|
||||
elif dataframe_util.is_dataframe_like(arg):
|
||||
flush_buffer()
|
||||
self.dg.dataframe(arg)
|
||||
elif type_util.is_altair_chart(arg):
|
||||
flush_buffer()
|
||||
self.dg.altair_chart(arg)
|
||||
elif type_util.is_type(arg, "matplotlib.figure.Figure"):
|
||||
flush_buffer()
|
||||
self.dg.pyplot(arg)
|
||||
elif type_util.is_plotly_chart(arg):
|
||||
flush_buffer()
|
||||
self.dg.plotly_chart(arg)
|
||||
elif type_util.is_type(arg, "bokeh.plotting.figure.Figure"):
|
||||
flush_buffer()
|
||||
self.dg.bokeh_chart(arg)
|
||||
elif type_util.is_graphviz_chart(arg):
|
||||
flush_buffer()
|
||||
self.dg.graphviz_chart(arg)
|
||||
elif type_util.is_sympy_expression(arg):
|
||||
flush_buffer()
|
||||
self.dg.latex(arg)
|
||||
elif type_util.is_pillow_image(arg):
|
||||
flush_buffer()
|
||||
self.dg.image(arg)
|
||||
elif type_util.is_keras_model(arg):
|
||||
from tensorflow.python.keras.utils import vis_utils
|
||||
|
||||
flush_buffer()
|
||||
dot = vis_utils.model_to_dot(arg)
|
||||
self.dg.graphviz_chart(dot.to_string())
|
||||
elif (
|
||||
isinstance(
|
||||
arg,
|
||||
(
|
||||
dict,
|
||||
list,
|
||||
map,
|
||||
enumerate,
|
||||
types.MappingProxyType,
|
||||
UserDict,
|
||||
ChainMap,
|
||||
UserList,
|
||||
ItemsView,
|
||||
KeysView,
|
||||
ValuesView,
|
||||
),
|
||||
)
|
||||
or type_util.is_custom_dict(arg)
|
||||
or type_util.is_namedtuple(arg)
|
||||
or type_util.is_pydantic_model(arg)
|
||||
):
|
||||
flush_buffer()
|
||||
self.dg.json(arg)
|
||||
elif type_util.is_pydeck(arg):
|
||||
flush_buffer()
|
||||
self.dg.pydeck_chart(arg)
|
||||
elif isinstance(arg, StringIO):
|
||||
flush_buffer()
|
||||
self.dg.markdown(arg.getvalue())
|
||||
elif (
|
||||
inspect.isgenerator(arg)
|
||||
or inspect.isgeneratorfunction(arg)
|
||||
or inspect.isasyncgenfunction(arg)
|
||||
or inspect.isasyncgen(arg)
|
||||
or type_util.is_type(arg, "openai.Stream")
|
||||
):
|
||||
flush_buffer()
|
||||
self.write_stream(arg)
|
||||
elif isinstance(arg, HELP_TYPES):
|
||||
flush_buffer()
|
||||
self.dg.help(arg)
|
||||
elif dataclasses.is_dataclass(arg):
|
||||
flush_buffer()
|
||||
self.dg.help(arg)
|
||||
elif inspect.isclass(arg):
|
||||
flush_buffer()
|
||||
# We cast arg to type here to appease mypy, due to bug in mypy:
|
||||
# https://github.com/python/mypy/issues/12933
|
||||
self.dg.help(cast("type", arg))
|
||||
elif unsafe_allow_html and type_util.has_callable_attr(arg, "_repr_html_"):
|
||||
self.dg.html(arg._repr_html_())
|
||||
elif type_util.has_callable_attr(
|
||||
arg, "to_pandas"
|
||||
) or type_util.has_callable_attr(arg, "__dataframe__"):
|
||||
# This object can very likely be converted to a DataFrame
|
||||
# using the to_pandas, to_arrow, or the dataframe interchange
|
||||
# protocol.
|
||||
flush_buffer()
|
||||
self.dg.dataframe(arg)
|
||||
else:
|
||||
stringified_arg = str(arg)
|
||||
|
||||
if is_mem_address_str(stringified_arg):
|
||||
flush_buffer()
|
||||
self.dg.help(arg)
|
||||
|
||||
elif "\n" in stringified_arg:
|
||||
# With a multi-line string, use a preformatted block
|
||||
# To fully escape backticks, we wrap with backticks larger than
|
||||
# the largest sequence of backticks in the string.
|
||||
backtick_count = max(3, max_char_sequence(stringified_arg, "`") + 1)
|
||||
backtick_wrapper = "`" * backtick_count
|
||||
string_buffer.append(
|
||||
f"{backtick_wrapper}\n{stringified_arg}\n{backtick_wrapper}"
|
||||
)
|
||||
else:
|
||||
# With a single-line string, use a preformatted text
|
||||
# To fully escape backticks, we wrap with backticks larger than
|
||||
# the largest sequence of backticks in the string.
|
||||
backtick_count = max_char_sequence(stringified_arg, "`") + 1
|
||||
backtick_wrapper = "`" * backtick_count
|
||||
string_buffer.append(
|
||||
f"{backtick_wrapper}{stringified_arg}{backtick_wrapper}"
|
||||
)
|
||||
|
||||
flush_buffer()
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
Reference in New Issue
Block a user