Mise à jour de Monitor.py et autres scripts

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

View File

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

View File

@@ -0,0 +1,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)

View 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)

View File

@@ -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)

View File

@@ -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

View 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)

View File

@@ -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

View File

@@ -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
)

View File

@@ -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 []

View 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)

View File

@@ -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

View 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)

View File

@@ -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

View 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

View 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

View 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

View 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)

View 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)

View 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)

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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__

View File

@@ -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}"
)

View File

@@ -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) != ""

View File

@@ -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
)

View 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 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"
)

View File

@@ -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)

View File

@@ -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,
)

View File

@@ -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

View 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.
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'."
)

View File

@@ -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"

View File

@@ -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 ""

View File

@@ -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}

View 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 = ""

View File

@@ -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 &mdash;\
... :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)

View 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,
)

View 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("-")

View File

@@ -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)

View File

@@ -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)

View 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()

View File

@@ -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)

View 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()

View File

@@ -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)

View File

@@ -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)

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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)