def beta_expander(self, label=None, expanded=False) -> "DeltaGenerator": """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 "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. expanded : bool If True, initializes the expander in "expanded" state. Defaults to False (collapsed). Examples -------- >>> st.line_chart({"data": [1, 5, 2, 6, 2, 1]}) >>> >>> with st.beta_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://static.streamlit.io/0.66.0-2BLtg/index.html?id=7v2tgefVbW278gemvYrRny height: 750px """ if label is None: raise StreamlitAPIException("A label is required for an expander") expandable_proto = Block_pb2.Block.Expandable() expandable_proto.expanded = expanded expandable_proto.label = label block_proto = Block_pb2.Block() block_proto.allow_empty = True block_proto.expandable.CopyFrom(expandable_proto) return self._block(block_proto=block_proto)
def enqueue(self, msg: ForwardMsg) -> None: if msg.HasField( "page_config_changed") and not self._set_page_config_allowed: raise StreamlitAPIException( "`set_page_config()` can only be called once per app, " + "and must be called as the first Streamlit command in your script.\n\n" + "For more information refer to the [docs]" + "(https://docs.streamlit.io/en/stable/api.html#streamlit.set_page_config)." ) if msg.HasField("delta") or msg.HasField("page_config_changed"): self._set_page_config_allowed = False self._enqueue(msg)
def __init__( self, name: str, path: Optional[str] = None, url: Optional[str] = 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
def time_input(self, label, value=None, key=None): """Display a time input widget. Parameters ---------- label : str A short label explaining to the user what this time input is for. value : datetime.time/datetime.datetime The value of this widget when it first renders. This will be cast to str internally. Defaults to the current time. key : str An optional string 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. Multiple widgets of the same type may not share the same key. Returns ------- datetime.time The current value of the time input widget. Example ------- >>> t = st.time_input('Set an alarm for', datetime.time(8, 45)) >>> st.write('Alarm is set for', t) """ # Set value default. if value is None: value = datetime.now().time() # Ensure that the value is either datetime/time if not isinstance(value, datetime) and not isinstance(value, time): raise StreamlitAPIException( "The type of the value should be either datetime or time.") # Convert datetime to time if isinstance(value, datetime): value = value.time() time_input_proto = TimeInputProto() time_input_proto.label = label time_input_proto.default = time.strftime(value, "%H:%M") ui_value = register_widget("time_input", time_input_proto, user_key=key) current_value = (datetime.strptime(ui_value, "%H:%M").time() if ui_value is not None else value) return self.dg._enqueue("time_input", time_input_proto, current_value)
def __setitem__(self, key: str, value: Any) -> None: from streamlit.report_thread import get_report_ctx, ReportContext ctx = get_report_ctx() if ctx is not None: widget_ids = ctx.widget_ids_this_run.items() form_ids = ctx.form_ids_this_run.items() if key in widget_ids or key in form_ids: raise StreamlitAPIException( f"`st.session_state.{key}` cannot be modified after the widget" f" with key `{key}` is instantiated.") self._new_session_state[key] = value
def test_markdown_flag(self): """Test that ExceptionProtos for StreamlitAPIExceptions (and subclasses) have the "message_is_markdown" flag set. """ proto = ExceptionProto() exception.marshall(proto, RuntimeError("oh no!")) self.assertFalse(proto.message_is_markdown) proto = ExceptionProto() exception.marshall(proto, StreamlitAPIException("oh no!")) self.assertTrue(proto.message_is_markdown) proto = ExceptionProto() exception.marshall(proto, errors.DuplicateWidgetID("oh no!")) self.assertTrue(proto.message_is_markdown)
def _button( self, label: str, key: Optional[str], help: Optional[str], is_form_submitter: bool, ) -> "streamlit.delta_generator.DeltaGenerator": button_proto = ButtonProto() # It doesn't make sense to create a button inside a form (except # for the "Form Submitter" button that's automatically created in # every 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 report_ctx. if streamlit._is_running_with_streamlit: if is_in_form(self.dg) and not is_form_submitter: raise StreamlitAPIException( f"`st.button()` can't be used in an `st.form()`.{FORM_DOCS_INFO}" ) elif not is_in_form(self.dg) and is_form_submitter: raise StreamlitAPIException( f"`st.submit_button()` must be used inside an `st.form()`.{FORM_DOCS_INFO}" ) button_proto.label = label button_proto.default = False button_proto.is_form_submitter = is_form_submitter button_proto.form_id = current_form_id(self.dg) if help is not None: button_proto.help = help ui_value = register_widget("button", button_proto, user_key=key) current_value = ui_value if ui_value is not None else False return self.dg._enqueue("button", button_proto, current_value) # type: ignore
def marshall(proto, figure_or_dot, use_container_width): """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 elif isinstance(figure_or_dot, str): dot = figure_or_dot else: raise StreamlitAPIException("Unhandled type for graphviz chart: %s" % type(figure_or_dot)) proto.spec = dot proto.use_container_width = use_container_width
def clean_filename(name: str) -> str: """ Taken from https://github.com/django/django/blob/196a99da5d9c4c33a78259a58d38fb114a4d2ee8/django/utils/text.py#L225-L238 Return the given string converted to a string that can be used for a clean filename. Remove leading and trailing spaces; convert other spaces to underscores; and remove anything that is not an alphanumeric, dash, underscore, or dot. """ s = str(name).strip().replace(" ", "_") s = re.sub(r"(?u)[^-\w.]", "", s) if s in {"", ".", ".."}: raise StreamlitAPIException("Could not derive file name from '%s'" % name) return s
def __setitem__(self, user_key: str, value: Any) -> None: from streamlit.report_thread import get_report_ctx ctx = get_report_ctx() if ctx is not None: widget_id = self._key_id_mapping.get(user_key, None) widget_ids = ctx.widget_ids_this_run.items() form_ids = ctx.form_ids_this_run.items() if widget_id in widget_ids or user_key in form_ids: raise StreamlitAPIException( f"`st.session_state.{user_key}` cannot be modified after the widget" f" with key `{user_key}` is instantiated.") self._new_session_state[user_key] = value
def enqueue(self, msg: ForwardMsg) -> None: if msg.HasField( "page_config_changed") and not self._set_page_config_allowed: raise StreamlitAPIException( "`set_page_config()` can only be called once per app, " + "and must be called as the first Streamlit command in your script.\n\n" + "For more information refer to the [docs]" + "(https://docs.streamlit.io/library/api-reference/utilities/st.set_page_config)." ) # We want to disallow set_page config if one of the following occurs: # - set_page_config was called on this message # - The script has already started and a different st call occurs (a delta) if msg.HasField("page_config_changed") or (msg.HasField("delta") and self._has_script_started): self._set_page_config_allowed = False self._enqueue(msg)
def wrapper(*args, **kwargs): if name in streamlit_methods: if self._root_container == RootContainer.SIDEBAR: message = ( "Method `%(name)s()` does not exist for " "`st.sidebar`. Did you mean `st.%(name)s()`?" % {"name": name} ) else: message = ( "Method `%(name)s()` does not exist for " "`DeltaGenerator` objects. Did you mean " "`st.%(name)s()`?" % {"name": name} ) else: message = "`%(name)s()` is not a valid Streamlit command." % { "name": name } raise StreamlitAPIException(message)
def __setitem__(self, user_key: str, value: Any) -> None: """Set the value of the session_state entry with the given user_key. If the key corresponds to a widget or form that's been instantiated during the current script run, raise an Exception instead. """ from streamlit.script_run_context import get_script_run_ctx ctx = get_script_run_ctx() if ctx is not None: widget_id = self._key_id_mapping.get(user_key, None) widget_ids = ctx.widget_ids_this_run form_ids = ctx.form_ids_this_run if widget_id in widget_ids or user_key in form_ids: raise StreamlitAPIException( f"`st.session_state.{user_key}` cannot be modified after the widget" f" with key `{user_key}` is instantiated.") self._new_session_state[user_key] = value
def enqueue(self, msg): """Enqueue a new ForwardMsg to our browser queue. This can be called on both the main thread and a ScriptRunner run thread. Parameters ---------- msg : ForwardMsg The message to enqueue """ if not config.get_option("client.displayEnabled"): return # Avoid having two maybe_handle_execution_control_request running on # top of each other when tracer is installed. This leads to a lock # contention. if not config.get_option("runner.installTracer"): # If we have an active ScriptRunner, signal that it can handle an # execution control request. (Copy the scriptrunner reference to # avoid it being unset from underneath us, as this function can be # called outside the main thread.) scriptrunner = self._scriptrunner if scriptrunner is not None: scriptrunner.maybe_handle_execution_control_request() if msg.HasField("page_config_changed") and not self._set_page_config_allowed: raise StreamlitAPIException( "`beta_set_page_config()` can only be called once per app, " + "and must be called as the first Streamlit command in your script.\n\n" + "For more information refer to the [docs]" + "(https://docs.streamlit.io/en/stable/api.html#streamlit.beta_set_page_config)." ) if msg.HasField("delta") or msg.HasField("page_config_changed"): self._set_page_config_allowed = False self._report.enqueue(msg)
def beta_expander(self, label=None, expanded=False): """Creates a container that can be expanded and collapsed. [TODO: get more container verbage] Similar to `st.container`, `st.expander` provides a container to add elements to. However, it has the added benefit of being expandable and collapsible. Users will be able to expand and collapse the container that is identifiable with the provided label. Parameters ---------- label : str A short label used as the header for the expander. This will always be displayed even when the container is collapsed. expanded : boolean The default state for the expander. Defaults to False Returns ------- [TODO] Technically a delta generator but let's please not tell the users that... Examples -------- >>> expander = st.beta_expander("Expand Me") >>> expander.write("I can be expanded") """ if label is None: raise StreamlitAPIException("A label is required for an expander") expandable_proto = Block_pb2.Block.Expandable() expandable_proto.expanded = expanded expandable_proto.label = label block_proto = Block_pb2.Block() block_proto.expandable.CopyFrom(expandable_proto) return self._block(block_proto=block_proto)
def check_session_state_rules(default_value: Any, key: Optional[str], writes_allowed: bool = True) -> None: global _shown_default_value_warning if key is None or not streamlit._is_running_with_streamlit: return session_state = get_session_state() if not session_state.is_new_state_value(key): return if not writes_allowed: raise StreamlitAPIException( "Values for st.button, st.download_button, st.file_uploader, and " "st.form cannot be set using st.session_state.") if default_value is not None and not _shown_default_value_warning: streamlit.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
def _maybe_melt_data_for_add_rows(data, delta_type, last_index): import pandas as pd # For some delta types 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. if ( delta_type in DELTA_TYPES_THAT_MELT_DATAFRAMES or delta_type in ARROW_DELTA_TYPES_THAT_MELT_DATAFRAMES ): if not isinstance(data, pd.DataFrame): data = type_util.convert_anything_to_df(data) if type(data.index) is pd.RangeIndex: old_step = _get_pandas_index_attr(data, "step") # We have to drop the predefined index data = data.reset_index(drop=True) old_stop = _get_pandas_index_attr(data, "stop") if old_step is None or old_stop is None: raise StreamlitAPIException( "'RangeIndex' object has no attribute 'step'" ) start = last_index + old_step stop = last_index + old_step + old_stop data.index = pd.RangeIndex(start=start, stop=stop, step=old_step) last_index = stop - 1 index_name = data.index.name if index_name is None: index_name = "index" data = pd.melt(data.reset_index(), id_vars=[index_name]) return data, last_index
def _check_and_convert_to_indices(opt, default_values): if default_values is None and None not in opt: return None if not isinstance(default_values, list): # This if is done before others because calling if not x (done # right below) when x is of type pd.Series() or np.array() throws a # ValueError exception. if is_type(default_values, "numpy.ndarray") or is_type( default_values, "pandas.core.series.Series"): default_values = list(default_values) elif not default_values or default_values in opt: default_values = [default_values] else: default_values = list(default_values) for value in default_values: if value not in opt: raise StreamlitAPIException( "Every Multiselect default value must exist in options" ) return [opt.index(value) for value in default_values]
def determine_delta_color_and_direction( self, 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 self.is_negative(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 register_component(self, component: CustomComponent) -> None: """Register a CustomComponent. Parameters ---------- component : CustomComponent 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 color_picker(self, label, value=None, key=None): """Display a color picker widget. Note: This is a beta feature. See https://docs.streamlit.io/en/latest/api.html#pre-release-features for more information. Parameters ---------- label : str A short label explaining to the user what this input is for. value : str or None The hex value of this widget when it first renders. If None, defaults to black. key : str An optional string 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. Multiple widgets of the same type may not share the same key. Returns ------- str The selected color as a hex string. Example ------- >>> color = st.color_picker('Pick A Color', '#00f900') >>> st.write('The current color is', color) """ # 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.label = label color_picker_proto.default = str(value) ui_value = register_widget("color_picker", color_picker_proto, user_key=key) current_value = ui_value if ui_value is not None else value return self.dg._enqueue("color_picker", color_picker_proto, str(current_value))
def create_instance( self, *args, default: Any = None, key: Optional[str] = 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". **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 from streamlit.elements import arrow_table except ImportError: import sys if sys.version_info >= (3, 9): raise StreamlitAPIException( """To use Custom Components in Streamlit, you need to install PyArrow. Unfortunately, PyArrow does not yet support Python 3.9. You can either switch to Python 3.8 with an environment manager like PyEnv, or stay on 3.9 by [installing Streamlit with conda](https://discuss.streamlit.io/t/note-installation-issues-with-python-3-9-and-streamlit/6946): `conda install -c conda-forge streamlit` """) else: 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 Sharing, add "pyarrow" to your requirements.txt.""" ) # 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 type_util.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 type_util.is_dataframe_like(arg_val): dataframe_arg = SpecialArg() dataframe_arg.key = arg_name arrow_table.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 BaseException as e: raise MarshallComponentException( "Could not convert component args to JSON", e) def marshall_component(element: Element) -> Union[Any, Type[NoValue]]: element.component_instance.component_name = self.name 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 the entirety of its protobuf. 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. # # So: if `key` is None, we marshall the element's arguments # *before* computing its widget_ui_value (which creates its hash). # If `key` is not None, we marshall the arguments *after*. def marshall_element_args(): element.component_instance.json_args = serialized_json_args element.component_instance.special_args.extend(special_args) if key is None: marshall_element_args() widget_value = register_widget( element_type="component_instance", element_proto=element.component_instance, user_key=key, widget_func_name=self.name, ) if key is not None: marshall_element_args() if widget_value is None: widget_value = default elif isinstance(widget_value, ArrowTableProto): widget_value = arrow_table.arrow_proto_to_dataframe( widget_value) # widget_value will be either None or whatever the component's most # recent setWidgetValue value is. We coerce None -> NoValue, # because that's what DeltaGenerator._enqueue expects. return widget_value if widget_value is not None else NoValue # We currently only support writing to st._main, but this will change # when we settle on an improved API in a post-layout world. element = Element() return_value = marshall_component(element) result = streamlit._main._enqueue("component_instance", element.component_instance, return_value) return result
def _color_picker( self, label: str, value: Optional[str] = None, key: Optional[Key] = None, help: Optional[str] = None, on_change: Optional[WidgetCallback] = None, args: Optional[WidgetArgs] = None, kwargs: Optional[WidgetKwargs] = None, *, # keyword-only arguments: disabled: bool = False, ctx: Optional[ScriptRunContext] = None, ) -> str: key = to_key(key) check_callback_rules(self.dg, on_change) check_session_state_rules(default_value=value, key=key) # 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.label = label color_picker_proto.default = str(value) color_picker_proto.form_id = current_form_id(self.dg) color_picker_proto.disabled = disabled if help is not None: color_picker_proto.help = dedent(help) def deserialize_color_picker(ui_value: Optional[str], widget_id: str = "") -> str: return str(ui_value if ui_value is not None else value) current_value, set_frontend_value = register_widget( "color_picker", color_picker_proto, user_key=key, on_change_handler=on_change, args=args, kwargs=kwargs, deserializer=deserialize_color_picker, serializer=str, ctx=ctx, ) if set_frontend_value: color_picker_proto.value = current_value color_picker_proto.set_value = True self.dg._enqueue("color_picker", color_picker_proto) return cast(str, current_value)
def _radio( self, label: str, options: OptionSequence, index: int = 0, format_func: Callable[[Any], Any] = str, key: Optional[Key] = None, help: Optional[str] = None, on_change: Optional[WidgetCallback] = None, args: Optional[WidgetArgs] = None, kwargs: Optional[WidgetKwargs] = None, *, # keyword-only args: disabled: bool = False, ctx: Optional[ScriptRunContext], ) -> Any: key = to_key(key) check_callback_rules(self.dg, on_change) check_session_state_rules(default_value=None if index == 0 else index, key=key) opt = ensure_indexable(options) if not isinstance(index, int): raise StreamlitAPIException("Radio Value has invalid type: %s" % type(index).__name__) if len(opt) > 0 and not 0 <= index < len(opt): raise StreamlitAPIException( "Radio index must be between 0 and length of options") radio_proto = RadioProto() radio_proto.label = label radio_proto.default = index radio_proto.options[:] = [str(format_func(option)) for option in opt] radio_proto.form_id = current_form_id(self.dg) radio_proto.disabled = disabled if help is not None: radio_proto.help = dedent(help) def deserialize_radio(ui_value, widget_id=""): idx = ui_value if ui_value is not None else index return opt[idx] if len(opt) > 0 and opt[idx] is not None else None def serialize_radio(v): if len(options) == 0: return 0 return index_(options, v) current_value, set_frontend_value = register_widget( "radio", radio_proto, user_key=key, on_change_handler=on_change, args=args, kwargs=kwargs, deserializer=deserialize_radio, serializer=serialize_radio, ctx=ctx, ) if set_frontend_value: radio_proto.value = serialize_radio(current_value) radio_proto.set_value = True self.dg._enqueue("radio", radio_proto) return cast(str, current_value)
def number_input( self, label, min_value=None, max_value=None, value=NoValue(), step=None, format=None, key=None, help=None, ): """Display a numeric input widget. Parameters ---------- label : str A short label explaining to the user what this input is for. min_value : int or float or None The minimum permitted value. If None, there will be no minimum. max_value : int or float or None The maximum permitted value. If None, there will be no maximum. value : int or float or None The value of this widget when it first renders. Defaults to min_value, or 0.0 if min_value is None step : int or 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. Output must be purely numeric. This does not impact the return value. Valid formatters: %d %e %f %g %i %u key : str An optional string 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. Multiple widgets of the same type may not share the same key. help : str A tooltip that gets displayed next to the input. Returns ------- int or float The current value of the numeric input widget. The return type will match the data type of the value parameter. Example ------- >>> number = st.number_input('Insert a number') >>> st.write('The current number is ', number) """ # Ensure that all arguments are of the same type. args = [min_value, max_value, value, step] int_args = all( isinstance(a, (numbers.Integral, type(None), NoValue)) for a in args) float_args = all( isinstance(a, (float, type(None), NoValue)) for a in args) if not int_args and not float_args: raise StreamlitAPIException( "All numerical arguments must be of the same type." f"\n`value` has {type(value).__name__} type." f"\n`min_value` has {type(min_value).__name__} type." f"\n`max_value` has {type(max_value).__name__} type." f"\n`step` has {type(step).__name__} type.") if isinstance(value, NoValue): if min_value is not None: value = min_value elif int_args and float_args: value = 0.0 # if no values are provided, defaults to float elif int_args: value = 0 else: value = 0.0 int_value = isinstance(value, numbers.Integral) float_value = isinstance(value, float) if value is None: raise StreamlitAPIException( "Default value for number_input should be an int or a float.") else: 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 StreamlitAPIException( "Format string for st.number_input contains invalid characters: %s" % format) # Ensure that the value matches arguments' types. all_ints = int_value and int_args if (min_value and min_value > value) or (max_value and max_value < value): raise StreamlitAPIException( "The default `value` of %(value)s " "must lie between the `min_value` of %(min)s " "and the `max_value` of %(max)s, inclusively." % { "value": value, "min": min_value, "max": 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(min_value, "`min_value`") if max_value is not None: JSNumber.validate_int_bounds(max_value, "`max_value`") if step is not None: JSNumber.validate_int_bounds(step, "`step`") JSNumber.validate_int_bounds(value, "`value`") else: if min_value is not None: JSNumber.validate_float_bounds(min_value, "`min_value`") if max_value is not None: JSNumber.validate_float_bounds(max_value, "`max_value`") if step is not None: JSNumber.validate_float_bounds(step, "`step`") JSNumber.validate_float_bounds(value, "`value`") except JSNumberBoundsException as e: raise StreamlitAPIException(str(e)) number_input_proto = NumberInputProto() number_input_proto.data_type = (NumberInputProto.INT if all_ints else NumberInputProto.FLOAT) number_input_proto.label = label number_input_proto.default = value number_input_proto.form_id = current_form_id(self.dg) if help is not None: number_input_proto.help = 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 ui_value = register_widget("number_input", number_input_proto, user_key=key) return_value = ui_value if ui_value is not None else value return self.dg._enqueue("number_input", number_input_proto, return_value)
def download_button( self, label: str, data: DownloadButtonDataType, file_name: Optional[str] = None, mime: Optional[str] = None, key: Optional[Key] = None, help: Optional[str] = None, on_click: Optional[WidgetCallback] = None, args: Optional[WidgetArgs] = None, kwargs: Optional[WidgetKwargs] = None, ) -> bool: """Display a download button widget. Download button has a few constraints: - Download button is designed to download data that is stored in the Streamlit server's memory and works best when file size is reasonably small, <50MB. - For large file sizes, it is recommended to use a third party cloud based object storage solution. - We recommend doing any file transformation operations outside the download button declaration. Caching such transformations also prevents from slowing down the app on every rerun. See the examples below to learn more. Parameters ---------- label : str A short label explaining to what this button is for. data : str or bytes or file The contents of the file to be downloaded. file_name: str An optional string to use as the name of the file to be downloaded, eg. 'my_file.csv'. If file name is not specified, then we provide a generic name for the download. mime : str or None The MIME type of the data. If None, defaults to "text/plain" or "application/octet-stream" depending on the data type. 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. Multiple widgets of the same type may not share the same key. help : str An optional tooltip that gets displayed when the button is hovered over. 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. Returns ------- bool If the button was clicked on the last run of the app. Examples -------- While download button can be used to download arbitrary files, here are some common use-cases to get you started. Download a large DataFrame: >>> @st.cache ... def convert_df(df): ... # Cache the conversion to prevent computation on every rerun ... return df.to_csv().encode('utf-8') >>> csv = convert_df(my_large_df) >>> st.download_button( ... label="Press to Download", ... data=csv, ... file_name='large_df.csv', ... mime='text/csv', ... ) Download a CSV file: >>> text_contents = ''' ... Col1, Col2 ... 123, 456 ... 789, 000 ... ''' >>> st.download_button( ... label='Download CSV', data=text_contents, ... file_name='file.csv', mime='text/csv' ... ) Download a binary file: >>> binary_contents = b'example content' ... # Defaults to 'application/octet-stream' >>> st.download_button('Download binary file', binary_contents) Download an image: >>> with open("flower.png", "rb") as file: ... btn = st.download_button( ... label="Download Image", ... data=file, ... file_name="flower.png", ... mime="image/png" ... ) """ key = to_key(key) check_session_state_rules(default_value=None, key=key, writes_allowed=False) if is_in_form(self.dg): raise StreamlitAPIException( f"`st.download_button()` can't be used in an `st.form()`.{FORM_DOCS_INFO}" ) download_button_proto = DownloadButtonProto() download_button_proto.label = label download_button_proto.default = False marshall_file(self.dg._get_delta_path_str(), data, download_button_proto, mime, file_name) if help is not None: download_button_proto.help = dedent(help) def deserialize_button(ui_value, widget_id=""): return ui_value or False current_value, _ = register_widget( "download_button", download_button_proto, user_key=key, on_change_handler=on_click, args=args, kwargs=kwargs, deserializer=deserialize_button, serializer=bool, ) self.dg._enqueue("download_button", download_button_proto) return cast(bool, current_value)
def _slider( self, label: str, min_value=None, max_value=None, value=None, step=None, format=None, key: Optional[Key] = None, help: Optional[str] = None, on_change: Optional[WidgetCallback] = None, args: Optional[WidgetArgs] = None, kwargs: Optional[WidgetKwargs] = None, *, # keyword-only arguments: disabled: bool = False, ctx: Optional[ScriptRunContext] = None, ): key = to_key(key) check_callback_rules(self.dg, on_change) check_session_state_rules(default_value=value, key=key) # Set value default. if value is None: value = min_value if min_value is not None else 0 SUPPORTED_TYPES = { int: SliderProto.INT, float: SliderProto.FLOAT, datetime: SliderProto.DATETIME, date: SliderProto.DATE, time: SliderProto.TIME, } TIMELIKE_TYPES = (SliderProto.DATETIME, SliderProto.TIME, SliderProto.DATE) # Ensure that the value is either a single value or a range of values. single_value = isinstance(value, tuple(SUPPORTED_TYPES.keys())) range_value = isinstance(value, (list, tuple)) and len(value) in (0, 1, 2) if not single_value and not range_value: raise StreamlitAPIException( "Slider value should either be an int/float/datetime or a list/tuple of " "0 to 2 ints/floats/datetimes") # Simplify future logic by always making value a list if single_value: value = [value] def all_same_type(items): return len(set(map(type, items))) < 2 if not all_same_type(value): raise StreamlitAPIException( "Slider tuple/list components must be of the same type.\n" f"But were: {list(map(type, value))}") if len(value) == 0: data_type = SliderProto.INT else: data_type = SUPPORTED_TYPES[type(value[0])] datetime_min = time.min datetime_max = time.max if data_type == SliderProto.TIME: datetime_min = time.min.replace(tzinfo=value[0].tzinfo) datetime_max = time.max.replace(tzinfo=value[0].tzinfo) if data_type in (SliderProto.DATETIME, SliderProto.DATE): datetime_min = value[0] - timedelta(days=14) datetime_max = value[0] + timedelta(days=14) DEFAULTS = { SliderProto.INT: { "min_value": 0, "max_value": 100, "step": 1, "format": "%d", }, SliderProto.FLOAT: { "min_value": 0.0, "max_value": 1.0, "step": 0.01, "format": "%0.2f", }, SliderProto.DATETIME: { "min_value": datetime_min, "max_value": datetime_max, "step": timedelta(days=1), "format": "YYYY-MM-DD", }, SliderProto.DATE: { "min_value": datetime_min, "max_value": datetime_max, "step": timedelta(days=1), "format": "YYYY-MM-DD", }, SliderProto.TIME: { "min_value": datetime_min, "max_value": datetime_max, "step": timedelta(minutes=15), "format": "HH:mm", }, } if min_value is None: min_value = DEFAULTS[data_type]["min_value"] if max_value is None: max_value = DEFAULTS[data_type]["max_value"] if step is None: step = DEFAULTS[data_type]["step"] if (data_type in ( SliderProto.DATETIME, SliderProto.DATE, ) and max_value - min_value < timedelta(days=1)): step = timedelta(minutes=15) if format is None: format = DEFAULTS[data_type]["format"] if step == 0: raise StreamlitAPIException( "Slider components cannot be passed a `step` of 0.") # Ensure that all arguments are of the same type. slider_args = [min_value, max_value, step] int_args = all(map(lambda a: isinstance(a, int), slider_args)) float_args = all(map(lambda a: isinstance(a, float), slider_args)) # When min and max_value are the same timelike, step should be a timedelta timelike_args = (data_type in TIMELIKE_TYPES and isinstance(step, timedelta) and type(min_value) == type(max_value)) if not int_args and not float_args and not timelike_args: raise StreamlitAPIException( "Slider value arguments must be of matching types." "\n`min_value` has %(min_type)s type." "\n`max_value` has %(max_type)s type." "\n`step` has %(step)s type." % { "min_type": type(min_value).__name__, "max_type": type(max_value).__name__, "step": type(step).__name__, }) # Ensure that the value matches arguments' types. all_ints = data_type == SliderProto.INT and int_args all_floats = data_type == SliderProto.FLOAT and float_args all_timelikes = data_type in TIMELIKE_TYPES and timelike_args if not all_ints and not all_floats and not all_timelikes: raise StreamlitAPIException( "Both value and arguments must be of the same type." "\n`value` has %(value_type)s type." "\n`min_value` has %(min_type)s type." "\n`max_value` has %(max_type)s type." % { "value_type": type(value).__name__, "min_type": type(min_value).__name__, "max_type": type(max_value).__name__, }) # Ensure that min <= value(s) <= max, adjusting the bounds as necessary. min_value = min(min_value, max_value) max_value = max(min_value, max_value) if len(value) == 1: min_value = min(value[0], min_value) max_value = max(value[0], max_value) elif len(value) == 2: start, end = value if start > end: # Swap start and end, since they seem reversed start, end = end, start value = start, end min_value = min(start, min_value) max_value = max(end, max_value) else: # Empty list, so let's just use the outer bounds value = [min_value, max_value] # Bounds checks. JSNumber produces human-readable exceptions that # we simply re-package as StreamlitAPIExceptions. # (We check `min_value` and `max_value` here; `value` and `step` are # already known to be in the [min_value, max_value] range.) try: if all_ints: JSNumber.validate_int_bounds(min_value, "`min_value`") JSNumber.validate_int_bounds(max_value, "`max_value`") elif all_floats: JSNumber.validate_float_bounds(min_value, "`min_value`") JSNumber.validate_float_bounds(max_value, "`max_value`") elif all_timelikes: # No validation yet. TODO: check between 0001-01-01 to 9999-12-31 pass except JSNumberBoundsException as e: raise StreamlitAPIException(str(e)) # Convert dates or times into datetimes if data_type == SliderProto.TIME: def _time_to_datetime(time): # Note, here we pick an arbitrary date well after Unix epoch. # This prevents pre-epoch timezone issues (https://bugs.python.org/issue36759) # We're dropping the date from datetime laters, anyways. return datetime.combine(date(2000, 1, 1), time) value = list(map(_time_to_datetime, value)) min_value = _time_to_datetime(min_value) max_value = _time_to_datetime(max_value) if data_type == SliderProto.DATE: def _date_to_datetime(date): return datetime.combine(date, time()) value = list(map(_date_to_datetime, value)) min_value = _date_to_datetime(min_value) max_value = _date_to_datetime(max_value) # Now, convert to microseconds (so we can serialize datetime to a long) if data_type in TIMELIKE_TYPES: SECONDS_TO_MICROS = 1000 * 1000 DAYS_TO_MICROS = 24 * 60 * 60 * SECONDS_TO_MICROS def _delta_to_micros(delta): return (delta.microseconds + delta.seconds * SECONDS_TO_MICROS + delta.days * DAYS_TO_MICROS) UTC_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc) def _datetime_to_micros(dt): # The frontend is not aware of timezones and only expects a UTC-based timestamp (in microseconds). # Since we want to show the date/time exactly as it is in the given datetime object, # we just set the tzinfo to UTC and do not do any timezone conversions. # Only the backend knows about original timezone and will replace the UTC timestamp in the deserialization. utc_dt = dt.replace(tzinfo=timezone.utc) return _delta_to_micros(utc_dt - UTC_EPOCH) # Restore times/datetimes to original timezone (dates are always naive) orig_tz = (value[0].tzinfo if data_type in (SliderProto.TIME, SliderProto.DATETIME) else None) def _micros_to_datetime(micros): utc_dt = UTC_EPOCH + timedelta(microseconds=micros) # Add the original timezone. No conversion is required here, # since in the serialization, we also just replace the timestamp with UTC. return utc_dt.replace(tzinfo=orig_tz) value = list(map(_datetime_to_micros, value)) min_value = _datetime_to_micros(min_value) max_value = _datetime_to_micros(max_value) step = _delta_to_micros(step) # It would be great if we could guess the number of decimal places from # the `step` argument, but this would only be meaningful if step were a # decimal. As a possible improvement we could make this function accept # decimals and/or use some heuristics for floats. slider_proto = SliderProto() slider_proto.label = label slider_proto.format = format slider_proto.default[:] = value slider_proto.min = min_value slider_proto.max = max_value slider_proto.step = step slider_proto.data_type = data_type slider_proto.options[:] = [] slider_proto.form_id = current_form_id(self.dg) if help is not None: slider_proto.help = dedent(help) def deserialize_slider(ui_value: Optional[List[float]], widget_id=""): if ui_value is not None: val = ui_value else: # Widget has not been used; fallback to the original value, val = cast(List[float], value) # The widget always returns a float array, so fix the return type if necessary if data_type == SliderProto.INT: val = [int(v) for v in val] if data_type == SliderProto.DATETIME: val = [_micros_to_datetime(int(v)) for v in val] if data_type == SliderProto.DATE: val = [_micros_to_datetime(int(v)).date() for v in val] if data_type == SliderProto.TIME: val = [ _micros_to_datetime(int(v)).time().replace(tzinfo=orig_tz) for v in val ] return val[0] if single_value else tuple(val) def serialize_slider(v: Any) -> List[Any]: range_value = isinstance(v, (list, tuple)) value = list(v) if range_value else [v] if data_type == SliderProto.DATE: value = [ _datetime_to_micros(_date_to_datetime(v)) for v in value ] if data_type == SliderProto.TIME: value = [ _datetime_to_micros(_time_to_datetime(v)) for v in value ] if data_type == SliderProto.DATETIME: value = [_datetime_to_micros(v) for v in value] return value current_value, set_frontend_value = register_widget( "slider", slider_proto, user_key=key, on_change_handler=on_change, args=args, kwargs=kwargs, deserializer=deserialize_slider, serializer=serialize_slider, ctx=ctx, ) # This needs to be done after register_widget because we don't want # the following proto fields to affect a widget's ID. slider_proto.disabled = disabled if set_frontend_value: slider_proto.value[:] = serialize_slider(current_value) slider_proto.set_value = True self.dg._enqueue("slider", slider_proto) return current_value
def image( dg, image, caption=None, width=None, use_column_width=False, clamp=False, channels="RGB", output_format="auto", **kwargs, ): """Display an image or list of images. Parameters ---------- image : numpy.ndarray, [numpy.ndarray], BytesIO, str, or [str] Monochrome image of shape (w,h) or (w,h,1) OR a color image of shape (w,h,3) OR an RGBA image of shape (w,h,4) OR a URL to fetch the image from OR an SVG XML string like `<svg xmlns=...</svg>` OR a list of one of the above, to display multiple images. caption : str or list of str Image caption. If displaying multiple images, caption should be a list of captions (one for each image). width : int or None Image width. None means use the image width. Should be set for SVG images, as they have no default image width. use_column_width : bool If True, set the image width to the column width. This takes precedence over the `width` parameter. clamp : bool 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 : 'RGB' or 'BGR' 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 : 'JPEG', 'PNG', or 'auto' 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. Example ------- >>> from PIL import Image >>> image = Image.open('sunrise.jpg') >>> >>> st.image(image, caption='Sunrise by the mountains', ... use_column_width=True) .. output:: https://share.streamlit.io/0.61.0-yRE1/index.html?id=Sn228UQxBfKoE5C7A7Y2Qk height: 630px """ format = kwargs.get("format") if format != None: # override output compression type if specified output_format = format if config.get_option("deprecation.showImageFormat"): dg.exception(ImageFormatWarning(format)) # type: ignore if use_column_width: width = -2 elif width is None: width = -1 elif width <= 0: raise StreamlitAPIException("Image width must be positive.") image_list_proto = ImageListProto() marshall_images( dg._get_coordinates(), # type: ignore image, caption, width, image_list_proto, clamp, channels, output_format, ) return dg._enqueue("imgs", image_list_proto) # type: ignore
def image_to_url(image, width, clamp, channels, output_format, image_id, allow_emoji=False): # PIL Images if isinstance(image, ImageFile.ImageFile) or isinstance( image, Image.Image): format = _format_from_image_type(image, output_format) 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 type(image) is io.BytesIO: data = _BytesIO_to_bytes(image) # Numpy Arrays (ie opencv) elif type(image) is np.ndarray: data = _verify_np_shape(image) data = _clip_image(data, clamp) if channels == "BGR": if len(data.shape) == 3: data = data[:, :, [2, 1, 0]] else: raise StreamlitAPIException( 'When using `channels="BGR"`, the input image should ' "have exactly 3 color channels") data = _np_array_to_bytes(data, output_format=output_format) # Strings elif isinstance(image, str): # If it's a url, then set the protobuf and continue try: p = urlparse(image) if p.scheme: return image except UnicodeDecodeError: pass # Unpack local SVG image file to an SVG string if image.endswith(".svg"): with open(image) as textfile: image = textfile.read() # If it's an SVG string, then format and return an SVG data url if image.startswith("<svg") or image.strip().startswith("<svg"): return f"data:image/svg+xml,{image}" # Finally, see if it's a file. try: with open(image, "rb") as f: data = f.read() except: if allow_emoji: # This might be an emoji string, so just pass it to the frontend return image else: # Allow OS filesystem errors to raise raise # Assume input in bytes. else: data = image (data, mimetype) = _normalize_to_bytes(data, width, output_format) this_file = media_file_manager.add(data, mimetype, image_id) return this_file.url
def radio( self, label: str, options: OptionSequence, index: int = 0, format_func: Callable[[Any], str] = str, key: Optional[Key] = None, help: Optional[str] = None, on_change: Optional[WidgetCallback] = None, args: Optional[WidgetArgs] = None, kwargs: Optional[WidgetKwargs] = None, ) -> Any: """Display a radio button widget. Parameters ---------- label : str A short label explaining to the user what this radio group is for. options : Sequence, numpy.ndarray, pandas.Series, pandas.DataFrame, or pandas.Index Labels for the radio options. This will be cast to str internally by default. For pandas.DataFrame, the first column is selected. index : int The index of the preselected option on first render. format_func : function Function to modify the display of radio options. It receives the raw option as an argument and should output the label to be shown for that option. This has no impact on the return value of the radio. key : str or int An optional string or integer to use as the unique key for the widget. If this is omitted, a key will be generated for the widget based on its content. Multiple widgets of the same type may not share the same key. help : str An optional tooltip that gets displayed next to the radio. on_change : callable An optional callback invoked when this radio's value changes. args : tuple An optional tuple of args to pass to the callback. kwargs : dict An optional dict of kwargs to pass to the callback. Returns ------- any The selected option. Example ------- >>> genre = st.radio( ... "What\'s your favorite movie genre", ... ('Comedy', 'Drama', 'Documentary')) >>> >>> if genre == 'Comedy': ... st.write('You selected comedy.') ... else: ... st.write("You didn\'t select comedy.") """ key = to_key(key) check_callback_rules(self.dg, on_change) check_session_state_rules(default_value=None if index == 0 else index, key=key) opt = ensure_indexable(options) if not isinstance(index, int): raise StreamlitAPIException("Radio Value has invalid type: %s" % type(index).__name__) if len(opt) > 0 and not 0 <= index < len(opt): raise StreamlitAPIException( "Radio index must be between 0 and length of options") radio_proto = RadioProto() radio_proto.label = label radio_proto.default = index radio_proto.options[:] = [str(format_func(option)) for option in opt] radio_proto.form_id = current_form_id(self.dg) if help is not None: radio_proto.help = dedent(help) def deserialize_radio(ui_value, widget_id=""): idx = ui_value if ui_value is not None else index return opt[idx] if len(opt) > 0 and opt[idx] is not None else None def serialize_radio(v): if len(options) == 0: return 0 return index_(options, v) current_value, set_frontend_value = register_widget( "radio", radio_proto, user_key=key, on_change_handler=on_change, args=args, kwargs=kwargs, deserializer=deserialize_radio, serializer=serialize_radio, ) if set_frontend_value: radio_proto.value = serialize_radio(current_value) radio_proto.set_value = True self.dg._enqueue("radio", radio_proto) return cast(str, current_value)