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,307 @@
# 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.
# isort: skip_file
"""Streamlit.
How to use Streamlit in 3 seconds:
1. Write an app
>>> import streamlit as st
>>> st.write(anything_you_want)
2. Run your app
$ streamlit run my_script.py
3. Use your app
A new tab will open on your browser. That's your Streamlit app!
4. Modify your code, save it, and watch changes live on your browser.
Take a look at the other commands in this module to find out what else
Streamlit can do:
>>> dir(streamlit)
Or try running our "Hello World":
$ streamlit hello
For more detailed info, see https://docs.streamlit.io.
"""
# IMPORTANT: Prefix with an underscore anything that the user shouldn't see.
import os as _os
# Set Matplotlib backend to avoid a crash.
# The default Matplotlib backend crashes Python on OSX when run on a thread
# that's not the main thread, so here we set a safer backend as a fix.
# This fix is OS-independent. We didn't see a good reason to make this
# Mac-only. Consistency within Streamlit seemed more important.
# IMPORTANT: This needs to run on top of all imports before any other
# import of matplotlib could happen.
_os.environ["MPLBACKEND"] = "Agg"
# Must be at the top, to avoid circular dependency.
from streamlit import logger as _logger
from streamlit import config as _config
from streamlit.deprecation_util import deprecate_func_name as _deprecate_func_name
from streamlit.version import STREAMLIT_VERSION_STRING as _STREAMLIT_VERSION_STRING
# Give the package a version.
__version__ = _STREAMLIT_VERSION_STRING
# DeltaGenerator methods:
# We initialize them here so that it is clear where they are instantiated.
# Further, it helps us to break circular imports because the DeltaGenerator
# imports the different elements but some elements also require DeltaGenerator
# functions such as the dg_stack. Now, elements that require DeltaGenerator functions
# can import the singleton module.
from streamlit.delta_generator_singletons import (
DeltaGeneratorSingleton as _DeltaGeneratorSingleton,
)
from streamlit.delta_generator import DeltaGenerator as _DeltaGenerator
from streamlit.elements.lib.mutable_status_container import (
StatusContainer as _StatusContainer,
)
from streamlit.elements.lib.dialog import Dialog as _Dialog
# instantiate the DeltaGeneratorSingleton
_dg_singleton = _DeltaGeneratorSingleton(
delta_generator_cls=_DeltaGenerator,
status_container_cls=_StatusContainer,
dialog_container_cls=_Dialog,
)
_main = _dg_singleton._main_dg
sidebar = _dg_singleton._sidebar_dg
_event = _dg_singleton._event_dg
_bottom = _dg_singleton._bottom_dg
from streamlit.elements.dialog_decorator import (
dialog_decorator as _dialog_decorator,
experimental_dialog_decorator as _experimental_dialog_decorator,
)
from streamlit.runtime.caching import (
cache_resource as _cache_resource,
cache_data as _cache_data,
cache as _cache,
)
from streamlit.runtime.connection_factory import (
connection_factory as _connection,
)
from streamlit.runtime.fragment import (
experimental_fragment as _experimental_fragment,
fragment as _fragment,
)
from streamlit.runtime.metrics_util import gather_metrics as _gather_metrics
from streamlit.runtime.secrets import secrets_singleton as _secrets_singleton
from streamlit.runtime.context import ContextProxy as _ContextProxy
from streamlit.runtime.state import (
SessionStateProxy as _SessionStateProxy,
QueryParamsProxy as _QueryParamsProxy,
)
from streamlit.user_info import (
UserInfoProxy as _UserInfoProxy,
login as _login,
logout as _logout,
)
from streamlit.commands.experimental_query_params import (
get_query_params as _get_query_params,
set_query_params as _set_query_params,
)
import streamlit.column_config as _column_config
# Modules that the user should have access to. These are imported with the "as" syntax
# and the same name; note that renaming the import with "as" does not make it an
# explicit export. In this case, you should import it with an underscore to make clear
# that it is internal and then assign it to a variable with the new intended name.
# You can check the export behavior by running 'mypy --strict example_app.py', which
# disables implicit_reexport, where you use the respective command in the example_app.py
# Streamlit app.
from streamlit.commands.echo import echo as echo
from streamlit.commands.logo import logo as logo
from streamlit.commands.navigation import navigation as navigation
from streamlit.navigation.page import Page as Page
from streamlit.elements.spinner import spinner as spinner
from streamlit.commands.page_config import set_page_config as set_page_config
from streamlit.commands.execution_control import (
stop as stop,
rerun as rerun,
switch_page as switch_page,
)
def _update_logger() -> None:
_logger.set_log_level(_config.get_option("logger.level").upper())
_logger.update_formatter()
_logger.init_tornado_logs()
# Make this file only depend on config option in an asynchronous manner. This
# avoids a race condition when another file (such as a test file) tries to pass
# in an alternative config.
_config.on_config_parsed(_update_logger, True)
secrets = _secrets_singleton
altair_chart = _main.altair_chart
area_chart = _main.area_chart
audio = _main.audio
audio_input = _main.audio_input
badge = _main.badge
balloons = _main.balloons
bar_chart = _main.bar_chart
bokeh_chart = _main.bokeh_chart
button = _main.button
caption = _main.caption
camera_input = _main.camera_input
chat_message = _main.chat_message
chat_input = _main.chat_input
checkbox = _main.checkbox
code = _main.code
columns = _main.columns
tabs = _main.tabs
container = _main.container
dataframe = _main.dataframe
data_editor = _main.data_editor
date_input = _main.date_input
divider = _main.divider
download_button = _main.download_button
expander = _main.expander
feedback = _main.feedback
pydeck_chart = _main.pydeck_chart
empty = _main.empty
error = _main.error
exception = _main.exception
file_uploader = _main.file_uploader
form = _main.form
form_submit_button = _main.form_submit_button
graphviz_chart = _main.graphviz_chart
header = _main.header
help = _main.help
html = _main.html
image = _main.image
info = _main.info
json = _main.json
latex = _main.latex
line_chart = _main.line_chart
link_button = _main.link_button
map = _main.map
markdown = _main.markdown
metric = _main.metric
multiselect = _main.multiselect
number_input = _main.number_input
page_link = _main.page_link
pills = _main.pills
plotly_chart = _main.plotly_chart
popover = _main.popover
progress = _main.progress
pyplot = _main.pyplot
radio = _main.radio
scatter_chart = _main.scatter_chart
selectbox = _main.selectbox
select_slider = _main.select_slider
segmented_control = _main.segmented_control
slider = _main.slider
snow = _main.snow
subheader = _main.subheader
success = _main.success
table = _main.table
text = _main.text
text_area = _main.text_area
text_input = _main.text_input
toggle = _main.toggle
time_input = _main.time_input
title = _main.title
vega_lite_chart = _main.vega_lite_chart
video = _main.video
warning = _main.warning
write = _main.write
write_stream = _main.write_stream
color_picker = _main.color_picker
status = _main.status
# Events - Note: these methods cannot be called directly on sidebar
# (ex: st.sidebar.toast)
toast = _event.toast
# Config
# We add the metrics tracking here, since importing
# gather_metrics in config causes a circular dependency
get_option = _gather_metrics("get_option", _config.get_option)
set_option = _gather_metrics("set_option", _config.set_user_option)
# Session State
session_state = _SessionStateProxy()
query_params = _QueryParamsProxy()
context = _ContextProxy()
# Caching
cache_data = _cache_data
cache_resource = _cache_resource
# `st.cache` is deprecated and should be removed soon
cache = _cache
# Namespaces
column_config = _column_config
# Connection
connection = _connection
# Fragment and dialog
dialog = _dialog_decorator
fragment = _fragment
# Auth
login = _login
logout = _logout
# Experimental APIs
experimental_audio_input = _main.experimental_audio_input
experimental_dialog = _experimental_dialog_decorator
experimental_fragment = _experimental_fragment
experimental_user = _UserInfoProxy()
_EXPERIMENTAL_QUERY_PARAMS_DEPRECATE_MSG = "Refer to our [docs page](https://docs.streamlit.io/develop/api-reference/caching-and-state/st.query_params) for more information."
experimental_get_query_params = _deprecate_func_name(
_get_query_params,
"experimental_get_query_params",
"2024-04-11",
_EXPERIMENTAL_QUERY_PARAMS_DEPRECATE_MSG,
name_override="query_params",
)
experimental_set_query_params = _deprecate_func_name(
_set_query_params,
"experimental_set_query_params",
"2024-04-11",
_EXPERIMENTAL_QUERY_PARAMS_DEPRECATE_MSG,
name_override="query_params",
)
# make it possible to call streamlit.components.v1.html etc. by importing it here
# import in the very end to avoid partially-initialized module import errors, because
# streamlit.components.v1 also uses some streamlit imports
import streamlit.components.v1 # noqa: F401

View File

@@ -0,0 +1,20 @@
# 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 streamlit.web.cli import main
if __name__ == "__main__":
# Set prog_name so that the Streamlit server sees the same command line
# string whether streamlit is called directly or via `python -m streamlit`.
main(prog_name="streamlit")

View File

@@ -0,0 +1,219 @@
# 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 datetime import datetime, timedelta, timezone
from typing import TYPE_CHECKING, Any, TypedDict, cast
from streamlit import config
from streamlit.errors import StreamlitAuthError
from streamlit.runtime.secrets import AttrDict, secrets_singleton
if TYPE_CHECKING:
class ProviderTokenPayload(TypedDict):
provider: str
exp: int
class AuthCache:
"""Simple cache implementation for storing info required for Authlib."""
def __init__(self):
self.cache = {}
def get(self, key):
return self.cache.get(key)
# for set method, we are follow the same signature used in Authlib
# the expires_in is not used in our case
def set(self, key, value, expires_in):
self.cache[key] = value
def get_dict(self):
return self.cache
def delete(self, key):
self.cache.pop(key, None)
def is_authlib_installed() -> bool:
"""Check if Authlib is installed."""
try:
import authlib # type: ignore[import-untyped]
authlib_version = authlib.__version__
authlib_version_tuple = tuple(map(int, authlib_version.split(".")))
if authlib_version_tuple < (1, 3, 2):
return False
except (ImportError, ModuleNotFoundError):
return False
return True
def get_signing_secret() -> str:
"""Get the cookie signing secret from the configuration or secrets.toml."""
signing_secret: str = config.get_option("server.cookieSecret")
if secrets_singleton.load_if_toml_exists():
auth_section = secrets_singleton.get("auth")
if auth_section:
signing_secret = auth_section.get("cookie_secret", signing_secret)
return signing_secret
def get_secrets_auth_section() -> AttrDict:
auth_section = AttrDict({})
"""Get the 'auth' section of the secrets.toml."""
if secrets_singleton.load_if_toml_exists():
auth_section = cast("AttrDict", secrets_singleton.get("auth"))
return auth_section
def encode_provider_token(provider: str) -> str:
"""Returns a signed JWT token with the provider and expiration time."""
try:
from authlib.jose import jwt # type: ignore[import-untyped]
except ImportError:
raise StreamlitAuthError(
"""To use authentication features, you need to install Authlib>=1.3.2, e.g. via `pip install Authlib`."""
) from None
header = {"alg": "HS256"}
payload = {
"provider": provider,
"exp": datetime.now(timezone.utc) + timedelta(minutes=2),
}
provider_token: bytes = jwt.encode(header, payload, get_signing_secret())
# JWT token is a byte string, so we need to decode it to a URL compatible string
return provider_token.decode("latin-1")
def decode_provider_token(provider_token: str) -> ProviderTokenPayload:
"""Decode the JWT token and validate the claims."""
try:
from authlib.jose import JoseError, JWTClaims, jwt
except ImportError:
raise StreamlitAuthError(
"""To use authentication features, you need to install Authlib>=1.3.2, e.g. via `pip install Authlib`."""
) from None
# Our JWT token is short-lived (2 minutes), so we check here that it contains
# the 'exp' (and it is not expired), and 'provider' field exists.
claim_options = {"exp": {"essential": True}, "provider": {"essential": True}}
try:
payload: JWTClaims = jwt.decode(
provider_token, get_signing_secret(), claims_options=claim_options
)
payload.validate()
except JoseError as e:
raise StreamlitAuthError(f"Error decoding provider token: {e}") from None
return cast("ProviderTokenPayload", payload)
def generate_default_provider_section(auth_section) -> dict[str, Any]:
"""Generate a default provider section for the 'auth' section of secrets.toml."""
default_provider_section = {}
if auth_section.get("client_id"):
default_provider_section["client_id"] = auth_section.get("client_id")
if auth_section.get("client_secret"):
default_provider_section["client_secret"] = auth_section.get("client_secret")
if auth_section.get("server_metadata_url"):
default_provider_section["server_metadata_url"] = auth_section.get(
"server_metadata_url"
)
if auth_section.get("client_kwargs"):
default_provider_section["client_kwargs"] = auth_section.get(
"client_kwargs"
).to_dict()
return default_provider_section
def validate_auth_credentials(provider: str) -> None:
"""Validate the general auth credentials and auth credentials for the given
provider.
"""
if not secrets_singleton.load_if_toml_exists():
raise StreamlitAuthError(
"""To use authentication features you need to configure credentials for at
least one authentication provider in `.streamlit/secrets.toml`."""
)
auth_section = secrets_singleton.get("auth")
if auth_section is None:
raise StreamlitAuthError(
"""To use authentication features you need to configure credentials for at
least one authentication provider in `.streamlit/secrets.toml`."""
)
if "redirect_uri" not in auth_section:
raise StreamlitAuthError(
"""Authentication credentials in `.streamlit/secrets.toml` are missing the
"redirect_uri" key. Please check your configuration."""
)
if "cookie_secret" not in auth_section:
raise StreamlitAuthError(
"""Authentication credentials in `.streamlit/secrets.toml` are missing the
"cookie_secret" key. Please check your configuration."""
)
provider_section = auth_section.get(provider)
# TODO(kajarenc): Revisit this check later when investigating the ability
# TODO(kajarenc): to add "_" to the provider name.
if "_" in provider:
raise StreamlitAuthError(
f'Auth provider name "{provider}" contains an underscore. '
f"Please use a provider name without underscores."
)
if provider_section is None and provider == "default":
provider_section = generate_default_provider_section(auth_section)
if provider_section is None:
if provider == "default":
raise StreamlitAuthError(
"""Authentication credentials in `.streamlit/secrets.toml` are missing for
the default authentication provider. Please check your configuration."""
)
raise StreamlitAuthError(
f"Authentication credentials in `.streamlit/secrets.toml` are missing for "
f'the authentication provider "{provider}". Please check your '
f"configuration."
)
if not isinstance(provider_section, Mapping):
raise StreamlitAuthError(
f"Authentication credentials in `.streamlit/secrets.toml` for the "
f'authentication provider "{provider}" must be valid TOML. Please check '
f"your configuration."
)
required_keys = ["client_id", "client_secret", "server_metadata_url"]
missing_keys = [key for key in required_keys if key not in provider_section]
if missing_keys:
if provider == "default":
raise StreamlitAuthError(
"Authentication credentials in `.streamlit/secrets.toml` for the "
f"default authentication provider are missing the following keys: "
f"{missing_keys}. Please check your configuration."
)
raise StreamlitAuthError(
"Authentication credentials in `.streamlit/secrets.toml` for the "
f'authentication provider "{provider}" are missing the following keys: '
f"{missing_keys}. Please check your configuration."
)

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.
"""Utilities related to the CLI."""
from __future__ import annotations
import os
import subprocess
from streamlit import env_util, errors
def print_to_cli(message: str, **kwargs) -> None:
"""Print a message to the terminal using click if available, else print
using the built-in print function.
You can provide any keyword arguments that click.secho supports.
"""
try:
import click
click.secho(message, **kwargs)
except ImportError:
print(message, flush=True) # noqa: T201
def style_for_cli(message: str, **kwargs) -> str:
"""Style a message using click if available, else return the message
unchanged.
You can provide any keyword arguments that click.style supports.
"""
try:
import click
return click.style(message, **kwargs)
except ImportError:
return message
def _open_browser_with_webbrowser(url: str) -> None:
import webbrowser
webbrowser.open(url)
def _open_browser_with_command(command: str, url: str) -> None:
cmd_line = [command, url]
with open(os.devnull, "w") as devnull:
subprocess.Popen(cmd_line, stdout=devnull, stderr=subprocess.STDOUT)
def open_browser(url: str) -> None:
"""Open a web browser pointing to a given URL.
We use this function instead of Python's `webbrowser` module because this
way we can capture stdout/stderr to avoid polluting the terminal with the
browser's messages. For example, Chrome always prints things like "Created
new window in existing browser session", and those get on the user's way.
url : str
The URL. Must include the protocol.
"""
# Treat Windows separately because:
# 1. /dev/null doesn't exist.
# 2. subprocess.Popen(['start', url]) doesn't actually pop up the
# browser even though 'start url' works from the command prompt.
# Fun!
# Also, use webbrowser if we are on Linux and xdg-open is not installed.
#
# We don't use the webbrowser module on Linux and Mac because some browsers
# (ahem... Chrome) always print "Opening in existing browser session" to
# the terminal, which is spammy and annoying. So instead we start the
# browser ourselves and send all its output to /dev/null.
if env_util.IS_WINDOWS:
_open_browser_with_webbrowser(url)
return
if env_util.IS_LINUX_OR_BSD:
if env_util.is_executable_in_path("xdg-open"):
_open_browser_with_command("xdg-open", url)
return
_open_browser_with_webbrowser(url)
return
if env_util.IS_DARWIN:
_open_browser_with_command("open", url)
return
import platform
raise errors.Error(f'Cannot open browser in platform "{platform.system()}"')

View File

@@ -0,0 +1,56 @@
# 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.
"""Column types that can be configured via the ``column_config`` parameter of ``st.dataframe`` and ``st.data_editor``."""
from __future__ import annotations
__all__ = [
"AreaChartColumn",
"Column",
"TextColumn",
"NumberColumn",
"BarChartColumn",
"CheckboxColumn",
"DatetimeColumn",
"ImageColumn",
"SelectboxColumn",
"ProgressColumn",
"LinkColumn",
"LineChartColumn",
"ListColumn",
"DateColumn",
"TimeColumn",
"JsonColumn",
]
from streamlit.elements.lib.column_types import (
AreaChartColumn,
BarChartColumn,
CheckboxColumn,
Column,
DateColumn,
DatetimeColumn,
ImageColumn,
JsonColumn,
LineChartColumn,
LinkColumn,
ListColumn,
NumberColumn,
ProgressColumn,
SelectboxColumn,
TextColumn,
TimeColumn,
)

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,126 @@
# 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 ast
import contextlib
import re
import textwrap
import traceback
from typing import TYPE_CHECKING, Any
from streamlit.runtime.metrics_util import gather_metrics
if TYPE_CHECKING:
from collections.abc import Iterable
_SPACES_RE = re.compile("\\s*")
_EMPTY_LINE_RE = re.compile("\\s*\n")
@gather_metrics("echo")
@contextlib.contextmanager
def echo(code_location="above"):
"""Use in a `with` block to draw some code on the app, then execute it.
Parameters
----------
code_location : "above" or "below"
Whether to show the echoed code before or after the results of the
executed code block.
Example
-------
>>> import streamlit as st
>>>
>>> with st.echo():
>>> st.write('This code will be printed')
"""
from streamlit import code, empty, source_util, warning
if code_location == "below":
show_code = code
show_warning = warning
else:
placeholder = empty()
show_code = placeholder.code
show_warning = placeholder.warning
try:
# Get stack frame *before* running the echoed code. The frame's
# line number will point to the `st.echo` statement we're running.
frame = traceback.extract_stack()[-3]
filename, start_line = frame.filename, frame.lineno or 0
# Read the file containing the source code of the echoed statement.
with source_util.open_python_file(filename) as source_file:
source_lines = source_file.readlines()
# Use ast to parse the Python file and find the code block to display
root_node = ast.parse("".join(source_lines))
line_to_node_map: dict[int, Any] = {}
def collect_body_statements(node: ast.AST) -> None:
if not hasattr(node, "body"):
return
for child in ast.iter_child_nodes(node):
# If child doesn't have "lineno", it is not something we could display
if hasattr(child, "lineno"):
line_to_node_map[child.lineno] = child
collect_body_statements(child)
collect_body_statements(root_node)
# In AST module the lineno (line numbers) are 1-indexed,
# so we decrease it by 1 to lookup in source lines list
echo_block_start_line = line_to_node_map[start_line].body[0].lineno - 1
echo_block_end_line = line_to_node_map[start_line].end_lineno
lines_to_display = source_lines[echo_block_start_line:echo_block_end_line]
code_string = textwrap.dedent("".join(lines_to_display))
# Run the echoed code...
yield
# And draw the code string to the app!
show_code(code_string, "python")
except FileNotFoundError as err:
show_warning("Unable to display code. %s" % err)
def _get_initial_indent(lines: Iterable[str]) -> int:
"""Return the indent of the first non-empty line in the list.
If all lines are empty, return 0.
"""
for line in lines:
indent = _get_indent(line)
if indent is not None:
return indent
return 0
def _get_indent(line: str) -> int | None:
"""Get the number of whitespaces at the beginning of the given line.
If the line is empty, or if it contains just whitespace and a newline,
return None.
"""
if _EMPTY_LINE_RE.match(line) is not None:
return None
match = _SPACES_RE.match(line)
return match.end() if match is not None else 0

View File

@@ -0,0 +1,238 @@
# 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 itertools import dropwhile
from pathlib import Path
from typing import Literal, NoReturn
import streamlit as st
from streamlit.errors import NoSessionContext, StreamlitAPIException
from streamlit.file_util import get_main_script_directory, normalize_path_join
from streamlit.navigation.page import StreamlitPage
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.scriptrunner import (
RerunData,
ScriptRunContext,
get_script_run_ctx,
)
@gather_metrics("stop")
def stop() -> NoReturn: # type: ignore[misc]
"""Stops execution immediately.
Streamlit will not run any statements after `st.stop()`.
We recommend rendering a message to explain why the script has stopped.
Example
-------
>>> import streamlit as st
>>>
>>> name = st.text_input("Name")
>>> if not name:
>>> st.warning('Please input a name.')
>>> st.stop()
>>> st.success("Thank you for inputting a name.")
"""
ctx = get_script_run_ctx()
if ctx and ctx.script_requests:
ctx.script_requests.request_stop()
# Force a yield point so the runner can stop
st.empty()
def _new_fragment_id_queue(
ctx: ScriptRunContext,
scope: Literal["app", "fragment"],
) -> list[str]:
if scope == "app":
return []
else: # scope == "fragment"
curr_queue = ctx.fragment_ids_this_run
# If st.rerun(scope="fragment") is called during a full script run, we raise an
# exception. This occurs, of course, if st.rerun(scope="fragment") is called
# outside of a fragment, but it somewhat surprisingly occurs if it gets called
# from within a fragment during a run of the full script. While this behvior may
# be surprising, it seems somewhat reasonable given that the correct behavior of
# calling st.rerun(scope="fragment") in this situation is unclear to me:
# * Rerunning just the fragment immediately may cause weirdness down the line
# as any part of the script that occurs after the fragment will not be
# executed.
# * Waiting until the full script run completes before rerunning the fragment
# seems odd (even if we normally do this before running a fragment not
# triggered by st.rerun()) because it defers the execution of st.rerun().
# * Rerunning the full app feels incorrect as we're seemingly ignoring the
# `scope` argument.
# With these issues and given that it seems pretty unnatural to have a
# fragment-scoped rerun happen during a full script run to begin with, it seems
# reasonable to just disallow this completely for now.
if not curr_queue:
raise StreamlitAPIException(
'scope="fragment" can only be specified from `@st.fragment`-decorated '
"functions during fragment reruns."
)
assert (
new_queue := list(
dropwhile(lambda x: x != ctx.current_fragment_id, curr_queue)
)
), (
"Could not find current_fragment_id in fragment_id_queue. This should never happen."
)
return new_queue
@gather_metrics("rerun")
def rerun( # type: ignore[misc]
*, # The scope argument can only be passed via keyword.
scope: Literal["app", "fragment"] = "app",
) -> NoReturn:
"""Rerun the script immediately.
When ``st.rerun()`` is called, Streamlit halts the current script run and
executes no further statements. Streamlit immediately queues the script to
rerun.
When using ``st.rerun`` in a fragment, you can scope the rerun to the
fragment. However, if a fragment is running as part of a full-app rerun,
a fragment-scoped rerun is not allowed.
Parameters
----------
scope : "app" or "fragment"
Specifies what part of the app should rerun. If ``scope`` is ``"app"``
(default), the full app reruns. If ``scope`` is ``"fragment"``,
Streamlit only reruns the fragment from which this command is called.
Setting ``scope="fragment"`` is only valid inside a fragment during a
fragment rerun. If ``st.rerun(scope="fragment")`` is called during a
full-app rerun or outside of a fragment, Streamlit will raise a
``StreamlitAPIException``.
"""
if scope not in ["app", "fragment"]:
raise StreamlitAPIException(
f"'{scope}'is not a valid rerun scope. Valid scopes are 'app' and 'fragment'."
)
ctx = get_script_run_ctx()
if ctx and ctx.script_requests:
query_string = ctx.query_string
page_script_hash = ctx.page_script_hash
ctx.script_requests.request_rerun(
RerunData(
query_string=query_string,
page_script_hash=page_script_hash,
fragment_id_queue=_new_fragment_id_queue(ctx, scope),
is_fragment_scoped_rerun=scope == "fragment",
)
)
# Force a yield point so the runner can do the rerun
st.empty()
@gather_metrics("switch_page")
def switch_page(page: str | Path | StreamlitPage) -> NoReturn: # type: ignore[misc]
"""Programmatically switch the current page in a multipage app.
When ``st.switch_page`` is called, the current page execution stops and
the specified page runs as if the user clicked on it in the sidebar
navigation. The specified page must be recognized by Streamlit's multipage
architecture (your main Python file or a Python file in a ``pages/``
folder). Arbitrary Python scripts cannot be passed to ``st.switch_page``.
Parameters
----------
page: str, Path, or st.Page
The file path (relative to the main script) or an st.Page indicating
the page to switch to.
Example
-------
Consider the following example given this file structure:
>>> your-repository/
>>> ├── pages/
>>> │ ├── page_1.py
>>> │ └── page_2.py
>>> └── your_app.py
>>> import streamlit as st
>>>
>>> if st.button("Home"):
>>> st.switch_page("your_app.py")
>>> if st.button("Page 1"):
>>> st.switch_page("pages/page_1.py")
>>> if st.button("Page 2"):
>>> st.switch_page("pages/page_2.py")
.. output ::
https://doc-switch-page.streamlit.app/
height: 350px
"""
ctx = get_script_run_ctx()
if not ctx or not ctx.script_requests:
# This should never be the case
raise NoSessionContext()
page_script_hash = ""
if isinstance(page, StreamlitPage):
page_script_hash = page._script_hash
else:
# Convert Path to string if necessary
if isinstance(page, Path):
page = str(page)
main_script_directory = get_main_script_directory(ctx.main_script_path)
requested_page = os.path.realpath(
normalize_path_join(main_script_directory, page)
)
all_app_pages = ctx.pages_manager.get_pages().values()
matched_pages = [p for p in all_app_pages if p["script_path"] == requested_page]
if len(matched_pages) == 0:
raise StreamlitAPIException(
f"Could not find page: `{page}`. Must be the file path relative to the main script, from the directory: `{os.path.basename(main_script_directory)}`. Only the main app file and files in the `pages/` directory are supported."
)
page_script_hash = matched_pages[0]["page_script_hash"]
# We want to reset query params (with exception of embed) when switching pages
with ctx.session_state.query_params() as qp:
qp.clear()
ctx.script_requests.request_rerun(
RerunData(
query_string=ctx.query_string,
page_script_hash=page_script_hash,
)
)
# Force a yield point so the runner can do the rerun
st.empty()

View File

@@ -0,0 +1,169 @@
# 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 urllib.parse as parse
from typing import Any
from streamlit.errors import StreamlitAPIException
from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
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.query_params import (
EMBED_OPTIONS_QUERY_PARAM,
EMBED_QUERY_PARAM,
EMBED_QUERY_PARAMS_KEYS,
)
@gather_metrics("experimental_get_query_params")
def get_query_params() -> dict[str, list[str]]:
"""Return the query parameters that is currently showing in the browser's URL bar.
Returns
-------
dict
The current query parameters as a dict. "Query parameters" are the part of the URL that comes
after the first "?".
Example
-------
Let's say the user's web browser is at
`http://localhost:8501/?show_map=True&selected=asia&selected=america`.
Then, you can get the query parameters using the following:
>>> import streamlit as st
>>>
>>> st.experimental_get_query_params()
{"show_map": ["True"], "selected": ["asia", "america"]}
Note that the values in the returned dict are *always* lists. This is
because we internally use Python's urllib.parse.parse_qs(), which behaves
this way. And this behavior makes sense when you consider that every item
in a query string is potentially a 1-element array.
"""
ctx = get_script_run_ctx()
if ctx is None:
return {}
ctx.mark_experimental_query_params_used()
# Return new query params dict, but without embed, embed_options query params
return _exclude_keys_in_dict(
parse.parse_qs(ctx.query_string, keep_blank_values=True),
keys_to_exclude=EMBED_QUERY_PARAMS_KEYS,
)
@gather_metrics("experimental_set_query_params")
def set_query_params(**query_params: Any) -> None:
"""Set the query parameters that are shown in the browser's URL bar.
.. warning::
Query param `embed` cannot be set using this method.
Parameters
----------
**query_params : dict
The query parameters to set, as key-value pairs.
Example
-------
To point the user's web browser to something like
"http://localhost:8501/?show_map=True&selected=asia&selected=america",
you would do the following:
>>> import streamlit as st
>>>
>>> st.experimental_set_query_params(
... show_map=True,
... selected=["asia", "america"],
... )
"""
ctx = get_script_run_ctx()
if ctx is None:
return
ctx.mark_experimental_query_params_used()
msg = ForwardMsg()
msg.page_info_changed.query_string = _ensure_no_embed_params(
query_params, ctx.query_string
)
ctx.query_string = msg.page_info_changed.query_string
ctx.enqueue(msg)
def _exclude_keys_in_dict(
d: dict[str, Any], keys_to_exclude: list[str]
) -> dict[str, Any]:
"""Returns new object but without keys defined in keys_to_exclude."""
return {
key: value for key, value in d.items() if key.lower() not in keys_to_exclude
}
def _extract_key_query_params(
query_params: dict[str, list[str]], param_key: str
) -> set[str]:
"""Extracts key (case-insensitive) query params from Dict, and returns them as Set of str."""
return {
item.lower()
for sublist in [
[value.lower() for value in query_params[key]]
for key in query_params.keys()
if key.lower() == param_key and query_params.get(key)
]
for item in sublist
}
def _ensure_no_embed_params(
query_params: dict[str, list[str] | str], query_string: str
) -> str:
"""Ensures there are no embed params set (raises StreamlitAPIException) if there is a try,
also makes sure old param values in query_string are preserved. Returns query_string : str.
"""
# Get query params dict without embed, embed_options params
query_params_without_embed = _exclude_keys_in_dict(
query_params, keys_to_exclude=EMBED_QUERY_PARAMS_KEYS
)
if query_params != query_params_without_embed:
raise StreamlitAPIException(
"Query param embed and embed_options (case-insensitive) cannot be set using set_query_params method."
)
all_current_params = parse.parse_qs(query_string, keep_blank_values=True)
current_embed_params = parse.urlencode(
{
EMBED_QUERY_PARAM: list(
_extract_key_query_params(
all_current_params, param_key=EMBED_QUERY_PARAM
)
),
EMBED_OPTIONS_QUERY_PARAM: list(
_extract_key_query_params(
all_current_params, param_key=EMBED_OPTIONS_QUERY_PARAM
)
),
},
doseq=True,
)
query_string = parse.urlencode(query_params, doseq=True)
if query_string:
separator = "&" if current_embed_params else ""
return separator.join([query_string, current_embed_params])
return current_embed_params

View File

@@ -0,0 +1,189 @@
# 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.
"""Handle App logos."""
from __future__ import annotations
from typing import Literal
from streamlit import url_util
from streamlit.elements.lib.image_utils import AtomicImage, WidthBehavior, image_to_url
from streamlit.errors import StreamlitAPIException
from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
def _invalid_logo_text(field_name: str):
return f"The {field_name} passed to st.logo is invalid - See [documentation](https://docs.streamlit.io/develop/api-reference/media/st.logo) for more information on valid types"
@gather_metrics("logo")
def logo(
image: AtomicImage,
*, # keyword-only args:
size: Literal["small", "medium", "large"] = "medium",
link: str | None = None,
icon_image: AtomicImage | None = None,
) -> None:
r"""
Renders a logo in the upper-left corner of your app and its sidebar.
If ``st.logo`` is called multiple times within a page, Streamlit will
render the image passed in the last call. For the most consistent results,
call ``st.logo`` early in your page script and choose an image that works
well in both light and dark mode. Avoid empty margins around your image.
If your logo does not work well for both light and dark mode, consider
setting the theme and hiding the settings menu from users with the
`configuration option <https://docs.streamlit.io/develop/api-reference/configuration/config.toml>`_
``client.toolbarMode="minimal"``.
Parameters
----------
image: Anything supported by st.image (except list)
The image to display in the upper-left corner of your app and its
sidebar. This can be any of the types supported by |st.image|_ except
a list. If ``icon_image`` is also provided, then Streamlit will only
display ``image`` in the sidebar.
Streamlit scales the image to a max height set by ``size`` and a max
width to fit within the sidebar.
.. |st.image| replace:: ``st.image``
.. _st.image: https://docs.streamlit.io/develop/api-reference/media/st.image
size: "small", "medium", or "large"
The size of the image displayed in the upper-left corner of the app and its
sidebar. The possible values are as follows:
- ``"small"``: 20px max height
- ``"medium"`` (default): 24px max height
- ``"large"``: 32px max height
link : str or None
The external URL to open when a user clicks on the logo. The URL must
start with "\http://" or "\https://". If ``link`` is ``None`` (default),
the logo will not include a hyperlink.
icon_image: Anything supported by st.image (except list) or None
An optional, typically smaller image to replace ``image`` in the
upper-left corner when the sidebar is closed. This can be any of the
types supported by ``st.image`` except a list. If ``icon_image`` is
``None`` (default), Streamlit will always display ``image`` in the
upper-left corner, regardless of whether the sidebar is open or closed.
Otherwise, Streamlit will render ``icon_image`` in the upper-left
corner of the app when the sidebar is closed.
Streamlit scales the image to a max height set by ``size`` and a max
width to fit within the sidebar. If the sidebar is closed, the max
width is retained from when it was last open.
For best results, pass a wide or horizontal image to ``image`` and a
square image to ``icon_image``. Or, pass a square image to ``image``
and leave ``icon_image=None``.
Examples
--------
A common design practice is to use a wider logo in the sidebar, and a
smaller, icon-styled logo in your app's main body.
>>> import streamlit as st
>>>
>>> st.logo(
... LOGO_URL_LARGE,
... link="https://streamlit.io/gallery",
... icon_image=LOGO_URL_SMALL,
... )
Try switching logos around in the following example:
>>> import streamlit as st
>>>
>>> HORIZONTAL_RED = "images/horizontal_red.png"
>>> ICON_RED = "images/icon_red.png"
>>> HORIZONTAL_BLUE = "images/horizontal_blue.png"
>>> ICON_BLUE = "images/icon_blue.png"
>>>
>>> options = [HORIZONTAL_RED, ICON_RED, HORIZONTAL_BLUE, ICON_BLUE]
>>> sidebar_logo = st.selectbox("Sidebar logo", options, 0)
>>> main_body_logo = st.selectbox("Main body logo", options, 1)
>>>
>>> st.logo(sidebar_logo, icon_image=main_body_logo)
>>> st.sidebar.markdown("Hi!")
.. output::
https://doc-logo.streamlit.app/
height: 300px
"""
ctx = get_script_run_ctx()
if ctx is None:
return
fwd_msg = ForwardMsg()
try:
image_url = image_to_url(
image,
width=WidthBehavior.AUTO,
clamp=False,
channels="RGB",
output_format="auto",
image_id="logo",
)
fwd_msg.logo.image = image_url
except Exception as ex:
raise StreamlitAPIException(_invalid_logo_text("image")) from ex
if link:
# Handle external links:
if url_util.is_url(link, ("http", "https")):
fwd_msg.logo.link = link
else:
raise StreamlitAPIException(
f"Invalid link: {link} - the link param supports external links only and must start with either http:// or https://."
)
if icon_image:
try:
icon_image_url = image_to_url(
icon_image,
width=WidthBehavior.AUTO,
clamp=False,
channels="RGB",
output_format="auto",
image_id="icon-image",
)
fwd_msg.logo.icon_image = icon_image_url
except Exception as ex:
raise StreamlitAPIException(_invalid_logo_text("icon_image")) from ex
def validate_size(size):
if isinstance(size, str):
image_size = size.lower()
valid_sizes = ["small", "medium", "large"]
if image_size in valid_sizes:
return image_size
raise StreamlitAPIException(
f'The size argument to st.logo must be "small", "medium", or "large". \n'
f"The argument passed was {size}."
)
fwd_msg.logo.size = validate_size(size)
ctx.enqueue(fwd_msg)

View File

@@ -0,0 +1,385 @@
# 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, Sequence
from pathlib import Path
from typing import TYPE_CHECKING, Callable, Literal, Union
from typing_extensions import TypeAlias
from streamlit import config
from streamlit.errors import StreamlitAPIException
from streamlit.navigation.page import StreamlitPage
from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
from streamlit.proto.Navigation_pb2 import Navigation as NavigationProto
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.pages_manager import PagesManager
from streamlit.runtime.scriptrunner_utils.script_run_context import (
ScriptRunContext,
get_script_run_ctx,
)
if TYPE_CHECKING:
from streamlit.source_util import PageHash, PageInfo
SectionHeader: TypeAlias = str
PageType: TypeAlias = Union[str, Path, Callable[[], None], StreamlitPage]
def convert_to_streamlit_page(
page_input: PageType,
) -> StreamlitPage:
"""Convert various input types to StreamlitPage objects."""
if isinstance(page_input, StreamlitPage):
return page_input
if isinstance(page_input, str):
return StreamlitPage(page_input)
if isinstance(page_input, Path):
return StreamlitPage(page_input)
if callable(page_input):
# Convert function to StreamlitPage
return StreamlitPage(page_input)
raise StreamlitAPIException(
f"Invalid page type: {type(page_input)}. Must be either a string path, "
"a pathlib.Path, a callable function, or a st.Page object."
)
def pages_from_nav_sections(
nav_sections: dict[SectionHeader, list[StreamlitPage]],
) -> list[StreamlitPage]:
page_list = []
for pages in nav_sections.values():
for page in pages:
page_list.append(page)
return page_list
def send_page_not_found(ctx: ScriptRunContext):
msg = ForwardMsg()
msg.page_not_found.page_name = ""
ctx.enqueue(msg)
@gather_metrics("navigation")
def navigation(
pages: Sequence[PageType] | Mapping[SectionHeader, Sequence[PageType]],
*,
position: Literal["sidebar", "hidden"] = "sidebar",
expanded: bool = False,
) -> StreamlitPage:
"""
Configure the available pages in a multipage app.
Call ``st.navigation`` in your entrypoint file to define the available
pages for your app. ``st.navigation`` returns the current page, which can
be executed using ``.run()`` method.
When using ``st.navigation``, your entrypoint file (the file passed to
``streamlit run``) acts like a router or frame of common elements around
each of your pages. Streamlit executes the entrypoint file with every app
rerun. To execute the current page, you must call the ``.run()`` method on
the ``StreamlitPage`` object returned by ``st.navigation``.
The set of available pages can be updated with each rerun for dynamic
navigation. By default, ``st.navigation`` displays the available pages in
the sidebar if there is more than one page. This behavior can be changed
using the ``position`` keyword argument.
As soon as any session of your app executes the ``st.navigation`` command,
your app will ignore the ``pages/`` directory (across all sessions).
Parameters
----------
pages : Sequence[page-like], Mapping[str, Sequence[page-like]]
The available pages for the app.
To create a navigation menu with no sections or page groupings,
``pages`` must be a list of page-like objects. Page-like objects are
anything that can be passed to ``st.Page`` or a ``StreamlitPage``
object returned by ``st.Page``.
To create labeled sections or page groupings within the navigation
menu, ``pages`` must be a dictionary. Each key is the label of a
section and each value is the list of page-like objects for
that section.
When you use a string or path as a page-like object, they are
internally passed to ``st.Page`` and converted to ``StreamlitPage``
objects. In this case, the page will have the default title, icon, and
path inferred from its path or filename. To customize these attributes
for your page, initialize your page with ``st.Page``.
position : "sidebar" or "hidden"
The position of the navigation menu. If this is ``"sidebar"``
(default), the navigation widget appears at the top of the sidebar. If
this is ``"hidden"``, the navigation widget is not displayed.
If there is only one page in ``pages``, the navigation will be hidden
for any value of ``position``.
expanded : bool
Whether the navigation menu should be expanded. If this is ``False``
(default), the navigation menu will be collapsed and will include a
button to view more options when there are too many pages to display.
If this is ``True``, the navigation menu will always be expanded; no
button to collapse the menu will be displayed.
If ``st.navigation`` changes from ``expanded=True`` to
``expanded=False`` on a rerun, the menu will stay expanded and a
collapse button will be displayed.
Returns
-------
StreamlitPage
The current page selected by the user. To run the page, you must use
the ``.run()`` method on it.
Examples
--------
The following examples show different possible entrypoint files, each named
``streamlit_app.py``. An entrypoint file is passed to ``streamlit run``. It
manages your app's navigation and serves as a router between pages.
**Example 1: Use a callable or Python file as a page**
You can declare pages from callables or file paths. If you pass callables
or paths to ``st.navigation`` as a page-like objects, they are internally
converted to ``StreamlitPage`` objects using ``st.Page``. In this case, the
page titles, icons, and paths are inferred from the file or callable names.
``page_1.py`` (in the same directory as your entrypoint file):
>>> import streamlit as st
>>>
>>> st.title("Page 1")
``streamlit_app.py``:
>>> import streamlit as st
>>>
>>> def page_2():
... st.title("Page 2")
>>>
>>> pg = st.navigation(["page_1.py", page_2])
>>> pg.run()
.. output::
https://doc-navigation-example-1.streamlit.app/
height: 200px
**Example 2: Group pages into sections and customize them with ``st.Page``**
You can use a dictionary to create sections within your navigation menu. In
the following example, each page is similar to Page 1 in Example 1, and all
pages are in the same directory. However, you can use Python files from
anywhere in your repository. ``st.Page`` is used to give each page a custom
title. For more information, see |st.Page|_.
Directory structure:
>>> your_repository/
>>> ├── create_account.py
>>> ├── learn.py
>>> ├── manage_account.py
>>> ├── streamlit_app.py
>>> └── trial.py
``streamlit_app.py``:
>>> import streamlit as st
>>>
>>> pages = {
... "Your account": [
... st.Page("create_account.py", title="Create your account"),
... st.Page("manage_account.py", title="Manage your account"),
... ],
... "Resources": [
... st.Page("learn.py", title="Learn about us"),
... st.Page("trial.py", title="Try it out"),
... ],
... }
>>>
>>> pg = st.navigation(pages)
>>> pg.run()
.. output::
https://doc-navigation-example-2.streamlit.app/
height: 300px
**Example 3: Stateful widgets across multiple pages**
Call widget functions in your entrypoint file when you want a widget to be
stateful across pages. Assign keys to your common widgets and access their
values through Session State within your pages.
``streamlit_app.py``:
>>> import streamlit as st
>>>
>>> def page1():
>>> st.write(st.session_state.foo)
>>>
>>> def page2():
>>> st.write(st.session_state.bar)
>>>
>>> # Widgets shared by all the pages
>>> st.sidebar.selectbox("Foo", ["A", "B", "C"], key="foo")
>>> st.sidebar.checkbox("Bar", key="bar")
>>>
>>> pg = st.navigation([page1, page2])
>>> pg.run()
.. output::
https://doc-navigation-multipage-widgets.streamlit.app/
height: 350px
.. |st.Page| replace:: ``st.Page``
.. _st.Page: https://docs.streamlit.io/develop/api-reference/navigation/st.page
"""
# Disable the use of the pages feature (ie disregard v1 behavior of Multipage Apps)
PagesManager.uses_pages_directory = False
return _navigation(pages, position=position, expanded=expanded)
def _navigation(
pages: Sequence[PageType] | Mapping[SectionHeader, Sequence[PageType]],
*,
position: Literal["sidebar", "hidden"],
expanded: bool,
) -> StreamlitPage:
if isinstance(pages, Sequence):
converted_pages = [convert_to_streamlit_page(p) for p in pages]
nav_sections = {"": converted_pages}
else:
nav_sections = {
section: [convert_to_streamlit_page(p) for p in section_pages]
for section, section_pages in pages.items()
}
page_list = pages_from_nav_sections(nav_sections)
if not page_list:
raise StreamlitAPIException(
"`st.navigation` must be called with at least one `st.Page`."
)
default_page = None
pagehash_to_pageinfo: dict[PageHash, PageInfo] = {}
# Get the default page.
for section_header in nav_sections:
for page in nav_sections[section_header]:
if page._default:
if default_page is not None:
raise StreamlitAPIException(
"Multiple Pages specified with `default=True`. "
"At most one Page can be set to default."
)
default_page = page
if default_page is None:
default_page = page_list[0]
default_page._default = True
ctx = get_script_run_ctx()
if not ctx:
# This should never run in Streamlit, but we want to make sure that
# the function always returns a page
default_page._can_be_called = True
return default_page
# Build the pagehash-to-pageinfo mapping.
for section_header in nav_sections:
for page in nav_sections[section_header]:
if isinstance(page._page, Path):
script_path = str(page._page)
else:
script_path = ""
script_hash = page._script_hash
if script_hash in pagehash_to_pageinfo:
# The page script hash is soley based on the url path
# So duplicate page script hashes are due to duplicate url paths
raise StreamlitAPIException(
f"Multiple Pages specified with URL pathname {page.url_path}. "
"URL pathnames must be unique. The url pathname may be "
"inferred from the filename, callable name, or title."
)
pagehash_to_pageinfo[script_hash] = {
"page_script_hash": script_hash,
"page_name": page.title,
"icon": page.icon,
"script_path": script_path,
"url_pathname": page.url_path,
}
msg = ForwardMsg()
if position == "hidden":
msg.navigation.position = NavigationProto.Position.HIDDEN
elif config.get_option("client.showSidebarNavigation") is False:
msg.navigation.position = NavigationProto.Position.HIDDEN
else:
msg.navigation.position = NavigationProto.Position.SIDEBAR
msg.navigation.expanded = expanded
msg.navigation.sections[:] = nav_sections.keys()
for section_header in nav_sections:
for page in nav_sections[section_header]:
p = msg.navigation.app_pages.add()
p.page_script_hash = page._script_hash
p.page_name = page.title
p.icon = page.icon
p.is_default = page._default
p.section_header = section_header
p.url_pathname = page.url_path
# Inform our page manager about the set of pages we have
ctx.pages_manager.set_pages(pagehash_to_pageinfo)
found_page = ctx.pages_manager.get_page_script(
fallback_page_hash=default_page._script_hash
)
page_to_return = None
if found_page:
found_page_script_hash = found_page["page_script_hash"]
matching_pages = [
p for p in page_list if p._script_hash == found_page_script_hash
]
if len(matching_pages) > 0:
page_to_return = matching_pages[0]
if not page_to_return:
send_page_not_found(ctx)
page_to_return = default_page
# Ordain the page that can be called
page_to_return._can_be_called = True
msg.navigation.page_script_hash = page_to_return._script_hash
# Set the current page script hash to the page that is going to be executed
ctx.set_mpa_v2_page(page_to_return._script_hash)
# This will either navigation or yield if the page is not found
ctx.enqueue(msg)
return page_to_return

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
import random
from collections.abc import Mapping
from pathlib import Path
from textwrap import dedent
from typing import TYPE_CHECKING, Any, Final, Literal, Union, cast
from typing_extensions import TypeAlias
from streamlit.elements.lib.image_utils import AtomicImage, image_to_url
from streamlit.errors import (
StreamlitInvalidMenuItemKeyError,
StreamlitInvalidPageLayoutError,
StreamlitInvalidSidebarStateError,
StreamlitInvalidURLError,
)
from streamlit.proto.ForwardMsg_pb2 import ForwardMsg as ForwardProto
from streamlit.proto.PageConfig_pb2 import PageConfig as PageConfigProto
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
from streamlit.string_util import is_emoji, validate_material_icon
from streamlit.url_util import is_url
if TYPE_CHECKING:
from typing_extensions import TypeGuard
GET_HELP_KEY: Final = "get help"
REPORT_A_BUG_KEY: Final = "report a bug"
ABOUT_KEY: Final = "about"
PageIcon: TypeAlias = Union[AtomicImage, str]
Layout: TypeAlias = Literal["centered", "wide"]
InitialSideBarState: TypeAlias = Literal["auto", "expanded", "collapsed"]
_GetHelp: TypeAlias = Literal["Get help", "Get Help", "get help"]
_ReportABug: TypeAlias = Literal["Report a bug", "report a bug"]
_About: TypeAlias = Literal["About", "about"]
MenuKey: TypeAlias = Literal[_GetHelp, _ReportABug, _About]
MenuItems: TypeAlias = Mapping[MenuKey, Union[str, None]]
# Emojis recommended by https://share.streamlit.io/rensdimmendaal/emoji-recommender/main/app/streamlit.py
# for the term "streamlit". Watch out for zero-width joiners,
# as they won't parse correctly in the list() call!
RANDOM_EMOJIS: Final = list(
"🔥™🎉🚀🌌💣✨🌙🎆🎇💥🤩🤙🌛🤘⬆💡🤪🥂⚡💨🌠🎊🍿😛🔮🤟🌃🍃🍾💫▪🌴🎈🎬🌀🎄😝☔⛽🍂💃😎🍸🎨🥳☀😍🅱🌞😻🌟😜💦💅🦄😋😉👻🍁🤤👯🌻‼🌈👌🎃💛😚🔫🙌👽🍬🌅☁🍷👭☕🌚💁👅🥰🍜😌🎥🕺❕🧡☄💕🍻✅🌸🚬🤓🍹®☺💪😙☘🤠✊🤗🍵🤞😂💯😏📻🎂💗💜🌊❣🌝😘💆🤑🌿🦋😈⛄🚿😊🌹🥴😽💋😭🖤🙆👐⚪💟☃🙈🍭💻🥀🚗🤧🍝💎💓🤝💄💖🔞⁉⏰🕊🎧☠♥🌳🏾🙉⭐💊🍳🌎🙊💸❤🔪😆🌾✈📚💀🏠✌🏃🌵🚨💂🤫🤭😗😄🍒👏🙃🖖💞😅🎅🍄🆓👉💩🔊🤷⌚👸😇🚮💏👳🏽💘💿💉👠🎼🎶🎤👗❄🔐🎵🤒🍰👓🏄🌲🎮🙂📈🚙📍😵🗣❗🌺🙄👄🚘🥺🌍🏡♦💍🌱👑👙☑👾🍩🥶📣🏼🤣☯👵🍫➡🎀😃✋🍞🙇😹🙏👼🐝⚫🎁🍪🔨🌼👆👀😳🌏📖👃🎸👧💇🔒💙😞⛅🏻🍴😼🗿🍗♠🦁✔🤖☮🐢🐎💤😀🍺😁😴📺☹😲👍🎭💚🍆🍋🔵🏁🔴🔔🧐👰☎🏆🤡🐠📲🙋📌🐬✍🔑📱💰🐱💧🎓🍕👟🐣👫🍑😸🍦👁🆗🎯📢🚶🦅🐧💢🏀🚫💑🐟🌽🏊🍟💝💲🐍🍥🐸☝♣👊⚓❌🐯🏈📰🌧👿🐳💷🐺📞🆒🍀🤐🚲🍔👹🙍🌷🙎🐥💵🔝📸⚠❓🎩✂🍼😑⬇⚾🍎💔🐔⚽💭🏌🐷🍍✖🍇📝🍊🐙👋🤔🥊🗽🐑🐘🐰💐🐴♀🐦🍓✏👂🏴👇🆘😡🏉👩💌😺✝🐼🐒🐶👺🖕👬🍉🐻🐾⬅⏬▶👮🍌♂🔸👶🐮👪⛳🐐🎾🐕👴🐨🐊🔹©🎣👦👣👨👈💬⭕📹📷"
)
# Also pick out some vanity emojis.
ENG_EMOJIS: Final = [
"🎈", # st.balloons 🎈🎈
"🤓", # Abhi
"🏈", # Amey
"🚲", # Thiago
"🐧", # Matteo
"🦒", # Ken
"🐳", # Karrie
"🕹️", # Jonathan
"🇦🇲", # Henrikh
"🎸", # Guido
"🦈", # Austin
"💎", # Emiliano
"👩‍🎤", # Naomi
"🧙‍♂️", # Jon
"🐻", # Brandon
"🎎", # James
# TODO: Solicit emojis from the rest of Streamlit
]
def _lower_clean_dict_keys(dict: MenuItems) -> dict[str, Any]:
return {str(k).lower().strip(): v for k, v in dict.items()}
def _get_favicon_string(page_icon: PageIcon) -> str:
"""Return the string to pass to the frontend to have it show
the given PageIcon.
If page_icon is a string that looks like an emoji (or an emoji shortcode),
we return it as-is. Otherwise we use `image_to_url` to return a URL.
(If `image_to_url` raises an error and page_icon is a string, return
the unmodified page_icon string instead of re-raising the error.)
"""
# Choose a random emoji.
if page_icon == "random":
return get_random_emoji()
# If page_icon is an emoji, return it as is.
if isinstance(page_icon, str) and is_emoji(page_icon):
return f"emoji:{page_icon}"
if isinstance(page_icon, str) and page_icon.startswith(":material"):
return validate_material_icon(page_icon)
# Convert Path to string if necessary
if isinstance(page_icon, Path):
page_icon = str(page_icon)
# Fall back to image_to_url.
try:
return image_to_url(
page_icon,
width=-1, # Always use full width for favicons
clamp=False,
channels="RGB",
output_format="auto",
image_id="favicon",
)
except Exception:
if isinstance(page_icon, str):
# This fall-thru handles emoji shortcode strings (e.g. ":shark:"),
# which aren't valid filenames and so will cause an Exception from
# `image_to_url`.
return page_icon
raise
@gather_metrics("set_page_config")
def set_page_config(
page_title: str | None = None,
page_icon: PageIcon | None = None,
layout: Layout = "centered",
initial_sidebar_state: InitialSideBarState = "auto",
menu_items: MenuItems | None = None,
) -> None:
"""
Configures the default settings of the page.
.. note::
This must be the first Streamlit command used on an app page, and must only
be set once per page.
Parameters
----------
page_title: str or None
The page title, shown in the browser tab. If None, defaults to the
filename of the script ("app.py" would show "app • Streamlit").
page_icon : Anything supported by st.image (except list), str, or None
The page favicon. If ``page_icon`` is ``None`` (default), the favicon
will be a monochrome Streamlit logo.
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 ``page_icon="🦈"``.
- An emoji short code. For example, you can set ``page_icon=":shark:"``.
For a list of all supported codes, see
https://share.streamlit.io/streamlit/emoji-shortcodes.
- The string literal, ``"random"``. You can set ``page_icon="random"``
to set a random emoji from the supported list above.
- 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, ``page_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.
.. note::
Colors are not supported for Material icons. When you use a
Material icon for favicon, it will be black, regardless of browser
theme.
.. |st.image| replace:: ``st.image``
.. _st.image: https://docs.streamlit.io/develop/api-reference/media/st.image
layout: "centered" or "wide"
How the page content should be laid out. Defaults to "centered",
which constrains the elements into a centered column of fixed width;
"wide" uses the entire screen.
initial_sidebar_state: "auto", "expanded", or "collapsed"
How the sidebar should start out. Defaults to "auto",
which hides the sidebar on small devices and shows it otherwise.
"expanded" shows the sidebar initially; "collapsed" hides it.
In most cases, you should just use "auto", otherwise the app will
look bad when embedded and viewed on mobile.
menu_items: dict
Configure the menu that appears on the top-right side of this app.
The keys in this dict denote the menu item you'd like to configure:
- "Get help": str or None
The URL this menu item should point to.
If None, hides this menu item.
- "Report a Bug": str or None
The URL this menu item should point to.
If None, hides this menu item.
- "About": str or None
A markdown string to show in the About dialog.
If None, only shows Streamlit's default About text.
The URL may also refer to an email address e.g. ``mailto:john@example.com``.
Example
-------
>>> import streamlit as st
>>>
>>> st.set_page_config(
... page_title="Ex-stream-ly Cool App",
... page_icon="🧊",
... layout="wide",
... initial_sidebar_state="expanded",
... menu_items={
... 'Get Help': 'https://www.extremelycoolapp.com/help',
... 'Report a bug': "https://www.extremelycoolapp.com/bug",
... 'About': "# This is a header. This is an *extremely* cool app!"
... }
... )
"""
msg = ForwardProto()
if page_title is not None:
msg.page_config_changed.title = page_title
if page_icon is not None:
msg.page_config_changed.favicon = _get_favicon_string(page_icon)
pb_layout: PageConfigProto.Layout.ValueType
if layout == "centered":
pb_layout = PageConfigProto.CENTERED
elif layout == "wide":
pb_layout = PageConfigProto.WIDE
else:
raise StreamlitInvalidPageLayoutError(layout=layout)
msg.page_config_changed.layout = pb_layout
pb_sidebar_state: PageConfigProto.SidebarState.ValueType
if initial_sidebar_state == "auto":
pb_sidebar_state = PageConfigProto.AUTO
elif initial_sidebar_state == "expanded":
pb_sidebar_state = PageConfigProto.EXPANDED
elif initial_sidebar_state == "collapsed":
pb_sidebar_state = PageConfigProto.COLLAPSED
else:
raise StreamlitInvalidSidebarStateError(
initial_sidebar_state=initial_sidebar_state
)
msg.page_config_changed.initial_sidebar_state = pb_sidebar_state
if menu_items is not None:
lowercase_menu_items = cast("MenuItems", _lower_clean_dict_keys(menu_items))
validate_menu_items(lowercase_menu_items)
menu_items_proto = msg.page_config_changed.menu_items
set_menu_items_proto(lowercase_menu_items, menu_items_proto)
ctx = get_script_run_ctx()
if ctx is None:
return
ctx.enqueue(msg)
def get_random_emoji() -> str:
# Weigh our emojis 10x, cuz we're awesome!
# TODO: fix the random seed with a hash of the user's app code, for stability?
return random.choice(RANDOM_EMOJIS + 10 * ENG_EMOJIS)
def set_menu_items_proto(lowercase_menu_items, menu_items_proto) -> None:
if GET_HELP_KEY in lowercase_menu_items:
if lowercase_menu_items[GET_HELP_KEY] is not None:
menu_items_proto.get_help_url = lowercase_menu_items[GET_HELP_KEY]
else:
menu_items_proto.hide_get_help = True
if REPORT_A_BUG_KEY in lowercase_menu_items:
if lowercase_menu_items[REPORT_A_BUG_KEY] is not None:
menu_items_proto.report_a_bug_url = lowercase_menu_items[REPORT_A_BUG_KEY]
else:
menu_items_proto.hide_report_a_bug = True
if ABOUT_KEY in lowercase_menu_items:
if lowercase_menu_items[ABOUT_KEY] is not None:
menu_items_proto.about_section_md = dedent(lowercase_menu_items[ABOUT_KEY])
def validate_menu_items(menu_items: MenuItems) -> None:
for k, v in menu_items.items():
if not valid_menu_item_key(k):
raise StreamlitInvalidMenuItemKeyError(key=k)
if v is not None and (
not is_url(v, ("http", "https", "mailto")) and k != ABOUT_KEY
):
raise StreamlitInvalidURLError(url=v)
def valid_menu_item_key(key: str) -> TypeGuard[MenuKey]:
return key in {GET_HELP_KEY, REPORT_A_BUG_KEY, ABOUT_KEY}

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,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,84 @@
# 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 threading
from typing import TYPE_CHECKING, Final
from streamlit import util
from streamlit.components.types.base_component_registry import BaseComponentRegistry
from streamlit.errors import StreamlitAPIException
from streamlit.logger import get_logger
if TYPE_CHECKING:
from streamlit.components.types.base_custom_component import BaseCustomComponent
_LOGGER: Final = get_logger(__name__)
class LocalComponentRegistry(BaseComponentRegistry):
def __init__(self) -> None:
self._components: dict[str, BaseCustomComponent] = {}
self._lock = threading.Lock()
def __repr__(self) -> str:
return util.repr_(self)
def register_component(self, component: BaseCustomComponent) -> None:
"""Register a CustomComponent.
Parameters
----------
component : BaseCustomComponent
The component to register.
"""
# Validate the component's path
abspath = component.abspath
if abspath is not None and not os.path.isdir(abspath):
raise StreamlitAPIException(f"No such component directory: '{abspath}'")
with self._lock:
existing = self._components.get(component.name)
self._components[component.name] = component
if existing is not None and component != existing:
_LOGGER.warning(
"%s overriding previously-registered %s",
component,
existing,
)
_LOGGER.debug("Registered component %s", component)
def get_component_path(self, name: str) -> str | None:
"""Return the filesystem path for the component with the given name.
If no such component is registered, or if the component exists but is
being served from a URL, return None instead.
"""
component = self._components.get(name, None)
return component.abspath if component is not None else None
def get_module_name(self, name: str) -> str | None:
component = self._components.get(name, None)
return component.module_name if component is not None else None
def get_component(self, name: str) -> BaseCustomComponent | None:
return self._components.get(name, None)
def get_components(self) -> list[BaseCustomComponent]:
return list(self._components.values())

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,99 @@
# 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 abc import abstractmethod
from typing import TYPE_CHECKING, Protocol
if TYPE_CHECKING:
from streamlit.components.types.base_custom_component import BaseCustomComponent
class BaseComponentRegistry(Protocol):
"""Interface for ComponentRegistries."""
@abstractmethod
def register_component(self, component: BaseCustomComponent) -> None:
"""Register a CustomComponent.
Parameters
----------
component : CustomComponent
The component to register.
"""
raise NotImplementedError
@abstractmethod
def get_component_path(self, name: str) -> str | None:
"""Return the filesystem path for the component with the given name.
If no such component is registered, or if the component exists but is
being served from a URL, return None instead.
Parameters
----------
name: name of the component
Returns
-------
str or None
The name of the specified component or None if no component with the given name has been registered.
"""
raise NotImplementedError
@abstractmethod
def get_module_name(self, name: str) -> str | None:
"""Return the module name for the component with the given name.
If no such component is registered, return None instead.
Parameters
----------
name: name of the component
Returns
-------
str or None
The module_name of the specified component or None if no component with the given name has been registered.
"""
raise NotImplementedError
@abstractmethod
def get_component(self, name: str) -> BaseCustomComponent | None:
"""Return the registered component with the given name.
If no such component is registered, return None instead.
Parameters
----------
name: name of the component
Returns
-------
component or None
The component with the provided name or None if component with the given name has been registered.
"""
raise NotImplementedError
@abstractmethod
def get_components(self) -> list[BaseCustomComponent]:
"""Returns a list of custom components that are registered in this registry.
Returns
-------
list[CustomComponents]
A list of registered custom components.
"""
raise NotImplementedError

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.
from __future__ import annotations
import os
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any
from streamlit import util
from streamlit.errors import StreamlitAPIException
if TYPE_CHECKING:
from streamlit.runtime.state.common import WidgetCallback
class MarshallComponentException(StreamlitAPIException):
"""Class for exceptions generated during custom component marshalling."""
pass
class BaseCustomComponent(ABC):
"""Interface for CustomComponents."""
def __init__(
self,
name: str,
path: str | None = None,
url: str | None = None,
module_name: str | None = None,
):
if (path is None and url is None) or (path is not None and url is not None):
raise StreamlitAPIException(
"Either 'path' or 'url' must be set, but not both."
)
self._name = name
self._path = path
self._url = url
self._module_name = module_name
def __repr__(self) -> str:
return util.repr_(self)
def __call__(
self,
*args,
default: Any = None,
key: str | None = None,
on_change: WidgetCallback | None = None,
**kwargs,
) -> Any:
"""An alias for create_instance."""
return self.create_instance(
*args,
default=default,
key=key,
on_change=on_change,
**kwargs,
)
@property
def abspath(self) -> str | None:
if self._path is None:
return None
return os.path.abspath(self._path)
@property
def module_name(self) -> str | None:
return self._module_name
@property
def name(self) -> str:
return self._name
@property
def path(self) -> str | None:
return self._path
@property
def url(self) -> str | None:
return self._url
def __str__(self) -> str:
return f"'{self.name}': {self.path if self.path is not None else self.url}"
@abstractmethod
def __eq__(self, other) -> bool:
"""Equality operator."""
return NotImplemented
@abstractmethod
def __ne__(self, other) -> bool:
"""Inequality operator."""
return NotImplemented
@abstractmethod
def create_instance(
self,
*args,
default: Any = None,
key: str | None = None,
on_change: WidgetCallback | None = None,
**kwargs,
) -> Any:
"""Create a new instance of the component.
Parameters
----------
*args
Must be empty; all args must be named. (This parameter exists to
enforce correct use of the function.)
default: any or None
The default return value for the component. This is returned when
the component's frontend hasn't yet specified a value with
`setComponentValue`.
key: str or None
If not None, this is the user key we use to generate the
component's "widget ID".
on_change: WidgetCallback or None
An optional callback invoked when the widget's value changes. No arguments are passed to it.
**kwargs
Keyword args to pass to the component.
Raises
------
MarshallComponentException
Raised when args is not empty or component cannot be marshalled.
StreamlitAPIException
Raised when PyArrow is not installed.
Returns
-------
any or None
The component's widget value.
"""
raise NotImplementedError

View File

@@ -0,0 +1,29 @@
# 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.
"""Contains the files and modules for the exposed API."""
import streamlit
from streamlit.components.v1.component_registry import declare_component
# `html` and `iframe` are part of Custom Components, so they appear in this
# `streamlit.components.v1` namespace.
html = streamlit._main._html
iframe = streamlit._main._iframe
__all__ = [
"declare_component",
"html",
"iframe",
]

View File

@@ -0,0 +1,141 @@
# 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.
"""Data marshalling utilities for ArrowTable protobufs, which are used by
CustomComponent for dataframe serialization.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from streamlit import dataframe_util
from streamlit.elements.lib import pandas_styler_utils
if TYPE_CHECKING:
from pandas import DataFrame, Index, Series
from streamlit.proto.Components_pb2 import ArrowTable as ArrowTableProto
def _maybe_tuple_to_list(item: Any) -> Any:
"""Convert a tuple to a list. Leave as is if it's not a tuple."""
return list(item) if isinstance(item, tuple) else item
def marshall(
proto: ArrowTableProto, data: Any, default_uuid: str | None = None
) -> None:
"""Marshall data into an ArrowTable proto.
Parameters
----------
proto : proto.ArrowTable
Output. The protobuf for a Streamlit ArrowTable proto.
data : pandas.DataFrame, pandas.Styler, numpy.ndarray, Iterable, dict, or None
Something that is or can be converted to a dataframe.
"""
if dataframe_util.is_pandas_styler(data):
pandas_styler_utils.marshall_styler(proto, data, default_uuid) # type: ignore
df = dataframe_util.convert_anything_to_pandas_df(data)
_marshall_index(proto, df.index)
_marshall_columns(proto, df.columns)
_marshall_data(proto, df)
def _marshall_index(proto: ArrowTableProto, index: Index) -> None:
"""Marshall pandas.DataFrame index into an ArrowTable proto.
Parameters
----------
proto : proto.ArrowTable
Output. The protobuf for a Streamlit ArrowTable proto.
index : pd.Index
Index to use for resulting frame.
Will default to RangeIndex (0, 1, 2, ..., n) if no index is provided.
"""
import pandas as pd
index = map(_maybe_tuple_to_list, index.values)
index_df = pd.DataFrame(index)
proto.index = dataframe_util.convert_pandas_df_to_arrow_bytes(index_df)
def _marshall_columns(proto: ArrowTableProto, columns: Series) -> None:
"""Marshall pandas.DataFrame columns into an ArrowTable proto.
Parameters
----------
proto : proto.ArrowTable
Output. The protobuf for a Streamlit ArrowTable proto.
columns : Series
Column labels to use for resulting frame.
Will default to RangeIndex (0, 1, 2, ..., n) if no column labels are provided.
"""
import pandas as pd
columns = map(_maybe_tuple_to_list, columns.values)
columns_df = pd.DataFrame(columns)
proto.columns = dataframe_util.convert_pandas_df_to_arrow_bytes(columns_df)
def _marshall_data(proto: ArrowTableProto, df: DataFrame) -> None:
"""Marshall pandas.DataFrame data into an ArrowTable proto.
Parameters
----------
proto : proto.ArrowTable
Output. The protobuf for a Streamlit ArrowTable proto.
df : pandas.DataFrame
A dataframe to marshall.
"""
proto.data = dataframe_util.convert_pandas_df_to_arrow_bytes(df)
def arrow_proto_to_dataframe(proto: ArrowTableProto) -> DataFrame:
"""Convert ArrowTable proto to pandas.DataFrame.
Parameters
----------
proto : proto.ArrowTable
Output. pandas.DataFrame
"""
if dataframe_util.is_pyarrow_version_less_than("14.0.1"):
raise RuntimeError(
"The installed pyarrow version is not compatible with this component. "
"Please upgrade to 14.0.1 or higher: pip install -U pyarrow"
)
import pandas as pd
data = dataframe_util.convert_arrow_bytes_to_pandas_df(proto.data)
index = dataframe_util.convert_arrow_bytes_to_pandas_df(proto.index)
columns = dataframe_util.convert_arrow_bytes_to_pandas_df(proto.columns)
return pd.DataFrame(
data.to_numpy(),
index=index.to_numpy().T.tolist(),
columns=columns.to_numpy().T.tolist(),
)

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
import inspect
import os
from pathlib import Path
from typing import TYPE_CHECKING
from streamlit.components.v1.custom_component import CustomComponent
from streamlit.runtime import get_instance
from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
if TYPE_CHECKING:
from types import FrameType
from streamlit.components.types.base_component_registry import BaseComponentRegistry
def _get_module_name(caller_frame: FrameType) -> str:
# Get the caller's module name. `__name__` gives us the module's
# fully-qualified name, which includes its package.
module = inspect.getmodule(caller_frame)
assert module is not None
module_name = module.__name__
# If the caller was the main module that was executed (that is, if the
# user executed `python my_component.py`), then this name will be
# "__main__" instead of the actual package name. In this case, we use
# the main module's filename, sans `.py` extension, as the component name.
if module_name == "__main__":
file_path = inspect.getfile(caller_frame)
filename = os.path.basename(file_path)
module_name, _ = os.path.splitext(filename)
return module_name
def declare_component(
name: str,
path: str | Path | None = None,
url: str | None = None,
) -> CustomComponent:
"""Create a custom component and register it if there is a ``ScriptRunContext``.
The component is not registered when there is no ``ScriptRunContext``.
This can happen when a ``CustomComponent`` is executed as standalone
command (e.g. for testing).
To use this function, import it from the ``streamlit.components.v1``
module.
.. warning::
Using ``st.components.v1.declare_component`` directly (instead of
importing its module) is deprecated and will be disallowed in a later
version.
Parameters
----------
name : str
A short, descriptive name for the component, like "slider".
path: str, Path, or None
The path to serve the component's frontend files from. The path should
be absolute. If ``path`` is ``None`` (default), Streamlit will serve
the component from the location in ``url``. Either ``path`` or ``url``
must be specified, but not both.
url: str or None
The URL that the component is served from. If ``url`` is ``None``
(default), Streamlit will serve the component from the location in
``path``. Either ``path`` or ``url`` must be specified, but not both.
Returns
-------
CustomComponent
A ``CustomComponent`` that can be called like a function.
Calling the component will create a new instance of the component
in the Streamlit app.
"""
if path is not None and isinstance(path, Path):
path = str(path)
# Get our stack frame.
current_frame: FrameType | None = inspect.currentframe()
assert current_frame is not None
# Get the stack frame of our calling function.
caller_frame = current_frame.f_back
assert caller_frame is not None
module_name = _get_module_name(caller_frame)
# Build the component name.
component_name = f"{module_name}.{name}"
# Create our component object, and register it.
component = CustomComponent(
name=component_name, path=path, url=url, module_name=module_name
)
# the ctx can be None if a custom component script is run outside of Streamlit, e.g. via 'python ...'
ctx = get_script_run_ctx()
if ctx is not None:
get_instance().component_registry.register_component(component)
return component
# Keep for backwards-compatibility for now as we don't know whether existing custom
# components use this method. We made significant refactors to the custom component
# registry code in https://github.com/streamlit/streamlit/pull/8193 and after
# that is out in the wild, we can follow-up with more refactorings, e.g. remove
# the following class and method. When we do that, we should conduct some testing with
# popular custom components.
class ComponentRegistry:
@classmethod
def instance(cls) -> BaseComponentRegistry:
"""Returns the ComponentRegistry of the runtime instance."""
return get_instance().component_registry

View File

@@ -0,0 +1,38 @@
# 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.
# The components.py file exists because existing custom components have started
# to rely on internals of the components package. For example, streamlit-option-menu accesses
# [register_widget](https://github.com/victoryhb/streamlit-option-menu/blob/master/streamlit_option_menu/streamlit_callback.py#L28),
# which is only a transitive import through `streamlit.components.v1.custom_component`.
# Since we do not know what other internals are used out in the wild, let's try to
# model the old behavior and not to break things.
# This should be cleaned up in the future, e.g. as part of components v2.
from streamlit.components.v1.component_registry import (
declare_component,
)
from streamlit.components.v1.custom_component import (
CustomComponent,
MarshallComponentException,
)
from streamlit.runtime.state import register_widget
__all__ = [
"CustomComponent",
"declare_component",
"MarshallComponentException",
"register_widget",
]

View File

@@ -0,0 +1,243 @@
# 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 typing import TYPE_CHECKING, Any
from streamlit.components.types.base_custom_component import BaseCustomComponent
from streamlit.dataframe_util import is_dataframe_like
from streamlit.delta_generator_singletons import get_dg_singleton_instance
from streamlit.elements.lib.form_utils import current_form_id
from streamlit.elements.lib.policies import check_cache_replay_rules
from streamlit.elements.lib.utils import compute_and_register_element_id
from streamlit.errors import StreamlitAPIException
from streamlit.proto.Components_pb2 import ArrowTable as ArrowTableProto
from streamlit.proto.Components_pb2 import SpecialArg
from streamlit.proto.Element_pb2 import Element
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 register_widget
from streamlit.type_util import is_bytes_like, to_bytes
if TYPE_CHECKING:
from streamlit.delta_generator import DeltaGenerator
from streamlit.runtime.state.common import WidgetCallback
class MarshallComponentException(StreamlitAPIException):
"""Class for exceptions generated during custom component marshalling."""
pass
class CustomComponent(BaseCustomComponent):
"""A Custom Component declaration."""
def __call__(
self,
*args,
default: Any = None,
key: str | None = None,
on_change: WidgetCallback | None = None,
**kwargs,
) -> Any:
"""An alias for create_instance."""
return self.create_instance(
*args,
default=default,
key=key,
on_change=on_change,
**kwargs,
)
@gather_metrics("create_instance")
def create_instance(
self,
*args,
default: Any = None,
key: str | None = None,
on_change: WidgetCallback | None = None,
**kwargs,
) -> Any:
"""Create a new instance of the component.
Parameters
----------
*args
Must be empty; all args must be named. (This parameter exists to
enforce correct use of the function.)
default: any or None
The default return value for the component. This is returned when
the component's frontend hasn't yet specified a value with
`setComponentValue`.
key: str or None
If not None, this is the user key we use to generate the
component's "widget ID".
on_change: WidgetCallback or None
An optional callback invoked when the widget's value changes. No arguments are passed to it.
**kwargs
Keyword args to pass to the component.
Returns
-------
any or None
The component's widget value.
"""
if len(args) > 0:
raise MarshallComponentException(f"Argument '{args[0]}' needs a label")
try:
import pyarrow # noqa: F401, ICN001
from streamlit.components.v1 import component_arrow
except ImportError:
raise StreamlitAPIException(
"""To use Custom Components in Streamlit, you need to install
PyArrow. To do so locally:
`pip install pyarrow`
And if you're using Streamlit Cloud, add "pyarrow" to your requirements.txt."""
)
check_cache_replay_rules()
# In addition to the custom kwargs passed to the component, we also
# send the special 'default' and 'key' params to the component
# frontend.
all_args = dict(kwargs, default=default, key=key)
json_args = {}
special_args = []
for arg_name, arg_val in all_args.items():
if is_bytes_like(arg_val):
bytes_arg = SpecialArg()
bytes_arg.key = arg_name
bytes_arg.bytes = to_bytes(arg_val)
special_args.append(bytes_arg)
elif is_dataframe_like(arg_val):
dataframe_arg = SpecialArg()
dataframe_arg.key = arg_name
component_arrow.marshall(dataframe_arg.arrow_dataframe.data, arg_val)
special_args.append(dataframe_arg)
else:
json_args[arg_name] = arg_val
try:
serialized_json_args = json.dumps(json_args)
except Exception as ex:
raise MarshallComponentException(
"Could not convert component args to JSON", ex
)
def marshall_component(dg: DeltaGenerator, element: Element) -> Any:
element.component_instance.component_name = self.name
element.component_instance.form_id = current_form_id(dg)
if self.url is not None:
element.component_instance.url = self.url
# Normally, a widget's element_hash (which determines
# its identity across multiple runs of an app) is computed
# by hashing its arguments. This means that, if any of the arguments
# to the widget are changed, Streamlit considers it a new widget
# instance and it loses its previous state.
#
# However! If a *component* has a `key` argument, then the
# component's hash identity is determined by entirely by
# `component_name + url + key`. This means that, when `key`
# exists, the component will maintain its identity even when its
# other arguments change, and the component's iframe won't be
# remounted on the frontend.
def marshall_element_args():
element.component_instance.json_args = serialized_json_args
element.component_instance.special_args.extend(special_args)
ctx = get_script_run_ctx()
if key is None:
marshall_element_args()
computed_id = compute_and_register_element_id(
"component_instance",
user_key=key,
form_id=current_form_id(dg),
name=self.name,
url=self.url,
json_args=serialized_json_args,
special_args=special_args,
)
else:
computed_id = compute_and_register_element_id(
"component_instance",
user_key=key,
form_id=current_form_id(dg),
name=self.name,
url=self.url,
)
element.component_instance.id = computed_id
def deserialize_component(ui_value, widget_id=""):
# ui_value is an object from json, an ArrowTable proto, or a bytearray
return ui_value
component_state = register_widget(
element.component_instance.id,
deserializer=deserialize_component,
serializer=lambda x: x,
ctx=ctx,
on_change_handler=on_change,
value_type="json_value",
)
widget_value = component_state.value
if key is not None:
marshall_element_args()
if widget_value is None:
widget_value = default
elif isinstance(widget_value, ArrowTableProto):
widget_value = component_arrow.arrow_proto_to_dataframe(widget_value)
return widget_value
# We currently only support writing to st._main, but this will change
# when we settle on an improved API in a post-layout world.
dg = get_dg_singleton_instance().main_dg
element = Element()
return_value = marshall_component(dg, element)
dg._enqueue("component_instance", element.component_instance)
return return_value
def __eq__(self, other) -> bool:
"""Equality operator."""
return (
isinstance(other, CustomComponent)
and self.name == other.name
and self.path == other.path
and self.url == other.url
and self.module_name == other.module_name
)
def __ne__(self, other) -> bool:
"""Inequality operator."""
# we have to use "not X == Y"" here because if we use "X != Y"
# we call __ne__ again and end up in recursion
return not self == other
def __str__(self) -> str:
return f"'{self.name}': {self.path if self.path is not None else self.url}"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,312 @@
# 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.
"""Class to store a key-value pair for the config system."""
from __future__ import annotations
import datetime
import re
import textwrap
from typing import Any, Callable
from streamlit.string_util import to_snake_case
from streamlit.util import repr_
class ConfigOption:
'''Stores a Streamlit configuration option.
A configuration option, like 'browser.serverPort', which indicates which port
to use when connecting to the proxy. There are two ways to create a
ConfigOption:
Simple ConfigOptions are created as follows:
ConfigOption('browser.serverPort',
description = 'Connect to the proxy at this port.',
default_val = 8501)
More complex config options resolve their values at runtime as follows:
@ConfigOption('browser.serverPort')
def _proxy_port():
"""Connect to the proxy at this port.
Defaults to 8501.
"""
return 8501
NOTE: For complex config options, the function is called each time the
option.value is evaluated!
Attributes
----------
key : str
The fully qualified section.name
value : any
The value for this option. If this is a complex config option then
the callback is called EACH TIME value is evaluated.
section : str
The section of this option. Example: 'global'.
name : str
See __init__.
description : str
See __init__.
where_defined : str
Indicates which file set this config option.
ConfigOption.DEFAULT_DEFINITION means this file.
is_default: bool
True if the config value is equal to its default value.
visibility : {"visible", "hidden"}
See __init__.
scriptable : bool
See __init__.
deprecated: bool
See __init__.
deprecation_text : str or None
See __init__.
expiration_date : str or None
See __init__.
replaced_by : str or None
See __init__.
sensitive : bool
See __init__.
env_var: str
The name of the environment variable that can be used to set the option.
'''
# This is a special value for ConfigOption.where_defined which indicates
# that the option default was not overridden.
DEFAULT_DEFINITION = "<default>"
# This is a special value for ConfigOption.where_defined which indicates
# that the options was defined by Streamlit's own code.
STREAMLIT_DEFINITION = "<streamlit>"
def __init__(
self,
key: str,
description: str | None = None,
default_val: Any | None = None,
visibility: str = "visible",
scriptable: bool = False,
deprecated: bool = False,
deprecation_text: str | None = None,
expiration_date: str | None = None,
replaced_by: str | None = None,
type_: type = str,
sensitive: bool = False,
):
"""Create a ConfigOption with the given name.
Parameters
----------
key : str
Should be of the form "section.optionName"
Examples: server.name, deprecation.v1_0_featureName
description : str
Like a comment for the config option.
default_val : any
The value for this config option.
visibility : {"visible", "hidden"}
Whether this option should be shown to users.
scriptable : bool
Whether this config option can be set within a user script.
deprecated: bool
Whether this config option is deprecated.
deprecation_text : str or None
Required if deprecated == True. Set this to a string explaining
what to use instead.
expiration_date : str or None
Required if deprecated == True. set this to the date at which it
will no longer be accepted. Format: 'YYYY-MM-DD'.
replaced_by : str or None
If this is option has been deprecated in favor or another option,
set this to the path to the new option. Example:
'server.runOnSave'. If this is set, the 'deprecated' option
will automatically be set to True, and deprecation_text will have a
meaningful default (unless you override it).
type_ : one of str, int, float or bool
Useful to cast the config params sent by cmd option parameter.
sensitive: bool
Sensitive configuration options cannot be set by CLI parameter.
"""
# Parse out the section and name.
self.key = key
key_format = (
# Capture a group called "section"
r"(?P<section>"
# Matching text comprised of letters and numbers that begins
# with a lowercase letter with an optional "_" preceding it.
# Examples: "_section", "section1"
r"\_?[a-z][a-zA-Z0-9]*"
# Handling zero or additional parts, separated by period
# Examples: "_section.subsection", "section1._section2"
r"(\.[a-z][a-zA-Z0-9]*)*"
r")"
# The final period, separating section and name
r"\."
# Capture a group called "name"
r"(?P<name>"
# Match text comprised of letters and numbers beginning with a
# lowercase letter.
# Examples: "name", "nameOfConfig", "config1"
r"[a-z][a-zA-Z0-9]*"
r")$"
)
match = re.match(key_format, self.key)
assert match, f'Key "{self.key}" has invalid format.'
self.section, self.name = match.group("section"), match.group("name")
self.description = description
self.visibility = visibility
self.scriptable = scriptable
self.default_val = default_val
self.deprecated = deprecated
self.replaced_by = replaced_by
self.is_default = True
self._get_val_func: Callable[[], Any] | None = None
self.where_defined = ConfigOption.DEFAULT_DEFINITION
self.type = type_
self.sensitive = sensitive
# infer multiple values if the default value is a list or tuple
self.multiple = isinstance(default_val, (list, tuple))
if self.replaced_by:
self.deprecated = True
if deprecation_text is None:
deprecation_text = "Replaced by %s." % self.replaced_by
if self.deprecated:
assert expiration_date, "expiration_date is required for deprecated items"
assert deprecation_text, "deprecation_text is required for deprecated items"
self.expiration_date = expiration_date
self.deprecation_text = textwrap.dedent(deprecation_text)
self.set_value(default_val)
def __repr__(self) -> str:
return repr_(self)
def __call__(self, get_val_func: Callable[[], Any]) -> ConfigOption:
"""Assign a function to compute the value for this option.
This method is called when ConfigOption is used as a decorator.
Parameters
----------
get_val_func : function
A function which will be called to get the value of this parameter.
We will use its docString as the description.
Returns
-------
ConfigOption
Returns self, which makes testing easier. See config_test.py.
"""
assert get_val_func.__doc__, (
"Complex config options require doc strings for their description."
)
self.description = get_val_func.__doc__
self._get_val_func = get_val_func
return self
@property
def value(self) -> Any:
"""Get the value of this config option."""
if self._get_val_func is None:
return None
return self._get_val_func()
def set_value(self, value: Any, where_defined: str | None = None) -> None:
"""Set the value of this option.
Parameters
----------
value
The new value for this parameter.
where_defined : str
New value to remember where this parameter was set.
"""
self._get_val_func = lambda: value
if where_defined is None:
self.where_defined = ConfigOption.DEFAULT_DEFINITION
else:
self.where_defined = where_defined
self.is_default = value == self.default_val
if self.deprecated and self.where_defined != ConfigOption.DEFAULT_DEFINITION:
if self.is_expired():
# Import here to avoid circular imports
from streamlit.logger import get_logger
LOGGER = get_logger(__name__)
LOGGER.error(
textwrap.dedent(
f"""
════════════════════════════════════════════════
{self.key} IS NO LONGER SUPPORTED.
{self.deprecation_text}
Please update {self.where_defined}.
════════════════════════════════════════════════
"""
)
)
else:
# Import here to avoid circular imports
from streamlit.logger import get_logger
LOGGER = get_logger(__name__)
LOGGER.warning(
textwrap.dedent(
f"""s
════════════════════════════════════════════════
{self.key} IS DEPRECATED.
{self.deprecation_text}
This option will be removed on or after {self.expiration_date}.
Please update {self.where_defined}.
════════════════════════════════════════════════
"""
)
)
def is_expired(self) -> bool:
"""Returns true if expiration_date is in the past."""
if not self.deprecated:
return False
expiration_date = _parse_yyyymmdd_str(self.expiration_date)
now = datetime.datetime.now()
return now > expiration_date
@property
def env_var(self):
"""Get the name of the environment variable that can be used to set the option."""
name = self.key.replace(".", "_")
return f"STREAMLIT_{to_snake_case(name).upper()}"
def _parse_yyyymmdd_str(date_str: str) -> datetime.datetime:
year, month, day = (int(token) for token in date_str.split("-", 2))
return datetime.datetime(year, month, day)

View File

@@ -0,0 +1,177 @@
# 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 streamlit import cli_util
from streamlit.config_option import ConfigOption
def server_option_changed(
old_options: dict[str, ConfigOption], new_options: dict[str, ConfigOption]
) -> bool:
"""Return True if and only if an option in the server section differs
between old_options and new_options.
"""
for opt_name, opt_val in old_options.items():
if not opt_name.startswith("server"):
continue
old_val = opt_val.value
new_val = new_options[opt_name].value
if old_val != new_val:
return True
return False
def show_config(
section_descriptions: dict[str, str],
config_options: dict[str, ConfigOption],
) -> None:
"""Print the given config sections/options to the terminal."""
out = []
out.append(
_clean(
"""
# Below are all the sections and options you can have in
~/.streamlit/config.toml.
"""
)
)
def append_desc(text):
out.append("# " + cli_util.style_for_cli(text, bold=True))
def append_comment(text):
out.append("# " + cli_util.style_for_cli(text))
def append_section(text):
out.append(cli_util.style_for_cli(text, bold=True, fg="green"))
def append_setting(text):
out.append(cli_util.style_for_cli(text, fg="green"))
for section, _ in section_descriptions.items():
# We inject a fake config section used for unit tests that we exclude here as
# its options are often missing required properties, which confuses the code
# below.
if section == "_test":
continue
section_options = {
k: v
for k, v in config_options.items()
if v.section == section and v.visibility == "visible" and not v.is_expired()
}
# Only show config header if section is non-empty.
if len(section_options) == 0:
continue
out.append("")
append_section("[%s]" % section)
out.append("")
for key, option in section_options.items():
key = option.key.split(".")[-1]
description_paragraphs = _clean_paragraphs(option.description or "")
last_paragraph_idx = len(description_paragraphs) - 1
for i, paragraph in enumerate(description_paragraphs):
# Split paragraph into lines
lines = paragraph.rstrip().split(
"\n"
) # Remove trailing newline characters
# If the first line is empty, remove it
if lines and not lines[0].strip():
lines = lines[1:]
# Choose function based on whether it's the first paragraph or not
append_func = append_desc if i == 0 else append_comment
# Add comment character to each line and add to out
for line in lines:
append_func(line.lstrip())
# # Add a line break after a paragraph only if it's not the last paragraph
if i != last_paragraph_idx:
out.append("")
import toml
toml_default = toml.dumps({"default": option.default_val})
toml_default = toml_default[10:].strip()
if len(toml_default) > 0:
# Ensure a line break before appending "Default" comment, if not already there
if out[-1] != "":
out.append("")
append_comment("Default: %s" % toml_default)
else:
# Don't say "Default: (unset)" here because this branch applies
# to complex config settings too.
pass
if option.deprecated:
append_comment(cli_util.style_for_cli("DEPRECATED.", fg="yellow"))
for line in _clean_paragraphs(option.deprecation_text):
append_comment(line)
append_comment(
"This option will be removed on or after %s."
% option.expiration_date
)
option_is_manually_set = (
option.where_defined != ConfigOption.DEFAULT_DEFINITION
)
if option_is_manually_set:
append_comment("The value below was set in %s" % option.where_defined)
toml_setting = toml.dumps({key: option.value})
if len(toml_setting) == 0:
toml_setting = f"# {key} =\n"
elif not option_is_manually_set:
toml_setting = f"# {toml_setting}"
append_setting(toml_setting)
cli_util.print_to_cli("\n".join(out))
def _clean(txt: str) -> str:
"""Replace sequences of multiple spaces with a single space, excluding newlines.
Preserves leading and trailing spaces, and does not modify spaces in between lines.
"""
return re.sub(" +", " ", txt)
def _clean_paragraphs(txt: str) -> list[str]:
"""Split the text into paragraphs, preserve newlines within the paragraphs."""
# Strip both leading and trailing newlines.
txt = txt.strip("\n")
paragraphs = txt.split("\n\n")
cleaned_paragraphs = [
"\n".join(_clean(line) for line in paragraph.split("\n"))
for paragraph in paragraphs
]
return cleaned_paragraphs

View File

@@ -0,0 +1,28 @@
# 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 streamlit.connections.base_connection import BaseConnection
from streamlit.connections.snowflake_connection import SnowflakeConnection
from streamlit.connections.snowpark_connection import SnowparkConnection
from streamlit.connections.sql_connection import SQLConnection
ExperimentalBaseConnection = BaseConnection
__all__ = [
"BaseConnection",
"SnowflakeConnection",
"SnowparkConnection",
"SQLConnection",
"ExperimentalBaseConnection",
]

View File

@@ -0,0 +1,174 @@
# 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 abc import ABC, abstractmethod
from typing import Any, Generic, TypeVar
from streamlit.runtime.secrets import AttrDict, secrets_singleton
from streamlit.util import calc_md5
RawConnectionT = TypeVar("RawConnectionT")
class BaseConnection(ABC, Generic[RawConnectionT]):
"""The abstract base class that all Streamlit Connections must inherit from.
This base class provides connection authors with a standardized way to hook into the
``st.connection()`` factory function: connection authors are required to provide an
implementation for the abstract method ``_connect`` in their subclasses.
Additionally, it also provides a few methods/properties designed to make
implementation of connections more convenient. See the docstrings for each of the
methods of this class for more information
.. note::
While providing an implementation of ``_connect`` is technically all that's
required to define a valid connection, connections should also provide the user
with context-specific ways of interacting with the underlying connection object.
For example, the first-party SQLConnection provides a ``query()`` method for
reads and a ``session`` property for more complex operations.
"""
def __init__(self, connection_name: str, **kwargs) -> None:
"""Create a BaseConnection.
This constructor is called by the connection factory machinery when a user
script calls ``st.connection()``.
Subclasses of BaseConnection that want to overwrite this method should take care
to also call the base class' implementation.
Parameters
----------
connection_name : str
The name of this connection. This corresponds to the
``[connections.<connection_name>]`` config section in ``st.secrets``.
kwargs : dict
Any other kwargs to pass to this connection class' ``_connect`` method.
Returns
-------
None
"""
self._connection_name = connection_name
self._kwargs = kwargs
self._config_section_hash = calc_md5(json.dumps(self._secrets.to_dict()))
secrets_singleton.file_change_listener.connect(self._on_secrets_changed)
self._raw_instance: RawConnectionT | None = self._connect(**kwargs)
def __del__(self) -> None:
secrets_singleton.file_change_listener.disconnect(self._on_secrets_changed)
def __getattribute__(self, name: str) -> Any:
try:
return object.__getattribute__(self, name)
except AttributeError as e:
if hasattr(self._instance, name):
raise AttributeError(
f"`{name}` doesn't exist here, but you can call `._instance.{name}` instead"
)
raise e
# Methods with default implementations that we don't expect subclasses to want or
# need to overwrite.
def _on_secrets_changed(self, _) -> None:
"""Reset the raw connection object when this connection's secrets change.
We don't expect either user scripts or connection authors to have to use or
overwrite this method.
"""
new_hash = calc_md5(json.dumps(self._secrets.to_dict()))
# Only reset the connection if the secrets file section specific to this
# connection has changed.
if new_hash != self._config_section_hash:
self._config_section_hash = new_hash
self.reset()
@property
def _secrets(self) -> AttrDict:
"""Get the secrets for this connection from the corresponding st.secrets section.
We expect this property to be used primarily by connection authors when they
are implementing their class' ``_connect`` method. User scripts should, for the
most part, have no reason to use this property.
"""
connections_section = None
if secrets_singleton.load_if_toml_exists():
connections_section = secrets_singleton.get("connections")
if type(connections_section) is not AttrDict:
return AttrDict({})
return connections_section.get(self._connection_name, AttrDict({}))
def reset(self) -> None:
"""Reset this connection so that it gets reinitialized the next time it's used.
This method can be useful when a connection has become stale, an auth token has
expired, or in similar scenarios where a broken connection might be fixed by
reinitializing it. Note that some connection methods may already use ``reset()``
in their error handling code.
Returns
-------
None
Example
-------
>>> import streamlit as st
>>>
>>> conn = st.connection("my_conn")
>>>
>>> # Reset the connection before using it if it isn't healthy
>>> # Note: is_healthy() isn't a real method and is just shown for example here.
>>> if not conn.is_healthy():
... conn.reset()
>>>
>>> # Do stuff with conn...
"""
self._raw_instance = None
@property
def _instance(self) -> RawConnectionT:
"""Get an instance of the underlying connection, creating a new one if needed."""
if self._raw_instance is None:
self._raw_instance = self._connect(**self._kwargs)
return self._raw_instance
# Abstract fields/methods that subclasses of BaseConnection must implement
@abstractmethod
def _connect(self, **kwargs) -> RawConnectionT:
"""Create an instance of an underlying connection object.
This abstract method is the one method that we require subclasses of
BaseConnection to provide an implementation for. It is called when first
creating a connection and when reconnecting after a connection is reset.
Parameters
----------
kwargs : dict
Returns
-------
RawConnectionT
The underlying connection object.
"""
raise NotImplementedError

View File

@@ -0,0 +1,561 @@
# 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.
# NOTE: We won't always be able to import from snowflake.{connector, snowpark}.* so need
# the `type: ignore` comment below, but that comment will explode if `warn-unused-ignores`
# is turned on when the package is available. Unfortunately, mypy doesn't provide a good
# way to configure this at a per-line level :(
# mypy: no-warn-unused-ignores
from __future__ import annotations
from typing import TYPE_CHECKING, Final, cast
from streamlit import logger
from streamlit.connections import BaseConnection
from streamlit.connections.util import running_in_sis
from streamlit.errors import StreamlitAPIException
from streamlit.runtime.caching import cache_data
_LOGGER: Final = logger.get_logger(__name__)
if TYPE_CHECKING:
from datetime import timedelta
from pandas import DataFrame
from snowflake.connector.cursor import SnowflakeCursor # type:ignore[import]
from snowflake.snowpark.session import Session # type:ignore[import]
from snowflake.connector import ( # type:ignore[import] # isort: skip
SnowflakeConnection as InternalSnowflakeConnection,
)
class SnowflakeConnection(BaseConnection["InternalSnowflakeConnection"]):
"""A connection to Snowflake using the Snowflake Connector for Python.
Initialize this connection object using ``st.connection("snowflake")`` or
``st.connection("<name>", type="snowflake")``. Connection parameters for a
SnowflakeConnection can be specified using ``secrets.toml`` and/or
``**kwargs``. Connection parameters are passed to
|snowflake.connector.connect()|.
When an app is running in Streamlit in Snowflake,
``st.connection("snowflake")`` connects automatically using the app owner's
role without further configuration. ``**kwargs`` will be ignored in this
case. Use ``secrets.toml`` and ``**kwargs`` to configure your connection
for local development.
SnowflakeConnection includes several convenience methods. For example, you
can directly execute a SQL query with ``.query()`` or access the underlying
Snowflake Connector object with ``.raw_connection``.
.. |snowflake.connector.connect()| replace:: ``snowflake.connector.connect()``
.. _snowflake.connector.connect(): https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-api#label-snowflake-connector-methods-connect
.. Tip::
`snowflake-snowpark-python <https://pypi.org/project/snowflake-snowpark-python/>`_
must be installed in your environment to use this connection. You can
install Snowflake extras along with Streamlit:
>>> pip install streamlit[snowflake]
.. Important::
Account identifiers must be of the form ``<orgname>-<account_name>``
where ``<orgname>`` is the name of your Snowflake organization and
``<account_name>`` is the unique name of your account within your
organization. This is dash-separated, not dot-separated like when used
in SQL queries. For more information, see `Account identifiers
<https://docs.snowflake.com/en/user-guide/admin-account-identifier>`_.
Examples
--------
**Example 1: Configuration with Streamlit secrets**
You can configure your Snowflake connection using Streamlit's
`Secrets management <https://docs.streamlit.io/develop/concepts/connections/secrets-management>`_.
For example, if you have MFA enabled on your account, you can connect using
`key-pair authentication <https://docs.snowflake.com/en/user-guide/key-pair-auth>`_.
``.streamlit/secrets.toml``:
>>> [connections.snowflake]
>>> account = "xxx-xxx"
>>> user = "xxx"
>>> private_key_file = "/xxx/xxx/xxx.p8"
>>> role = "xxx"
>>> warehouse = "xxx"
>>> database = "xxx"
>>> schema = "xxx"
Your app code:
>>> import streamlit as st
>>> conn = st.connection("snowflake")
>>> df = conn.query("SELECT * FROM my_table")
**Example 2: Configuration with keyword arguments and external authentication**
You can configure your Snowflake connection with keyword arguments. The
keyword arguments are merged with (and take precedence over) the values in
``secrets.toml``. However, if you name your connection ``"snowflake"`` and
don't have a ``[connections.snowflake]`` dictionary in your
``secrets.toml`` file, Streamlit will ignore any keyword arguments and use
the default Snowflake connection as described in Example 5 and Example 6.
To configure your connection using only keyword arguments, declare a name
for the connection other than ``"snowflake"``.
For example, if your Snowflake account supports SSO, you can set up a quick
local connection for development using `browser-based SSO
<https://docs.snowflake.com/en/user-guide/admin-security-fed-auth-use#how-browser-based-sso-works>`_.
Because there is nothing configured in ``secrets.toml``, the name is an
empty string and the type is set to ``"snowflake"``. This prevents
Streamlit from ignoring the keyword arguments and using a default
Snowflake connection.
>>> import streamlit as st
>>> conn = st.connection(
... "",
... type="snowflake",
... account="xxx-xxx",
... user="xxx",
... authenticator="externalbrowser",
... )
>>> df = conn.query("SELECT * FROM my_table")
**Example 3: Named connection with Snowflake's connection configuration file**
Snowflake's Python Connector supports a `connection configuration file
<https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-connect#connecting-using-the-connections-toml-file>`_,
which is well integrated with Streamlit's ``SnowflakeConnection``. If you
already have one or more connections configured, all you need to do is pass
the name of the connection to use.
``~/.snowflake/connections.toml``:
>>> [my_connection]
>>> account = "xxx-xxx"
>>> user = "xxx"
>>> password = "xxx"
>>> warehouse = "xxx"
>>> database = "xxx"
>>> schema = "xxx"
Your app code:
>>> import streamlit as st
>>> conn = st.connection("my_connection", type="snowflake")
>>> df = conn.query("SELECT * FROM my_table")
**Example 4: Named connection with Streamlit secrets and Snowflake's connection configuration file**
If you have a Snowflake configuration file with a connection named
``my_connection`` as in Example 3, you can pass the connection name through
``secrets.toml``.
``.streamlit/secrets.toml``:
>>> [connections.snowflake]
>>> connection_name = "my_connection"
Your app code:
>>> import streamlit as st
>>> conn = st.connection("snowflake")
>>> df = conn.query("SELECT * FROM my_table")
**Example 5: Default connection with an environment variable**
If you don't have a ``[connections.snowflake]`` dictionary in your
``secrets.toml`` file and use ``st.connection("snowflake")``, Streamlit
will use the default connection for the `Snowflake Python Connector
<https://docs.snowflake.cn/en/developer-guide/python-connector/python-connector-connect#setting-a-default-connection>`_.
If you have a Snowflake configuration file with a connection named
``my_connection`` as in Example 3, you can set an environment variable to
declare it as the default Snowflake connection.
>>> SNOWFLAKE_DEFAULT_CONNECTION_NAME = "my_connection"
Your app code:
>>> import streamlit as st
>>> conn = st.connection("snowflake")
>>> df = conn.query("SELECT * FROM my_table")
**Example 6: Default connection in Snowflake's connection configuration file**
If you have a Snowflake configuration file that defines your ``default``
connection, Streamlit will automatically use it if no other connection is
declared.
``~/.snowflake/connections.toml``:
>>> [default]
>>> account = "xxx-xxx"
>>> user = "xxx"
>>> password = "xxx"
>>> warehouse = "xxx"
>>> database = "xxx"
>>> schema = "xxx"
Your app code:
>>> import streamlit as st
>>> conn = st.connection("snowflake")
>>> df = conn.query("SELECT * FROM my_table")
"""
def _connect(self, **kwargs) -> InternalSnowflakeConnection:
import snowflake.connector # type:ignore[import]
from snowflake.connector import Error as SnowflakeError # type:ignore[import]
# If we're running in SiS, just call get_active_session() and retrieve the
# lower-level connection from it.
if running_in_sis():
from snowflake.snowpark.context import ( # type:ignore[import] # isort: skip
get_active_session,
)
session = get_active_session()
if hasattr(session, "connection"):
return session.connection
# session.connection is only a valid attr in more recent versions of
# snowflake-connector-python, so we fall back to grabbing
# session._conn._conn if `.connection` is unavailable.
return session._conn._conn
# We require qmark-style parameters everywhere for consistency across different
# environments where SnowflakeConnections may be used.
snowflake.connector.paramstyle = "qmark"
# Otherwise, attempt to create a new connection from whatever credentials we
# have available.
try:
st_secrets = self._secrets.to_dict()
if len(st_secrets):
_LOGGER.info(
"Connect to Snowflake using the Streamlit secret defined under "
"[connections.snowflake]."
)
conn_kwargs = {**st_secrets, **kwargs}
return snowflake.connector.connect(**conn_kwargs)
# Use the default configuration as defined in https://docs.snowflake.cn/en/developer-guide/python-connector/python-connector-connect#setting-a-default-connection
if self._connection_name == "snowflake":
_LOGGER.info(
"Connect to Snowflake using the default configuration as defined "
"in https://docs.snowflake.cn/en/developer-guide/python-connector/python-connector-connect#setting-a-default-connection"
)
return snowflake.connector.connect()
return snowflake.connector.connect(**kwargs)
except SnowflakeError as e:
if not len(st_secrets) and not kwargs:
raise StreamlitAPIException(
"Missing Snowflake connection configuration. "
"Did you forget to set this in `secrets.toml`, a Snowflake configuration file, "
"or as kwargs to `st.connection`? "
"See the [SnowflakeConnection configuration documentation](https://docs.streamlit.io/st.connections.snowflakeconnection-configuration) "
"for more details and examples."
)
raise e
def query(
self,
sql: str,
*, # keyword-only arguments:
ttl: float | int | timedelta | None = None,
show_spinner: bool | str = "Running `snowflake.query(...)`.",
params=None,
**kwargs,
) -> DataFrame:
"""Run a read-only SQL query.
This method implements query result caching and simple error
handling/retries. The caching behavior is identical to that of using
``@st.cache_data``.
.. note::
Queries that are run without a specified ``ttl`` are cached
indefinitely.
Parameters
----------
sql : str
The read-only SQL query to execute.
ttl : float, int, timedelta or None
The maximum number of seconds to keep results in the cache. If this
is ``None`` (default), cached results do not expire with time.
show_spinner : boolean or string
Whether to enable the spinner. When a cached query is executed, no
spinner is displayed because the result is immediately available.
When a new query is executed, the default is to show a spinner with
the message "Running ``snowflake.query(...)``."
If this is ``False``, no spinner displays while executing the
query. If this is a string, the string will be used as the message
for the spinner.
params : list, tuple, dict or None
List of parameters to pass to the Snowflake Connector for Python
``Cursor.execute()`` method. This connector supports binding data
to a SQL statement using qmark bindings. For more information and
examples, see the `Snowflake Connector for Python documentation
<https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-example#using-qmark-or-numeric-binding>`_.
This defaults to ``None``.
Returns
-------
pandas.DataFrame
The result of running the query, formatted as a pandas DataFrame.
Example
-------
>>> import streamlit as st
>>>
>>> conn = st.connection("snowflake")
>>> df = conn.query("SELECT * FROM my_table")
>>> st.dataframe(df)
"""
from tenacity import retry, retry_if_exception, stop_after_attempt, wait_fixed
# the ANSI-compliant SQL code for "connection was not established" (see docs: https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-api#id6)
SQLSTATE_CONNECTION_WAS_NOT_ESTABLISHED = "08001"
@retry(
after=lambda _: self.reset(),
stop=stop_after_attempt(3),
reraise=True,
# We don't have to implement retries ourself for most error types as the
# `snowflake-connector-python` library already implements retries for
# retryable HTTP errors.
retry=retry_if_exception(
lambda e: hasattr(e, "sqlstate")
and e.sqlstate == SQLSTATE_CONNECTION_WAS_NOT_ESTABLISHED
),
wait=wait_fixed(1),
)
def _query(sql: str) -> DataFrame:
cur = self._instance.cursor()
cur.execute(sql, params=params, **kwargs)
return cur.fetch_pandas_all()
# We modify our helper function's `__qualname__` here to work around default
# `@st.cache_data` behavior. Otherwise, `.query()` being called with different
# `ttl` values will reset the cache with each call, and the query caches won't
# be scoped by connection.
ttl_str = str( # Avoid adding extra `.` characters to `__qualname__`
ttl
).replace(".", "_")
_query.__qualname__ = f"{_query.__qualname__}_{self._connection_name}_{ttl_str}"
_query = cache_data(
show_spinner=show_spinner,
ttl=ttl,
)(_query)
return _query(sql)
def write_pandas(
self,
df: DataFrame,
table_name: str,
database: str | None = None,
schema: str | None = None,
chunk_size: int | None = None,
**kwargs,
) -> tuple[bool, int, int]:
"""Write a ``pandas.DataFrame`` to a table in a Snowflake database.
This convenience method is a thin wrapper around
``snowflake.connector.pandas_tools.write_pandas()`` using the
underlying connection. The ``conn`` parameter is passed automatically.
For more information and additional keyword arguments, see the
`Snowflake Connector for Python documentation
<https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-api#write_pandas>`_.
Parameters
----------
df: pandas.DataFrame
The ``pandas.DataFrame`` object containing the data to be copied
into the table.
table_name: str
Name of the table where the data should be copied to.
database: str
Name of the database containing the table. By default, the function
writes to the database that is currently in use in the session.
.. Note::
If you specify this parameter, you must also specify the schema
parameter.
schema: str
Name of the schema containing the table. By default, the function
writes to the table in the schema that is currently in use in the
session.
chunk_size: int
Number of elements to insert at a time. By default, the function
inserts all elements in one chunk.
**kwargs: Any
Additional keyword arguments for
``snowflake.connector.pandas_tools.write_pandas()``.
Returns
-------
tuple[bool, int, int]
A tuple containing three values:
1. A boolean value that is ``True`` if the write was successful.
2. An integer giving the number of chunks of data that were copied.
3. An integer giving the number of rows that were inserted.
Example
-------
The following example uses the database and schema currently in use in
the session and copies the data into a table named "my_table."
>>> import streamlit as st
>>> import pandas as pd
>>>
>>> df = pd.DataFrame(
... {"Name": ["Mary", "John", "Robert"], "Pet": ["dog", "cat", "bird"]}
... )
>>> conn = st.connection("snowflake")
>>> conn.write_pandas(df, "my_table")
"""
from snowflake.connector.pandas_tools import write_pandas # type:ignore[import]
success, nchunks, nrows, _ = write_pandas(
conn=self._instance,
df=df,
table_name=table_name,
database=database,
schema=schema,
chunk_size=chunk_size,
**kwargs,
)
return (success, nchunks, nrows)
def cursor(self) -> SnowflakeCursor:
"""Create a new cursor object from this connection.
Snowflake Connector cursors implement the Python Database API v2.0
specification (PEP-249). For more information, see the
`Snowflake Connector for Python documentation
<https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-api#object-cursor>`_.
Returns
-------
snowflake.connector.cursor.SnowflakeCursor
A cursor object for the connection.
Example
-------
The following example uses a cursor to insert multiple rows into a
table. The ``qmark`` parameter style is specified as an optional
keyword argument. Alternatively, the parameter style can be declared in
your connection configuration file. For more information, see the
`Snowflake Connector for Python documentation
<https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-example#using-qmark-or-numeric-binding>`_.
>>> import streamlit as st
>>>
>>> conn = st.connection("snowflake", "paramstyle"="qmark")
>>> rows_to_insert = [("Mary", "dog"), ("John", "cat"), ("Robert", "bird")]
>>> conn.cursor().executemany(
... "INSERT INTO mytable (name, pet) VALUES (?, ?)", rows_to_insert
... )
"""
return self._instance.cursor()
@property
def raw_connection(self) -> InternalSnowflakeConnection:
"""Access the underlying connection object from the Snowflake\
Connector for Python.
For information on how to use the Snowflake Connector for Python, see
the `Snowflake Connector for Python documentation
<https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-example>`_.
Returns
-------
snowflake.connector.connection.SnowflakeConnection
The connection object.
Example
-------
The following example uses a cursor to submit an asynchronous query,
saves the query ID, then periodically checks the query status through
the connection before retrieving the results.
>>> import streamlit as st
>>> import time
>>>
>>> conn = st.connection("snowflake")
>>> cur = conn.cursor()
>>> cur.execute_async("SELECT * FROM my_table")
>>> query_id = cur.sfqid
>>> while True:
... status = conn.raw_connection.get_query_status(query_id)
... if conn.raw_connection.is_still_running(status):
... time.sleep(1)
... else:
... break
>>> cur.get_results_from_sfqid(query_id)
>>> df = cur.fetchall()
"""
return self._instance
def session(self) -> Session:
"""Create a new Snowpark session from this connection.
For information on how to use Snowpark sessions, see the
`Snowpark developer guide
<https://docs.snowflake.com/en/developer-guide/snowpark/python/working-with-dataframes>`_
and `Snowpark API Reference
<https://docs.snowflake.com/en/developer-guide/snowpark/reference/python/latest/snowpark/session>`_.
Returns
-------
snowflake.snowpark.Session
A new Snowpark session for this connection.
Example
-------
The following example creates a new Snowpark session and uses it to run
a query.
>>> import streamlit as st
>>>
>>> conn = st.connection("snowflake")
>>> session = conn.session()
>>> df = session.sql("SELECT * FROM my_table").collect()
"""
from snowflake.snowpark.context import get_active_session # type:ignore[import]
from snowflake.snowpark.session import Session # type:ignore[import]
if running_in_sis():
return get_active_session()
return cast(
"Session", Session.builder.configs({"connection": self._instance}).create()
)

View File

@@ -0,0 +1,213 @@
# 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.
# NOTE: We won't always be able to import from snowflake.snowpark.session so need the
# `type: ignore` comment below, but that comment will explode if `warn-unused-ignores` is
# turned on when the package is available. Unfortunately, mypy doesn't provide a good
# way to configure this at a per-line level :(
# mypy: no-warn-unused-ignores
from __future__ import annotations
import threading
from collections import ChainMap
from contextlib import contextmanager
from typing import TYPE_CHECKING, cast
from streamlit.connections import BaseConnection
from streamlit.connections.util import (
SNOWSQL_CONNECTION_FILE,
load_from_snowsql_config_file,
running_in_sis,
)
from streamlit.errors import StreamlitAPIException
from streamlit.runtime.caching import cache_data
if TYPE_CHECKING:
from collections.abc import Iterator
from datetime import timedelta
from pandas import DataFrame
from snowflake.snowpark.session import Session # type:ignore[import]
_REQUIRED_CONNECTION_PARAMS = {"account"}
class SnowparkConnection(BaseConnection["Session"]):
"""A connection to Snowpark using snowflake.snowpark.session.Session. Initialize using
``st.connection("<name>", type="snowpark")``.
In addition to providing access to the Snowpark Session, SnowparkConnection supports
direct SQL querying using ``query("...")`` and thread safe access using
``with conn.safe_session():``. See methods below for more information.
SnowparkConnections should always be created using ``st.connection()``, **not**
initialized directly.
.. note::
We don't expect this iteration of SnowparkConnection to be able to scale
well in apps with many concurrent users due to the lock contention that will occur
over the single underlying Session object under high load.
"""
def __init__(self, connection_name: str, **kwargs) -> None:
self._lock = threading.RLock()
super().__init__(connection_name, **kwargs)
def _connect(self, **kwargs) -> Session:
from snowflake.snowpark.context import get_active_session # type:ignore[import]
from snowflake.snowpark.session import Session
# If we're running in SiS, just call get_active_session(). Otherwise, attempt to
# create a new session from whatever credentials we have available.
if running_in_sis():
return get_active_session()
conn_params = ChainMap(
kwargs,
self._secrets.to_dict(),
load_from_snowsql_config_file(self._connection_name),
)
if not len(conn_params):
raise StreamlitAPIException(
"Missing Snowpark connection configuration. "
f"Did you forget to set this in `secrets.toml`, `{SNOWSQL_CONNECTION_FILE}`, "
"or as kwargs to `st.connection`?"
)
for p in _REQUIRED_CONNECTION_PARAMS:
if p not in conn_params:
raise StreamlitAPIException(f"Missing Snowpark connection param: {p}")
return cast("Session", Session.builder.configs(conn_params).create())
def query(
self,
sql: str,
ttl: float | int | timedelta | None = None,
) -> DataFrame:
"""Run a read-only SQL query.
This method implements both query result caching (with caching behavior
identical to that of using ``@st.cache_data``) as well as simple error handling/retries.
.. note::
Queries that are run without a specified ttl are cached indefinitely.
Parameters
----------
sql : str
The read-only SQL query to execute.
ttl : float, int, timedelta or None
The maximum number of seconds to keep results in the cache, or
None if cached results should not expire. The default is None.
Returns
-------
pandas.DataFrame
The result of running the query, formatted as a pandas DataFrame.
Example
-------
>>> import streamlit as st
>>>
>>> conn = st.connection("snowpark")
>>> df = conn.query("SELECT * FROM pet_owners")
>>> st.dataframe(df)
"""
from snowflake.snowpark.exceptions import ( # type:ignore[import]
SnowparkServerException,
)
from tenacity import (
retry,
retry_if_exception_type,
stop_after_attempt,
wait_fixed,
)
@retry(
after=lambda _: self.reset(),
stop=stop_after_attempt(3),
reraise=True,
retry=retry_if_exception_type(SnowparkServerException),
wait=wait_fixed(1),
)
def _query(sql: str) -> DataFrame:
with self._lock:
return self._instance.sql(sql).to_pandas()
# We modify our helper function's `__qualname__` here to work around default
# `@st.cache_data` behavior. Otherwise, `.query()` being called with different
# `ttl` values will reset the cache with each call, and the query caches won't
# be scoped by connection.
ttl_str = str( # Avoid adding extra `.` characters to `__qualname__`
ttl
).replace(".", "_")
_query.__qualname__ = f"{_query.__qualname__}_{self._connection_name}_{ttl_str}"
_query = cache_data(
show_spinner="Running `snowpark.query(...)`.",
ttl=ttl,
)(_query)
return _query(sql)
@property
def session(self) -> Session:
"""Access the underlying Snowpark session.
.. note::
Snowpark sessions are **not** thread safe. Users of this method are
responsible for ensuring that access to the session returned by this method is
done in a thread-safe manner. For most users, we recommend using the thread-safe
safe_session() method and a ``with`` block.
Information on how to use Snowpark sessions can be found in the `Snowpark documentation
<https://docs.snowflake.com/en/developer-guide/snowpark/python/working-with-dataframes>`_.
Example
-------
>>> import streamlit as st
>>>
>>> session = st.connection("snowpark").session
>>> df = session.table("mytable").limit(10).to_pandas()
>>> st.dataframe(df)
"""
return self._instance
@contextmanager
def safe_session(self) -> Iterator[Session]:
"""Grab the underlying Snowpark session in a thread-safe manner.
As operations on a Snowpark session are not thread safe, we need to take care
when using a session in the context of a Streamlit app where each script run
occurs in its own thread. Using the contextmanager pattern to do this ensures
that access on this connection's underlying Session is done in a thread-safe
manner.
Information on how to use Snowpark sessions can be found in the `Snowpark documentation
<https://docs.snowflake.com/en/developer-guide/snowpark/python/working-with-dataframes>`_.
Example
-------
>>> import streamlit as st
>>>
>>> conn = st.connection("snowpark")
>>> with conn.safe_session() as session:
... df = session.table("mytable").limit(10).to_pandas()
>>>
>>> st.dataframe(df)
"""
with self._lock:
yield self.session

View File

@@ -0,0 +1,424 @@
# 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.
# NOTE: We ignore all mypy import-not-found errors as top-level since
# this module is optional and the SQLAlchemy dependency is not installed
# by default.
# mypy: disable-error-code="import-not-found, redundant-cast"
from __future__ import annotations
from collections import ChainMap
from copy import deepcopy
from typing import TYPE_CHECKING, cast
from streamlit.connections import BaseConnection
from streamlit.connections.util import extract_from_dict
from streamlit.errors import StreamlitAPIException
from streamlit.runtime.caching import cache_data
if TYPE_CHECKING:
from datetime import timedelta
from pandas import DataFrame
from sqlalchemy.engine import Connection as SQLAlchemyConnection
from sqlalchemy.engine.base import Engine
from sqlalchemy.orm import Session
_ALL_CONNECTION_PARAMS = {
"url",
"driver",
"dialect",
"username",
"password",
"host",
"port",
"database",
"query",
}
_REQUIRED_CONNECTION_PARAMS = {"dialect", "username", "host"}
class SQLConnection(BaseConnection["Engine"]):
"""A connection to a SQL database using a SQLAlchemy Engine.
Initialize this connection object using ``st.connection("sql")`` or
``st.connection("<name>", type="sql")``. Connection parameters for a
SQLConnection can be specified using ``secrets.toml`` and/or ``**kwargs``.
Possible connection parameters include:
- ``url`` or keyword arguments for |sqlalchemy.engine.URL.create()|_, except
``drivername``. Use ``dialect`` and ``driver`` instead of ``drivername``.
- Keyword arguments for |sqlalchemy.create_engine()|_, including custom
``connect()`` arguments used by your specific ``dialect`` or ``driver``.
- ``autocommit``. If this is ``False`` (default), the connection operates
in manual commit (transactional) mode. If this is ``True``, the
connection operates in autocommit (non-transactional) mode.
If ``url`` exists as a connection parameter, Streamlit will pass it to
``sqlalchemy.engine.make_url()``. Otherwise, Streamlit requires (at a
minimum) ``dialect``, ``username``, and ``host``. Streamlit will use
``dialect`` and ``driver`` (if defined) to derive ``drivername``, then pass
the relevant connection parameters to ``sqlalchemy.engine.URL.create()``.
In addition to the default keyword arguments for ``sqlalchemy.create_engine()``,
your dialect may accept additional keyword arguments. For example, if you
use ``dialect="snowflake"`` with `Snowflake SQLAlchemy
<https://github.com/snowflakedb/snowflake-sqlalchemy#key-pair-authentication-support>`_,
you can pass a value for ``private_key`` to use key-pair authentication. If
you use ``dialect="bigquery"`` with `Google BigQuery
<https://github.com/googleapis/python-bigquery-sqlalchemy#authentication>`_,
you can pass a value for ``location``.
SQLConnection provides the ``.query()`` convenience method, which can be
used to run simple, read-only queries with both caching and simple error
handling/retries. More complex database interactions can be performed by
using the ``.session`` property to receive a regular SQLAlchemy Session.
.. Important::
`SQLAlchemy <https://pypi.org/project/SQLAlchemy/>`_ must be installed
in your environment to use this connection. You must also install your
driver, such as ``pyodbc`` or ``psycopg2``.
.. |sqlalchemy.engine.URL.create()| replace:: ``sqlalchemy.engine.URL.create()``
.. _sqlalchemy.engine.URL.create(): https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.engine.URL.create
.. |sqlalchemy.engine.make_url()| replace:: ``sqlalchemy.engine.make_url()``
.. _sqlalchemy.engine.make_url(): https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.engine.make_url
.. |sqlalchemy.create_engine()| replace:: ``sqlalchemy.create_engine()``
.. _sqlalchemy.create_engine(): https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine
Examples
--------
**Example 1: Configuration with URL**
You can configure your SQL connection using Streamlit's
`Secrets management <https://docs.streamlit.io/develop/concepts/connections/secrets-management>`_.
The following example specifies a SQL connection URL.
``.streamlit/secrets.toml``:
>>> [connections.sql]
>>> url = "xxx+xxx://xxx:xxx@xxx:xxx/xxx"
Your app code:
>>> import streamlit as st
>>>
>>> conn = st.connection("sql")
>>> df = conn.query("SELECT * FROM pet_owners")
>>> st.dataframe(df)
**Example 2: Configuration with dialect, host, and username**
If you do not specify ``url``, you must at least specify ``dialect``,
``host``, and ``username`` instead. The following example also includes
``password``.
``.streamlit/secrets.toml``:
>>> [connections.sql]
>>> dialect = "xxx"
>>> host = "xxx"
>>> username = "xxx"
>>> password = "xxx"
Your app code:
>>> import streamlit as st
>>>
>>> conn = st.connection("sql")
>>> df = conn.query("SELECT * FROM pet_owners")
>>> st.dataframe(df)
**Example 3: Configuration with keyword arguments**
You can configure your SQL connection with keyword arguments (with or
without ``secrets.toml``). For example, if you use Microsoft Entra ID with
a Microsoft Azure SQL server, you can quickly set up a local connection for
development using `interactive authentication
<https://learn.microsoft.com/en-us/sql/connect/odbc/using-azure-active-directory?view=sql-server-ver16#new-andor-modified-dsn-and-connection-string-keywords>`_.
This example requires the `Microsoft ODBC Driver for SQL Server
<https://learn.microsoft.com/en-us/sql/connect/odbc/microsoft-odbc-driver-for-sql-server?view=sql-server-ver16>`_
for *Windows* in addition to the ``sqlalchemy`` and ``pyodbc`` packages for
Python.
>>> import streamlit as st
>>>
>>> conn = st.connection(
... "sql",
... dialect="mssql",
... driver="pyodbc",
... host="xxx.database.windows.net",
... database="xxx",
... username="xxx",
... query={
... "driver": "ODBC Driver 18 for SQL Server",
... "authentication": "ActiveDirectoryInteractive",
... "encrypt": "yes",
... },
... )
>>>
>>> df = conn.query("SELECT * FROM pet_owners")
>>> st.dataframe(df)
"""
def _connect(self, autocommit: bool = False, **kwargs) -> Engine:
import sqlalchemy
kwargs = deepcopy(kwargs)
conn_param_kwargs = extract_from_dict(_ALL_CONNECTION_PARAMS, kwargs)
conn_params = ChainMap(conn_param_kwargs, self._secrets.to_dict())
if not len(conn_params):
raise StreamlitAPIException(
"Missing SQL DB connection configuration. "
"Did you forget to set this in `secrets.toml` or as kwargs to `st.connection`?"
)
if "url" in conn_params:
url = sqlalchemy.engine.make_url(conn_params["url"])
else:
for p in _REQUIRED_CONNECTION_PARAMS:
if p not in conn_params:
raise StreamlitAPIException(f"Missing SQL DB connection param: {p}")
drivername = conn_params["dialect"] + (
f"+{conn_params['driver']}" if "driver" in conn_params else ""
)
url = sqlalchemy.engine.URL.create(
drivername=drivername,
username=conn_params["username"],
password=conn_params.get("password"),
host=conn_params["host"],
port=int(conn_params["port"]) if "port" in conn_params else None,
database=conn_params.get("database"),
query=conn_params["query"] if "query" in conn_params else None,
)
create_engine_kwargs = ChainMap(
kwargs, self._secrets.get("create_engine_kwargs", {})
)
eng = sqlalchemy.create_engine(url, **create_engine_kwargs)
if autocommit:
return cast("Engine", eng.execution_options(isolation_level="AUTOCOMMIT"))
else:
return cast("Engine", eng)
def query(
self,
sql: str,
*, # keyword-only arguments:
show_spinner: bool | str = "Running `sql.query(...)`.",
ttl: float | int | timedelta | None = None,
index_col: str | list[str] | None = None,
chunksize: int | None = None,
params=None,
**kwargs,
) -> DataFrame:
"""Run a read-only query.
This method implements query result caching and simple error
handling/retries. The caching behavior is identical to that of using
``@st.cache_data``.
.. note::
Queries that are run without a specified ttl are cached indefinitely.
All keyword arguments passed to this function are passed down to
|pandas.read_sql|_, except ``ttl``.
.. |pandas.read_sql| replace:: ``pandas.read_sql``
.. _pandas.read_sql: https://pandas.pydata.org/docs/reference/api/pandas.read_sql.html
Parameters
----------
sql : str
The read-only SQL query to execute.
show_spinner : boolean or string
Enable the spinner. The default is to show a spinner when there is a
"cache miss" and the cached resource is being created. If a string, the value
of the show_spinner param will be used for the spinner text.
ttl : float, int, timedelta or None
The maximum number of seconds to keep results in the cache, or
None if cached results should not expire. The default is None.
index_col : str, list of str, or None
Column(s) to set as index(MultiIndex). Default is None.
chunksize : int or None
If specified, return an iterator where chunksize is the number of
rows to include in each chunk. Default is None.
params : list, tuple, dict or None
List of parameters to pass to the execute method. The syntax used to pass
parameters is database driver dependent. Check your database driver
documentation for which of the five syntax styles, described in `PEP 249
paramstyle <https://peps.python.org/pep-0249/#paramstyle>`_, is supported.
Default is None.
**kwargs: dict
Additional keyword arguments are passed to |pandas.read_sql|_.
.. |pandas.read_sql| replace:: ``pandas.read_sql``
.. _pandas.read_sql: https://pandas.pydata.org/docs/reference/api/pandas.read_sql.html
Returns
-------
pandas.DataFrame
The result of running the query, formatted as a pandas DataFrame.
Example
-------
>>> import streamlit as st
>>>
>>> conn = st.connection("sql")
>>> df = conn.query(
... "SELECT * FROM pet_owners WHERE owner = :owner",
... ttl=3600,
... params={"owner": "barbara"},
... )
>>> st.dataframe(df)
"""
from sqlalchemy import text
from sqlalchemy.exc import DatabaseError, InternalError, OperationalError
from tenacity import (
retry,
retry_if_exception_type,
stop_after_attempt,
wait_fixed,
)
@retry(
after=lambda _: self.reset(),
stop=stop_after_attempt(3),
reraise=True,
retry=retry_if_exception_type(
(DatabaseError, InternalError, OperationalError)
),
wait=wait_fixed(1),
)
def _query(
sql: str,
index_col=None,
chunksize=None,
params=None,
**kwargs,
) -> DataFrame:
import pandas as pd
instance = self._instance.connect()
return pd.read_sql(
text(sql),
instance,
index_col=index_col,
chunksize=chunksize,
params=params,
**kwargs,
)
# We modify our helper function's `__qualname__` here to work around default
# `@st.cache_data` behavior. Otherwise, `.query()` being called with different
# `ttl` values will reset the cache with each call, and the query caches won't
# be scoped by connection.
ttl_str = str( # Avoid adding extra `.` characters to `__qualname__`
ttl
).replace(".", "_")
_query.__qualname__ = f"{_query.__qualname__}_{self._connection_name}_{ttl_str}"
_query = cache_data(
show_spinner=show_spinner,
ttl=ttl,
)(_query)
return _query(
sql,
index_col=index_col,
chunksize=chunksize,
params=params,
**kwargs,
)
def connect(self) -> SQLAlchemyConnection:
"""Call ``.connect()`` on the underlying SQLAlchemy Engine, returning a new\
connection object.
Calling this method is equivalent to calling ``self._instance.connect()``.
NOTE: This method should not be confused with the internal ``_connect`` method used
to implement a Streamlit Connection.
Returns
-------
sqlalchemy.engine.Connection
A new SQLAlchemy connection object.
"""
return self._instance.connect()
@property
def engine(self) -> Engine:
"""The underlying SQLAlchemy Engine.
This is equivalent to accessing ``self._instance``.
Returns
-------
sqlalchemy.engine.base.Engine
The underlying SQLAlchemy Engine.
"""
return self._instance
@property
def driver(self) -> str:
"""The name of the driver used by the underlying SQLAlchemy Engine.
This is equivalent to accessing ``self._instance.driver``.
Returns
-------
str
The name of the driver. For example, ``"pyodbc"`` or ``"psycopg2"``.
"""
return cast("str", self._instance.driver)
@property
def session(self) -> Session:
"""Return a SQLAlchemy Session.
Users of this connection should use the contextmanager pattern for writes,
transactions, and anything more complex than simple read queries.
See the usage example below, which assumes we have a table ``numbers`` with a
single integer column ``val``. The `SQLAlchemy
<https://docs.sqlalchemy.org/en/20/orm/session_basics.html>`_ docs also contain
much more information on the usage of sessions.
Returns
-------
sqlalchemy.orm.Session
A SQLAlchemy Session.
Example
-------
>>> import streamlit as st
>>> conn = st.connection("sql")
>>> n = st.slider("Pick a number")
>>> if st.button("Add the number!"):
... with conn.session as session:
... session.execute("INSERT INTO numbers (val) VALUES (:n);", {"n": n})
... session.commit()
"""
from sqlalchemy.orm import Session
return Session(self._instance)

View File

@@ -0,0 +1,97 @@
# 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.
# NOTE: We won't always be able to import from snowflake.connector.connection so need the
# `type: ignore` comment below, but that comment will explode if `warn-unused-ignores` is
# turned on when the package is available. Unfortunately, mypy doesn't provide a good
# way to configure this at a per-line level :(
# mypy: no-warn-unused-ignores
from __future__ import annotations
import os
from typing import TYPE_CHECKING, Any, cast
if TYPE_CHECKING:
from collections.abc import Collection
SNOWSQL_CONNECTION_FILE = "~/.snowsql/config"
def extract_from_dict(
keys: Collection[str], source_dict: dict[str, Any]
) -> dict[str, Any]:
"""Extract the specified keys from source_dict and return them in a new dict.
Parameters
----------
keys : Collection[str]
The keys to extract from source_dict.
source_dict : Dict[str, Any]
The dict to extract keys from. Note that this function mutates source_dict.
Returns
-------
Dict[str, Any]
A new dict containing the keys/values extracted from source_dict.
"""
d = {}
for k in keys:
if k in source_dict:
d[k] = source_dict.pop(k)
return d
def load_from_snowsql_config_file(connection_name: str) -> dict[str, Any]:
"""Loads the dictionary from snowsql config file."""
snowsql_config_file = os.path.expanduser(SNOWSQL_CONNECTION_FILE)
if not os.path.exists(snowsql_config_file):
return {}
# Lazy-load config parser for better import / startup performance
import configparser
config = configparser.ConfigParser(inline_comment_prefixes="#")
config.read(snowsql_config_file)
if f"connections.{connection_name}" in config:
raw_conn_params = config[f"connections.{connection_name}"]
elif "connections" in config:
raw_conn_params = config["connections"]
else:
return {}
conn_params = {
k.replace("name", ""): v.strip('"') for k, v in raw_conn_params.items()
}
if "db" in conn_params:
conn_params["database"] = conn_params["db"]
del conn_params["db"]
return conn_params
def running_in_sis() -> bool:
"""Return whether this app is running in SiS."""
try:
from snowflake.snowpark._internal.utils import ( # type: ignore[import] # isort: skip
is_in_stored_procedure,
)
return cast("bool", is_in_stored_procedure())
except ModuleNotFoundError:
return False

View File

@@ -0,0 +1,210 @@
# 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 Any
from streamlit import util
from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
def make_delta_path(
root_container: int, parent_path: tuple[int, ...], index: int
) -> list[int]:
delta_path = [root_container]
delta_path.extend(parent_path)
delta_path.append(index)
return delta_path
def get_container_cursor(
root_container: int | None,
) -> RunningCursor | None:
"""Return the top-level RunningCursor for the given container.
This is the cursor that is used when user code calls something like
`st.foo` (which uses the main container) or `st.sidebar.foo` (which uses
the sidebar container).
"""
if root_container is None:
return None
ctx = get_script_run_ctx()
if ctx is None:
return None
if root_container in ctx.cursors:
return ctx.cursors[root_container]
cursor = RunningCursor(root_container=root_container)
ctx.cursors[root_container] = cursor
return cursor
class Cursor:
"""A pointer to a delta location in the app.
When adding an element to the app, you should always call
get_locked_cursor() on that element's respective Cursor.
"""
def __repr__(self) -> str:
return util.repr_(self)
@property
def root_container(self) -> int:
"""The top-level container this cursor lives within - either
RootContainer.MAIN or RootContainer.SIDEBAR.
"""
raise NotImplementedError()
@property
def parent_path(self) -> tuple[int, ...]:
"""The cursor's parent's path within its container."""
raise NotImplementedError()
@property
def index(self) -> int:
"""The index of the Delta within its parent block."""
raise NotImplementedError()
@property
def delta_path(self) -> list[int]:
"""The complete path of the delta pointed to by this cursor - its
container, parent path, and index.
"""
return make_delta_path(self.root_container, self.parent_path, self.index)
@property
def is_locked(self) -> bool:
raise NotImplementedError()
def get_locked_cursor(self, **props) -> LockedCursor:
raise NotImplementedError()
@property
def props(self) -> Any:
"""Other data in this cursor. This is a temporary measure that will go
away when we implement improved return values for elements.
This is only implemented in LockedCursor.
"""
raise NotImplementedError()
class RunningCursor(Cursor):
def __init__(self, root_container: int, parent_path: tuple[int, ...] = ()):
"""A moving pointer to a delta location in the app.
RunningCursors auto-increment to the next available location when you
call get_locked_cursor() on them.
Parameters
----------
root_container: int
The root container this cursor lives in.
parent_path: tuple of ints
The full path of this cursor, consisting of the IDs of all ancestors.
The 0th item is the topmost ancestor.
"""
self._root_container = root_container
self._parent_path = parent_path
self._index = 0
@property
def root_container(self) -> int:
return self._root_container
@property
def parent_path(self) -> tuple[int, ...]:
return self._parent_path
@property
def index(self) -> int:
return self._index
@property
def is_locked(self) -> bool:
return False
def get_locked_cursor(self, **props) -> LockedCursor:
locked_cursor = LockedCursor(
root_container=self._root_container,
parent_path=self._parent_path,
index=self._index,
**props,
)
self._index += 1
return locked_cursor
class LockedCursor(Cursor):
def __init__(
self,
root_container: int,
parent_path: tuple[int, ...] = (),
index: int = 0,
**props,
):
"""A locked pointer to a location in the app.
LockedCursors always point to the same location, even when you call
get_locked_cursor() on them.
Parameters
----------
root_container: int
The root container this cursor lives in.
parent_path: tuple of ints
The full path of this cursor, consisting of the IDs of all ancestors. The
0th item is the topmost ancestor.
index: int
**props: any
Anything else you want to store in this cursor. This is a temporary
measure that will go away when we implement improved return values
for elements.
"""
self._root_container = root_container
self._index = index
self._parent_path = parent_path
self._props = props
@property
def root_container(self) -> int:
return self._root_container
@property
def parent_path(self) -> tuple[int, ...]:
return self._parent_path
@property
def index(self) -> int:
return self._index
@property
def is_locked(self) -> bool:
return True
def get_locked_cursor(self, **props) -> LockedCursor:
self._props = props
return self
@property
def props(self) -> Any:
return self._props

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,602 @@
# 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 sys
from collections.abc import Iterable
from copy import deepcopy
from typing import (
TYPE_CHECKING,
Any,
Callable,
Final,
Literal,
NoReturn,
TypeVar,
cast,
)
from typing_extensions import TypeAlias
from streamlit import (
cli_util,
config,
cursor,
env_util,
logger,
runtime,
util,
)
from streamlit.delta_generator_singletons import (
context_dg_stack,
get_last_dg_added_to_context_stack,
)
from streamlit.elements.alert import AlertMixin
from streamlit.elements.arrow import ArrowMixin
from streamlit.elements.balloons import BalloonsMixin
from streamlit.elements.bokeh_chart import BokehMixin
from streamlit.elements.code import CodeMixin
from streamlit.elements.deck_gl_json_chart import PydeckMixin
from streamlit.elements.doc_string import HelpMixin
from streamlit.elements.empty import EmptyMixin
from streamlit.elements.exception import ExceptionMixin
from streamlit.elements.form import FormMixin
from streamlit.elements.graphviz_chart import GraphvizMixin
from streamlit.elements.heading import HeadingMixin
from streamlit.elements.html import HtmlMixin
from streamlit.elements.iframe import IframeMixin
from streamlit.elements.image import ImageMixin
from streamlit.elements.json import JsonMixin
from streamlit.elements.layouts import LayoutsMixin
from streamlit.elements.lib.form_utils import FormData, current_form_id
from streamlit.elements.map import MapMixin
from streamlit.elements.markdown import MarkdownMixin
from streamlit.elements.media import MediaMixin
from streamlit.elements.metric import MetricMixin
from streamlit.elements.plotly_chart import PlotlyMixin
from streamlit.elements.progress import ProgressMixin
from streamlit.elements.pyplot import PyplotMixin
from streamlit.elements.snow import SnowMixin
from streamlit.elements.text import TextMixin
from streamlit.elements.toast import ToastMixin
from streamlit.elements.vega_charts import VegaChartsMixin
from streamlit.elements.widgets.audio_input import AudioInputMixin
from streamlit.elements.widgets.button import ButtonMixin
from streamlit.elements.widgets.button_group import ButtonGroupMixin
from streamlit.elements.widgets.camera_input import CameraInputMixin
from streamlit.elements.widgets.chat import ChatMixin
from streamlit.elements.widgets.checkbox import CheckboxMixin
from streamlit.elements.widgets.color_picker import ColorPickerMixin
from streamlit.elements.widgets.data_editor import DataEditorMixin
from streamlit.elements.widgets.file_uploader import FileUploaderMixin
from streamlit.elements.widgets.multiselect import MultiSelectMixin
from streamlit.elements.widgets.number_input import NumberInputMixin
from streamlit.elements.widgets.radio import RadioMixin
from streamlit.elements.widgets.select_slider import SelectSliderMixin
from streamlit.elements.widgets.selectbox import SelectboxMixin
from streamlit.elements.widgets.slider import SliderMixin
from streamlit.elements.widgets.text_widgets import TextWidgetsMixin
from streamlit.elements.widgets.time_widgets import TimeWidgetsMixin
from streamlit.elements.write import WriteMixin
from streamlit.errors import StreamlitAPIException
from streamlit.proto import Block_pb2, ForwardMsg_pb2
from streamlit.proto.RootContainer_pb2 import RootContainer
from streamlit.runtime import caching
from streamlit.runtime.scriptrunner import enqueue_message as _enqueue_message
from streamlit.runtime.scriptrunner import get_script_run_ctx
if TYPE_CHECKING:
from google.protobuf.message import Message
from streamlit.cursor import Cursor
from streamlit.elements.lib.built_in_chart_utils import AddRowsMetadata
MAX_DELTA_BYTES: Final[int] = 14 * 1024 * 1024 # 14MB
Value = TypeVar("Value")
# Type aliases for Ancestor Block Types
BlockType: TypeAlias = str
AncestorBlockTypes: TypeAlias = Iterable[BlockType]
_use_warning_has_been_displayed: bool = False
def _maybe_print_use_warning() -> None:
"""Print a warning if Streamlit is imported but not being run with `streamlit run`.
The warning is printed only once, and is printed using the root logger.
"""
global _use_warning_has_been_displayed
if not _use_warning_has_been_displayed:
_use_warning_has_been_displayed = True
warning = cli_util.style_for_cli("Warning:", bold=True, fg="yellow")
if env_util.is_repl():
logger.get_logger("root").warning(
f"\n {warning} to view a Streamlit app on a browser, use Streamlit in "
"a file and\n run it with the following command:\n\n streamlit run "
"[FILE_NAME] [ARGUMENTS]"
)
elif not runtime.exists() and config.get_option(
"global.showWarningOnDirectExecution"
):
script_name = sys.argv[0]
logger.get_logger("root").warning(
f"\n {warning} to view this Streamlit app on a browser, run it with "
f"the following\n command:\n\n streamlit run {script_name} "
"[ARGUMENTS]"
)
class DeltaGenerator(
AlertMixin,
AudioInputMixin,
BalloonsMixin,
BokehMixin,
ButtonMixin,
ButtonGroupMixin,
CameraInputMixin,
ChatMixin,
CheckboxMixin,
CodeMixin,
ColorPickerMixin,
EmptyMixin,
ExceptionMixin,
FileUploaderMixin,
FormMixin,
GraphvizMixin,
HeadingMixin,
HelpMixin,
HtmlMixin,
IframeMixin,
ImageMixin,
LayoutsMixin,
MarkdownMixin,
MapMixin,
MediaMixin,
MetricMixin,
MultiSelectMixin,
NumberInputMixin,
PlotlyMixin,
ProgressMixin,
PydeckMixin,
PyplotMixin,
RadioMixin,
SelectboxMixin,
SelectSliderMixin,
SliderMixin,
SnowMixin,
JsonMixin,
TextMixin,
TextWidgetsMixin,
TimeWidgetsMixin,
ToastMixin,
WriteMixin,
ArrowMixin,
VegaChartsMixin,
DataEditorMixin,
):
"""Creator of Delta protobuf messages.
Parameters
----------
root_container: BlockPath_pb2.BlockPath.ContainerValue or None
The root container for this DeltaGenerator. If None, this is a null
DeltaGenerator which doesn't print to the app at all (useful for
testing).
cursor: cursor.Cursor or None
This is either:
- None: if this is the running DeltaGenerator for a top-level
container (MAIN or SIDEBAR)
- RunningCursor: if this is the running DeltaGenerator for a
non-top-level container (created with dg.container())
- LockedCursor: if this is a locked DeltaGenerator returned by some
other DeltaGenerator method. E.g. the dg returned in dg =
st.text("foo").
parent: DeltaGenerator
To support the `with dg` notation, DGs are arranged as a tree. Each DG
remembers its own parent, and the root of the tree is the main DG.
block_type: None or "vertical" or "horizontal" or "column" or "expandable"
If this is a block DG, we track its type to prevent nested columns/expanders
"""
# The pydoc below is for user consumption, so it doesn't talk about
# DeltaGenerator constructor parameters (which users should never use). For
# those, see above.
def __init__(
self,
root_container: int | None = RootContainer.MAIN,
cursor: Cursor | None = None,
parent: DeltaGenerator | None = None,
block_type: str | None = None,
) -> None:
"""Inserts or updates elements in Streamlit apps.
As a user, you should never initialize this object by hand. Instead,
DeltaGenerator objects are initialized for you in two places:
1) When you call `dg = st.foo()` for some method "foo", sometimes `dg`
is a DeltaGenerator object. You can call methods on the `dg` object to
update the element `foo` that appears in the Streamlit app.
2) This is an internal detail, but `st.sidebar` itself is a
DeltaGenerator. That's why you can call `st.sidebar.foo()` to place
an element `foo` inside the sidebar.
"""
# Sanity check our Container + Cursor, to ensure that our Cursor
# is using the same Container that we are.
if (
root_container is not None
and cursor is not None
and root_container != cursor.root_container
):
raise RuntimeError(
"DeltaGenerator root_container and cursor.root_container must be the same"
)
# Whether this DeltaGenerator is nested in the main area or sidebar.
# No relation to `st.container()`.
self._root_container = root_container
# NOTE: You should never use this directly! Instead, use self._cursor,
# which is a computed property that fetches the right cursor.
self._provided_cursor = cursor
self._parent = parent
self._block_type = block_type
# If this an `st.form` block, this will get filled in.
self._form_data: FormData | None = None
# Change the module of all mixin'ed functions to be st.delta_generator,
# instead of the original module (e.g. st.elements.markdown)
for mixin in self.__class__.__bases__:
for _, func in mixin.__dict__.items():
if callable(func):
func.__module__ = self.__module__
def __repr__(self) -> str:
return util.repr_(self)
def __enter__(self) -> None:
# with block started
context_dg_stack.set(context_dg_stack.get() + (self,))
def __exit__(
self,
type: Any,
value: Any,
traceback: Any,
) -> Literal[False]:
# with block ended
context_dg_stack.set(context_dg_stack.get()[:-1])
# Re-raise any exceptions
return False
@property
def _active_dg(self) -> DeltaGenerator:
"""Return the DeltaGenerator that's currently 'active'.
If we are the main DeltaGenerator, and are inside a `with` block that
creates a container, our active_dg is that container. Otherwise,
our active_dg is self.
"""
if self == self._main_dg:
# We're being invoked via an `st.foo` pattern - use the current
# `with` dg (aka the top of the stack).
last_context_stack_dg = get_last_dg_added_to_context_stack()
if last_context_stack_dg is not None:
return last_context_stack_dg
# We're being invoked via an `st.sidebar.foo` pattern - ignore the
# current `with` dg.
return self
@property
def _main_dg(self) -> DeltaGenerator:
"""Return this DeltaGenerator's root - that is, the top-level ancestor
DeltaGenerator that we belong to (this generally means the st._main
DeltaGenerator).
"""
return self._parent._main_dg if self._parent else self
def __getattr__(self, name: str) -> Callable[..., NoReturn]:
import streamlit as st
streamlit_methods = [
method_name for method_name in dir(st) if callable(getattr(st, method_name))
]
def wrapper(*args: Any, **kwargs: Any) -> NoReturn:
if name in streamlit_methods:
if self._root_container == RootContainer.SIDEBAR:
message = (
f"Method `{name}()` does not exist for "
f"`st.sidebar`. Did you mean `st.{name}()`?"
)
else:
message = (
f"Method `{name}()` does not exist for "
"`DeltaGenerator` objects. Did you mean "
f"`st.{name}()`?"
)
else:
message = f"`{name}()` is not a valid Streamlit command."
raise StreamlitAPIException(message)
return wrapper
def __deepcopy__(self, _memo):
dg = DeltaGenerator(
root_container=self._root_container,
cursor=deepcopy(self._cursor),
parent=deepcopy(self._parent),
block_type=self._block_type,
)
dg._form_data = deepcopy(self._form_data)
return dg
@property
def _ancestors(self) -> Iterable[DeltaGenerator]:
current_dg: DeltaGenerator | None = self
while current_dg is not None:
yield current_dg
current_dg = current_dg._parent
@property
def _ancestor_block_types(self) -> AncestorBlockTypes:
"""Iterate all the block types used by this DeltaGenerator and all
its ancestor DeltaGenerators.
"""
for a in self._ancestors:
if a._block_type is not None:
yield a._block_type
def _count_num_of_parent_columns(
self, ancestor_block_types: AncestorBlockTypes
) -> int:
return sum(
1 for ancestor_block in ancestor_block_types if ancestor_block == "column"
)
@property
def _cursor(self) -> Cursor | None:
"""Return our Cursor. This will be None if we're not running in a
ScriptThread - e.g., if we're running a "bare" script outside of
Streamlit.
"""
if self._provided_cursor is None:
return cursor.get_container_cursor(self._root_container)
else:
return self._provided_cursor
@property
def _is_top_level(self) -> bool:
return self._provided_cursor is None
@property
def id(self) -> str:
return str(id(self))
def _get_delta_path_str(self) -> str:
"""Returns the element's delta path as a string like "[0, 2, 3, 1]".
This uniquely identifies the element's position in the front-end,
which allows (among other potential uses) the MediaFileManager to maintain
session-specific maps of MediaFile objects placed with their "coordinates".
This way, users can (say) use st.image with a stream of different images,
and Streamlit will expire the older images and replace them in place.
"""
# Operate on the active DeltaGenerator, in case we're in a `with` block.
dg = self._active_dg
return str(dg._cursor.delta_path) if dg._cursor is not None else "[]"
def _enqueue(
self,
delta_type: str,
element_proto: Message,
add_rows_metadata: AddRowsMetadata | None = None,
user_key: str | None = None,
) -> DeltaGenerator:
"""Create NewElement delta, fill it, and enqueue it.
Parameters
----------
delta_type : str
The name of the streamlit method being called
element_proto : proto
The actual proto in the NewElement type e.g. Alert/Button/Slider
add_rows_metadata : AddRowsMetadata or None
Metadata for the add_rows method
user_key : str or None
A custom key for the element provided by the user.
Returns
-------
DeltaGenerator
Return a DeltaGenerator that can be used to modify the newly-created
element.
"""
# Operate on the active DeltaGenerator, in case we're in a `with` block.
dg = self._active_dg
ctx = get_script_run_ctx()
if ctx and ctx.current_fragment_id and _writes_directly_to_sidebar(dg):
raise StreamlitAPIException(
"Calling `st.sidebar` in a function wrapped with `st.fragment` is not "
"supported. To write elements to the sidebar with a fragment, call your "
"fragment function inside a `with st.sidebar` context manager."
)
# Warn if an element is being changed but the user isn't running the streamlit server.
_maybe_print_use_warning()
# Copy the marshalled proto into the overall msg proto
msg = ForwardMsg_pb2.ForwardMsg()
msg_el_proto = getattr(msg.delta.new_element, delta_type)
msg_el_proto.CopyFrom(element_proto)
# Only enqueue message and fill in metadata if there's a container.
msg_was_enqueued = False
if dg._root_container is not None and dg._cursor is not None:
msg.metadata.delta_path[:] = dg._cursor.delta_path
_enqueue_message(msg)
msg_was_enqueued = True
if msg_was_enqueued:
# Get a DeltaGenerator that is locked to the current element
# position.
new_cursor = (
dg._cursor.get_locked_cursor(
delta_type=delta_type, add_rows_metadata=add_rows_metadata
)
if dg._cursor is not None
else None
)
output_dg = DeltaGenerator(
root_container=dg._root_container,
cursor=new_cursor,
parent=dg,
)
else:
# If the message was not enqueued, just return self since it's a
# no-op from the point of view of the app.
output_dg = dg
# Save message for replay if we're called from within @st.cache_data or @st.cache_resource
caching.save_element_message(
delta_type,
element_proto,
invoked_dg_id=self.id,
used_dg_id=dg.id,
returned_dg_id=output_dg.id,
)
return output_dg
def _block(
self,
block_proto: Block_pb2.Block = Block_pb2.Block(),
dg_type: type | None = None,
) -> DeltaGenerator:
# Operate on the active DeltaGenerator, in case we're in a `with` block.
dg = self._active_dg
# Prevent nested columns & expanders by checking all parents.
block_type = block_proto.WhichOneof("type")
# Convert the generator to a list, so we can use it multiple times.
ancestor_block_types = list(dg._ancestor_block_types)
_check_nested_element_violation(self, block_type, ancestor_block_types)
if dg._root_container is None or dg._cursor is None:
return dg
msg = ForwardMsg_pb2.ForwardMsg()
msg.metadata.delta_path[:] = dg._cursor.delta_path
msg.delta.add_block.CopyFrom(block_proto)
# Normally we'd return a new DeltaGenerator that uses the locked cursor
# below. But in this case we want to return a DeltaGenerator that uses
# a brand new cursor for this new block we're creating.
block_cursor = cursor.RunningCursor(
root_container=dg._root_container,
parent_path=dg._cursor.parent_path + (dg._cursor.index,),
)
# `dg_type` param added for st.status container. It allows us to
# instantiate DeltaGenerator subclasses from the function.
if dg_type is None:
dg_type = DeltaGenerator
block_dg = cast(
"DeltaGenerator",
dg_type(
root_container=dg._root_container,
cursor=block_cursor,
parent=dg,
block_type=block_type,
),
)
# Blocks inherit their parent form ids.
# NOTE: Container form ids aren't set in proto.
block_dg._form_data = FormData(current_form_id(dg))
# Must be called to increment this cursor's index.
dg._cursor.get_locked_cursor(add_rows_metadata=None)
_enqueue_message(msg)
caching.save_block_message(
block_proto,
invoked_dg_id=self.id,
used_dg_id=dg.id,
returned_dg_id=block_dg.id,
)
return block_dg
def _writes_directly_to_sidebar(dg: DeltaGenerator) -> bool:
in_sidebar = any(a._root_container == RootContainer.SIDEBAR for a in dg._ancestors)
has_container = bool(list(dg._ancestor_block_types))
return in_sidebar and not has_container
def _check_nested_element_violation(
dg: DeltaGenerator, block_type: str | None, ancestor_block_types: list[BlockType]
) -> None:
"""Check if elements are nested in a forbidden way.
Raises
------
StreamlitAPIException: throw if an invalid element nesting is detected.
"""
if block_type == "column":
num_of_parent_columns = dg._count_num_of_parent_columns(ancestor_block_types)
if dg._root_container == RootContainer.SIDEBAR and num_of_parent_columns > 0:
raise StreamlitAPIException(
"Columns cannot be placed inside other columns in the sidebar. This is only possible in the main area of the app."
)
if num_of_parent_columns > 1:
raise StreamlitAPIException(
"Columns can only be placed inside other columns up to one level of nesting."
)
if block_type == "chat_message" and block_type in ancestor_block_types:
raise StreamlitAPIException(
"Chat messages cannot nested inside other chat messages."
)
if block_type == "expandable" and block_type in ancestor_block_types:
raise StreamlitAPIException(
"Expanders may not be nested inside other expanders."
)
if block_type == "popover" and block_type in ancestor_block_types:
raise StreamlitAPIException("Popovers may not be nested inside other popovers.")

View File

@@ -0,0 +1,204 @@
# 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.
"""
The main purpose of this module (right now at least) is to avoid a dependency
cycle between streamlit.delta_generator and some elements.
"""
from __future__ import annotations
from contextvars import ContextVar, Token
from typing import TYPE_CHECKING, Callable, Generic, TypeVar
from streamlit.proto.RootContainer_pb2 import RootContainer as _RootContainer
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
class DeltaGeneratorSingleton:
"""Used to initialize the DeltaGenerator classes and store them as singletons.
This module allows us to avoid circular imports between DeltaGenerator and elements,
because elemens can import this singleton module instead of DeltaGenerator directly.
"""
_instance: DeltaGeneratorSingleton | None = None
@classmethod
def instance(cls) -> DeltaGeneratorSingleton:
"""Return the singleton DeltaGeneratorSingleton instance. Raise an Error if the
DeltaGeneratorSingleton hasn't been created yet.
"""
if cls._instance is None:
raise RuntimeError("DeltaGeneratorSingleton hasn't been created!")
return cls._instance
def __init__(
self,
delta_generator_cls: type[DeltaGenerator],
status_container_cls: type[StatusContainer],
dialog_container_cls: type[Dialog],
):
"""Registers and initializes all delta-generator classes.
Parameters
----------
delta_generator_cls : type[DeltaGenerator]
The main DeltaGenerator class.
status_container_cls : type[StatusContainer]
The delta-generator class that is used as return value for `st.status`.
dialog_container_cls : type[Dialog]
The delta-generator class used is used as return value for `st.dialog`.
Raises
------
RuntimeError
If the DeltaGeneratorSingleton instance already exists.
"""
if DeltaGeneratorSingleton._instance is not None:
raise RuntimeError("DeltaGeneratorSingleton instance already exists!")
DeltaGeneratorSingleton._instance = self
self._main_dg = delta_generator_cls(root_container=_RootContainer.MAIN)
self._sidebar_dg = delta_generator_cls(
root_container=_RootContainer.SIDEBAR, parent=self._main_dg
)
self._event_dg = delta_generator_cls(
root_container=_RootContainer.EVENT, parent=self._main_dg
)
self._bottom_dg = delta_generator_cls(
root_container=_RootContainer.BOTTOM, parent=self._main_dg
)
self._status_container_cls = status_container_cls
self._dialog_container_cls = dialog_container_cls
@property
def main_dg(self) -> DeltaGenerator:
return self._main_dg
@property
def sidebar_dg(self) -> DeltaGenerator:
return self._sidebar_dg
@property
def event_dg(self) -> DeltaGenerator:
return self._event_dg
@property
def bottom_dg(self) -> DeltaGenerator:
return self._bottom_dg
@property
def status_container_cls(
self,
) -> type[StatusContainer]:
"""Stub for StatusContainer. Since StatusContainer inherits from DeltaGenerator,
this is used to avoid circular imports.
"""
return self._status_container_cls
@property
def dialog_container_cls(self) -> type[Dialog]:
"""Stub for Dialog. Since Dialog inherits from DeltaGenerator,
this is used to avoid circular imports.
"""
return self._dialog_container_cls
def get_dg_singleton_instance() -> DeltaGeneratorSingleton:
"""Return the DeltaGeneratorSingleton instance. Raise an Error if the
DeltaGeneratorSingleton hasn't been created yet.
"""
return DeltaGeneratorSingleton.instance()
_T = TypeVar("_T")
class ContextVarWithLazyDefault(Generic[_T]):
"""The dg_stack tracks the currently active DeltaGenerator, and is pushed to when
a DeltaGenerator is entered via a `with` block. This is implemented as a ContextVar
so that different threads or async tasks can have their own stacks.
We have a wrapper around it because ContextVar default cannot be a function, but
the default dg (main_dg) might not exist yet when this module is imported.
"""
def __init__(self, name: str, *, default: Callable[[], _T]):
self._name = name
self._default = default
self._context_var: ContextVar[_T] | None = None
def _init_context_var(self) -> None:
self._context_var = ContextVar(self._name, default=self._default()) # noqa: B039
def get(self) -> _T:
if self._context_var is None:
self._init_context_var()
return self._context_var.get() # type: ignore[union-attr]
def set(self, value: _T) -> Token[_T]:
if self._context_var is None:
self._init_context_var()
return self._context_var.set(value) # type: ignore[union-attr]
def reset(self, token: Token[_T]) -> None:
if self._context_var is None:
self._init_context_var()
self._context_var.reset(token) # type: ignore[union-attr]
def __hash__(self) -> int:
if self._context_var is None:
self._init_context_var()
return self._context_var.__hash__()
# we don't use the default factory here because `main_dg` is not initialized when this
# module is imported. This is why we have our own ContextVar wrapper.
context_dg_stack: ContextVarWithLazyDefault[tuple[DeltaGenerator, ...]] = (
ContextVarWithLazyDefault(
"context_dg_stack", default=lambda: (get_dg_singleton_instance().main_dg,)
)
)
def get_default_dg_stack_value() -> tuple[DeltaGenerator, ...]:
"""Get the default dg_stack value with which the dg_stack should
be initialized and reset if needed.
"""
instance = get_dg_singleton_instance()
if instance._main_dg is None:
raise RuntimeError("main_dg is not set")
return (instance._main_dg,)
def get_last_dg_added_to_context_stack() -> DeltaGenerator | None:
"""Get the last added DeltaGenerator of the stack in the current context.
Returns None if the stack has only one element or is empty for whatever reason.
"""
current_stack = context_dg_stack.get()
# If set to "> 0" and thus return the only delta generator in the stack -
# which logically makes more sense -, some unit tests fail.
# It looks like the reason is that they create their own main delta generator
# but do not populate the dg_stack correctly. However, to be on the safe-side,
# we keep the logic but leave the comment as shared knowledge for whoever will look
# into this in the future.
if len(current_stack) > 1:
return current_stack[-1]
return None

View File

@@ -0,0 +1,209 @@
# 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 functools
from typing import Any, Callable, Final, TypeVar, cast
import streamlit
from streamlit import config
from streamlit.logger import get_logger
_LOGGER: Final = get_logger(__name__)
TFunc = TypeVar("TFunc", bound=Callable[..., Any])
TObj = TypeVar("TObj", bound=object)
def _should_show_deprecation_warning_in_browser() -> bool:
"""True if we should print deprecation warnings to the browser."""
return bool(config.get_option("client.showErrorDetails"))
def show_deprecation_warning(message: str) -> None:
"""Show a deprecation warning message."""
if _should_show_deprecation_warning_in_browser():
streamlit.warning(message)
# We always log deprecation warnings
_LOGGER.warning(message)
def make_deprecated_name_warning(
old_name: str,
new_name: str,
removal_date: str,
extra_message: str | None = None,
include_st_prefix: bool = True,
) -> str:
if include_st_prefix:
old_name = f"st.{old_name}"
new_name = f"st.{new_name}"
return (
f"Please replace `{old_name}` with `{new_name}`.\n\n"
f"`{old_name}` will be removed after {removal_date}."
+ (f"\n\n{extra_message}" if extra_message else "")
)
def deprecate_func_name(
func: TFunc,
old_name: str,
removal_date: str,
extra_message: str | None = None,
name_override: str | None = None,
) -> TFunc:
"""Wrap an `st` function whose name has changed.
Wrapped functions will run as normal, but will also show an st.warning
saying that the old name will be removed after removal_date.
(We generally set `removal_date` to 3 months from the deprecation date.)
Parameters
----------
func
The `st.` function whose name has changed.
old_name
The function's deprecated name within __init__.py.
removal_date
A date like "2020-01-01", indicating the last day we'll guarantee
support for the deprecated name.
extra_message
An optional extra message to show in the deprecation warning.
name_override
An optional name to use in place of func.__name__.
"""
@functools.wraps(func)
def wrapped_func(*args, **kwargs):
result = func(*args, **kwargs)
show_deprecation_warning(
make_deprecated_name_warning(
old_name, name_override or func.__name__, removal_date, extra_message
)
)
return result
# Update the wrapped func's name & docstring so st.help does the right thing
wrapped_func.__name__ = old_name
wrapped_func.__doc__ = func.__doc__
return cast("TFunc", wrapped_func)
def deprecate_obj_name(
obj: TObj,
old_name: str,
new_name: str,
removal_date: str,
include_st_prefix: bool = True,
) -> TObj:
"""Wrap an `st` object whose name has changed.
Wrapped objects will behave as normal, but will also show an st.warning
saying that the old name will be removed after `removal_date`.
(We generally set `removal_date` to 3 months from the deprecation date.)
Parameters
----------
obj
The `st.` object whose name has changed.
old_name
The object's deprecated name within __init__.py.
new_name
The object's new name within __init__.py.
removal_date
A date like "2020-01-01", indicating the last day we'll guarantee
support for the deprecated name.
include_st_prefix
If False, does not prefix each of the object names in the deprecation
essage with `st.*`. Defaults to True.
"""
return _create_deprecated_obj_wrapper(
obj,
lambda: show_deprecation_warning(
make_deprecated_name_warning(
old_name, new_name, removal_date, include_st_prefix=include_st_prefix
)
),
)
def _create_deprecated_obj_wrapper(obj: TObj, show_warning: Callable[[], Any]) -> TObj:
"""Create a wrapper for an object that has been deprecated. The first
time one of the object's properties or functions is accessed, the
given `show_warning` callback will be called.
"""
has_shown_warning = False
def maybe_show_warning() -> None:
# Call `show_warning` if it hasn't already been called once.
nonlocal has_shown_warning
if not has_shown_warning:
has_shown_warning = True
show_warning()
class Wrapper:
def __init__(self):
# Override all the Wrapped object's magic functions
for name in Wrapper._get_magic_functions(obj.__class__):
setattr(
self.__class__,
name,
property(self._make_magic_function_proxy(name)),
)
def __getattr__(self, attr):
# We handle __getattr__ separately from our other magic
# functions. The wrapped class may not actually implement it,
# but we still need to implement it to call all its normal
# functions.
if attr in self.__dict__:
return getattr(self, attr)
maybe_show_warning()
return getattr(obj, attr)
@staticmethod
def _get_magic_functions(cls) -> list[str]:
# ignore the handful of magic functions we cannot override without
# breaking the Wrapper.
ignore = ("__class__", "__dict__", "__getattribute__", "__getattr__")
return [
name
for name in dir(cls)
if name not in ignore and name.startswith("__")
]
@staticmethod
def _make_magic_function_proxy(name):
def proxy(self, *args):
maybe_show_warning()
return getattr(obj, name)
return proxy
return cast("TObj", Wrapper())

View File

@@ -0,0 +1,21 @@
# 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.
"""Variables for dev purposes.
The main purpose of this module (right now at least) is to avoid a dependency
cycle between streamlit.config and streamlit.logger.
"""
is_development_mode = False

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)

Some files were not shown because too many files have changed in this diff Show More