# 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]] RANDOM_EMOJIS: Final = list( "๐Ÿ”ฅโ„ข๐ŸŽ‰๐Ÿš€๐ŸŒŒ๐Ÿ’ฃโœจ๐ŸŒ™๐ŸŽ†๐ŸŽ‡๐Ÿ’ฅ๐Ÿคฉ๐Ÿค™๐ŸŒ›๐Ÿค˜โฌ†๐Ÿ’ก๐Ÿคช๐Ÿฅ‚โšก๐Ÿ’จ๐ŸŒ ๐ŸŽŠ๐Ÿฟ๐Ÿ˜›๐Ÿ”ฎ๐ŸคŸ๐ŸŒƒ๐Ÿƒ๐Ÿพ๐Ÿ’ซโ–ช๐ŸŒด๐ŸŽˆ๐ŸŽฌ๐ŸŒ€๐ŸŽ„๐Ÿ˜โ˜”โ›ฝ๐Ÿ‚๐Ÿ’ƒ๐Ÿ˜Ž๐Ÿธ๐ŸŽจ๐Ÿฅณโ˜€๐Ÿ˜๐Ÿ…ฑ๐ŸŒž๐Ÿ˜ป๐ŸŒŸ๐Ÿ˜œ๐Ÿ’ฆ๐Ÿ’…๐Ÿฆ„๐Ÿ˜‹๐Ÿ˜‰๐Ÿ‘ป๐Ÿ๐Ÿคค๐Ÿ‘ฏ๐ŸŒปโ€ผ๐ŸŒˆ๐Ÿ‘Œ๐ŸŽƒ๐Ÿ’›๐Ÿ˜š๐Ÿ”ซ๐Ÿ™Œ๐Ÿ‘ฝ๐Ÿฌ๐ŸŒ…โ˜๐Ÿท๐Ÿ‘ญโ˜•๐ŸŒš๐Ÿ’๐Ÿ‘…๐Ÿฅฐ๐Ÿœ๐Ÿ˜Œ๐ŸŽฅ๐Ÿ•บโ•๐Ÿงกโ˜„๐Ÿ’•๐Ÿปโœ…๐ŸŒธ๐Ÿšฌ๐Ÿค“๐Ÿนยฎโ˜บ๐Ÿ’ช๐Ÿ˜™โ˜˜๐Ÿค โœŠ๐Ÿค—๐Ÿต๐Ÿคž๐Ÿ˜‚๐Ÿ’ฏ๐Ÿ˜๐Ÿ“ป๐ŸŽ‚๐Ÿ’—๐Ÿ’œ๐ŸŒŠโฃ๐ŸŒ๐Ÿ˜˜๐Ÿ’†๐Ÿค‘๐ŸŒฟ๐Ÿฆ‹๐Ÿ˜ˆโ›„๐Ÿšฟ๐Ÿ˜Š๐ŸŒน๐Ÿฅด๐Ÿ˜ฝ๐Ÿ’‹๐Ÿ˜ญ๐Ÿ–ค๐Ÿ™†๐Ÿ‘โšช๐Ÿ’Ÿโ˜ƒ๐Ÿ™ˆ๐Ÿญ๐Ÿ’ป๐Ÿฅ€๐Ÿš—๐Ÿคง๐Ÿ๐Ÿ’Ž๐Ÿ’“๐Ÿค๐Ÿ’„๐Ÿ’–๐Ÿ”žโ‰โฐ๐Ÿ•Š๐ŸŽงโ˜ โ™ฅ๐ŸŒณ๐Ÿพ๐Ÿ™‰โญ๐Ÿ’Š๐Ÿณ๐ŸŒŽ๐Ÿ™Š๐Ÿ’ธโค๐Ÿ”ช๐Ÿ˜†๐ŸŒพโœˆ๐Ÿ“š๐Ÿ’€๐Ÿ โœŒ๐Ÿƒ๐ŸŒต๐Ÿšจ๐Ÿ’‚๐Ÿคซ๐Ÿคญ๐Ÿ˜—๐Ÿ˜„๐Ÿ’๐Ÿ‘๐Ÿ™ƒ๐Ÿ––๐Ÿ’ž๐Ÿ˜…๐ŸŽ…๐Ÿ„๐Ÿ†“๐Ÿ‘‰๐Ÿ’ฉ๐Ÿ”Š๐ŸคทโŒš๐Ÿ‘ธ๐Ÿ˜‡๐Ÿšฎ๐Ÿ’๐Ÿ‘ณ๐Ÿฝ๐Ÿ’˜๐Ÿ’ฟ๐Ÿ’‰๐Ÿ‘ ๐ŸŽผ๐ŸŽถ๐ŸŽค๐Ÿ‘—โ„๐Ÿ”๐ŸŽต๐Ÿค’๐Ÿฐ๐Ÿ‘“๐Ÿ„๐ŸŒฒ๐ŸŽฎ๐Ÿ™‚๐Ÿ“ˆ๐Ÿš™๐Ÿ“๐Ÿ˜ต๐Ÿ—ฃโ—๐ŸŒบ๐Ÿ™„๐Ÿ‘„๐Ÿš˜๐Ÿฅบ๐ŸŒ๐Ÿกโ™ฆ๐Ÿ’๐ŸŒฑ๐Ÿ‘‘๐Ÿ‘™โ˜‘๐Ÿ‘พ๐Ÿฉ๐Ÿฅถ๐Ÿ“ฃ๐Ÿผ๐Ÿคฃโ˜ฏ๐Ÿ‘ต๐Ÿซโžก๐ŸŽ€๐Ÿ˜ƒโœ‹๐Ÿž๐Ÿ™‡๐Ÿ˜น๐Ÿ™๐Ÿ‘ผ๐Ÿโšซ๐ŸŽ๐Ÿช๐Ÿ”จ๐ŸŒผ๐Ÿ‘†๐Ÿ‘€๐Ÿ˜ณ๐ŸŒ๐Ÿ“–๐Ÿ‘ƒ๐ŸŽธ๐Ÿ‘ง๐Ÿ’‡๐Ÿ”’๐Ÿ’™๐Ÿ˜žโ›…๐Ÿป๐Ÿด๐Ÿ˜ผ๐Ÿ—ฟ๐Ÿ—โ™ ๐Ÿฆโœ”๐Ÿค–โ˜ฎ๐Ÿข๐ŸŽ๐Ÿ’ค๐Ÿ˜€๐Ÿบ๐Ÿ˜๐Ÿ˜ด๐Ÿ“บโ˜น๐Ÿ˜ฒ๐Ÿ‘๐ŸŽญ๐Ÿ’š๐Ÿ†๐Ÿ‹๐Ÿ”ต๐Ÿ๐Ÿ”ด๐Ÿ””๐Ÿง๐Ÿ‘ฐโ˜Ž๐Ÿ†๐Ÿคก๐Ÿ ๐Ÿ“ฒ๐Ÿ™‹๐Ÿ“Œ๐Ÿฌโœ๐Ÿ”‘๐Ÿ“ฑ๐Ÿ’ฐ๐Ÿฑ๐Ÿ’ง๐ŸŽ“๐Ÿ•๐Ÿ‘Ÿ๐Ÿฃ๐Ÿ‘ซ๐Ÿ‘๐Ÿ˜ธ๐Ÿฆ๐Ÿ‘๐Ÿ†—๐ŸŽฏ๐Ÿ“ข๐Ÿšถ๐Ÿฆ…๐Ÿง๐Ÿ’ข๐Ÿ€๐Ÿšซ๐Ÿ’‘๐ŸŸ๐ŸŒฝ๐ŸŠ๐ŸŸ๐Ÿ’๐Ÿ’ฒ๐Ÿ๐Ÿฅ๐Ÿธโ˜โ™ฃ๐Ÿ‘Šโš“โŒ๐Ÿฏ๐Ÿˆ๐Ÿ“ฐ๐ŸŒง๐Ÿ‘ฟ๐Ÿณ๐Ÿ’ท๐Ÿบ๐Ÿ“ž๐Ÿ†’๐Ÿ€๐Ÿค๐Ÿšฒ๐Ÿ”๐Ÿ‘น๐Ÿ™๐ŸŒท๐Ÿ™Ž๐Ÿฅ๐Ÿ’ต๐Ÿ”๐Ÿ“ธโš โ“๐ŸŽฉโœ‚๐Ÿผ๐Ÿ˜‘โฌ‡โšพ๐ŸŽ๐Ÿ’”๐Ÿ”โšฝ๐Ÿ’ญ๐ŸŒ๐Ÿท๐Ÿโœ–๐Ÿ‡๐Ÿ“๐ŸŠ๐Ÿ™๐Ÿ‘‹๐Ÿค”๐ŸฅŠ๐Ÿ—ฝ๐Ÿ‘๐Ÿ˜๐Ÿฐ๐Ÿ’๐Ÿดโ™€๐Ÿฆ๐Ÿ“โœ๐Ÿ‘‚๐Ÿด๐Ÿ‘‡๐Ÿ†˜๐Ÿ˜ก๐Ÿ‰๐Ÿ‘ฉ๐Ÿ’Œ๐Ÿ˜บโœ๐Ÿผ๐Ÿ’๐Ÿถ๐Ÿ‘บ๐Ÿ–•๐Ÿ‘ฌ๐Ÿ‰๐Ÿป๐Ÿพโฌ…โฌโ–ถ๐Ÿ‘ฎ๐ŸŒโ™‚๐Ÿ”ธ๐Ÿ‘ถ๐Ÿฎ๐Ÿ‘ชโ›ณ๐Ÿ๐ŸŽพ๐Ÿ•๐Ÿ‘ด๐Ÿจ๐ŸŠ๐Ÿ”นยฉ๐ŸŽฃ๐Ÿ‘ฆ๐Ÿ‘ฃ๐Ÿ‘จ๐Ÿ‘ˆ๐Ÿ’ฌโญ•๐Ÿ“น๐Ÿ“ท" ) 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 | None = None, initial_sidebar_state: InitialSideBarState | None = None, menu_items: MenuItems | None = None, ) -> None: """ Configure the default settings of the page. This command can be called multiple times in a script run to dynamically change the page configuration. The calls are additive, with each successive call overriding only the parameters that are specified. Parameters ---------- page_title: str or None The page title, shown in the browser tab. If this is ``None`` (default), the page title is inherited from the previous call of ``st.set_page_config``. If this is ``None`` and no previous call exists, the page title is inferred from the page source. If a page source is a Python file, its inferred title is derived from the filename. If a page source is a callable object, its inferred title is derived from the callable's name. page_icon : Anything supported by st.image (except list), str, or None The page favicon. If ``page_icon`` is ``None`` (default), the page icon is inherited from the previous call of ``st.set_page_config``. If this is ``None`` and no previous call exists, the favicon is 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 \ `_ 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", "wide", or None How the page content should be laid out. If this is ``None`` (default), the page layout is inherited from the previous call of ``st.set_page_config``. If this is ``None`` and no previous call exists, the page layout is ``"centered"``. ``"centered"`` constrains the elements into a centered column of fixed width. ``"wide"`` uses the entire screen. initial_sidebar_state: "auto", "expanded", "collapsed", or None How the sidebar should start out. If this is ``None`` (default), the sidebar state is inherited from the previous call of ``st.set_page_config``. If no previous call exists, the sidebar state is ``"auto"``. The folowing states are supported: - ``"auto"``: The sidebar is hidden on small devices and shown otherwise. - ``"expanded"``: The sidebar is shown initially. - ``"collapsed"``: The sidebar is hidden initially. In most cases, ``"auto"`` provides the best user experience across devices of different sizes. 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``. To remove an item that was specified in a previous call to ``st.set_page_config``, set its value to ``None`` in the dictionary. 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 elif layout is None: # Allows for multiple (additive) calls to set_page_config pb_layout = PageConfigProto.LAYOUT_UNSET else: # Note: Pylance incorrectly notes this error as unreachable 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 elif initial_sidebar_state is None: # Allows for multiple (additive) calls to set_page_config pb_sidebar_state = PageConfigProto.SIDEBAR_UNSET else: # Note: Pylance incorrectly notes this error as unreachable 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: # TODO: fix the random seed with a hash of the user's app code, for stability? return random.choice(RANDOM_EMOJIS) # noqa: S311 def set_menu_items_proto( lowercase_menu_items: MenuItems, menu_items_proto: PageConfigProto.MenuItems ) -> 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]) else: # For multiple calls to set_page_config, clears previously set about markdown menu_items_proto.clear_about_md = True 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}