def marshall_component(dg, element: Element) -> Union[Any, Type[NoValue]]: 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 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() def deserialize_component(ui_value, widget_id=""): # ui_value is an object from json, an ArrowTable proto, or a bytearray return ui_value ctx = get_script_run_ctx() widget_value, _ = register_widget( element_type="component_instance", element_proto=element.component_instance, user_key=key, widget_func_name=self.name, deserializer=deserialize_component, serializer=lambda x: x, ctx=ctx, ) 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) # 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
def _button( self, label: str, key: Optional[str], help: Optional[str], is_form_submitter: bool, on_click: Optional[WidgetCallback] = None, args: Optional[WidgetArgs] = None, kwargs: Optional[WidgetKwargs] = None, *, # keyword-only arguments: disabled: bool = False, ctx: Optional[ScriptRunContext] = None, ) -> bool: if not is_form_submitter: check_callback_rules(self.dg, on_click) check_session_state_rules(default_value=None, key=key, writes_allowed=False) # 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 script_run_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.form_submit_button()` must be used inside an `st.form()`.{FORM_DOCS_INFO}" ) button_proto = ButtonProto() 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) button_proto.disabled = disabled if help is not None: button_proto.help = dedent(help) def deserialize_button(ui_value: bool, widget_id: str = "") -> bool: return ui_value or False current_value, _ = register_widget( "button", button_proto, user_key=key, on_change_handler=on_click, args=args, kwargs=kwargs, deserializer=deserialize_button, serializer=bool, ctx=ctx, ) self.dg._enqueue("button", button_proto) return cast(bool, current_value)
def _text_area( self, label: str, value: str = "", height: Optional[int] = None, max_chars: Optional[int] = 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: placeholder: Optional[str] = None, 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=None if value == "" else value, key=key) text_area_proto = TextAreaProto() text_area_proto.label = label text_area_proto.default = str(value) text_area_proto.form_id = current_form_id(self.dg) text_area_proto.disabled = disabled if help is not None: text_area_proto.help = dedent(help) if height is not None: text_area_proto.height = height if max_chars is not None: text_area_proto.max_chars = max_chars if placeholder is not None: text_area_proto.placeholder = str(placeholder) def deserialize_text_area(ui_value, widget_id="") -> str: return str(ui_value if ui_value is not None else value) current_value, set_frontend_value = register_widget( "text_area", text_area_proto, user_key=key, on_change_handler=on_change, args=args, kwargs=kwargs, deserializer=deserialize_text_area, serializer=lambda x: x, ctx=ctx, ) if set_frontend_value: text_area_proto.value = current_value text_area_proto.set_value = True self.dg._enqueue("text_area", text_area_proto) return cast(str, current_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, *, # keyword-only arguments: disabled: bool = False, ctx: Optional[ScriptRunContext] = None, ) -> bool: 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, 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. download_button_proto.disabled = disabled self.dg._enqueue("download_button", download_button_proto) return cast(bool, current_value)
def _checkbox( self, label: str, value: bool = False, 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, ) -> bool: key = to_key(key) check_callback_rules(self.dg, on_change) check_session_state_rules( default_value=None if value is False else value, key=key) checkbox_proto = CheckboxProto() checkbox_proto.label = label checkbox_proto.default = bool(value) checkbox_proto.form_id = current_form_id(self.dg) if help is not None: checkbox_proto.help = dedent(help) def deserialize_checkbox(ui_value: Optional[bool], widget_id: str = "") -> bool: return bool(ui_value if ui_value is not None else value) current_value, set_frontend_value = register_widget( "checkbox", checkbox_proto, user_key=key, on_change_handler=on_change, args=args, kwargs=kwargs, deserializer=deserialize_checkbox, serializer=bool, 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. checkbox_proto.disabled = disabled if set_frontend_value: checkbox_proto.value = current_value checkbox_proto.set_value = True self.dg._enqueue("checkbox", checkbox_proto) return cast(bool, 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 _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 _camera_input( self, label: str, 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, ) -> SomeUploadedSnapshotFile: key = to_key(key) check_callback_rules(self.dg, on_change) check_session_state_rules(default_value=None, key=key, writes_allowed=False) camera_input_proto = CameraInputProto() camera_input_proto.label = label camera_input_proto.form_id = current_form_id(self.dg) if help is not None: camera_input_proto.help = dedent(help) def serialize_camera_image_input( snapshot: SomeUploadedSnapshotFile, ) -> FileUploaderStateProto: state_proto = FileUploaderStateProto() ctx = get_script_run_ctx() if ctx is None: return state_proto # ctx.uploaded_file_mgr._file_id_counter stores the id to use for # the *next* uploaded file, so the current highest file id is the # counter minus 1. state_proto.max_file_id = ctx.uploaded_file_mgr._file_id_counter - 1 if not snapshot: return state_proto file_info: UploadedFileInfoProto = state_proto.uploaded_file_info.add( ) file_info.id = snapshot.id file_info.name = snapshot.name file_info.size = snapshot.size return state_proto def deserialize_camera_image_input( ui_value: Optional[FileUploaderStateProto], widget_id: str) -> SomeUploadedSnapshotFile: file_recs = self._get_file_recs_for_camera_input_widget( widget_id, ui_value) if len(file_recs) == 0: return_value = None else: return_value = UploadedFile(file_recs[0]) return return_value widget_value, _ = register_widget( "camera_input", camera_input_proto, user_key=key, on_change_handler=on_change, args=args, kwargs=kwargs, deserializer=deserialize_camera_image_input, serializer=serialize_camera_image_input, 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. camera_input_proto.disabled = disabled ctx = get_script_run_ctx() camera_image_input_state = serialize_camera_image_input(widget_value) uploaded_shapshot_info = camera_image_input_state.uploaded_file_info if ctx is not None and len(uploaded_shapshot_info) != 0: newest_file_id = camera_image_input_state.max_file_id active_file_ids = [f.id for f in uploaded_shapshot_info] ctx.uploaded_file_mgr.remove_orphaned_files( session_id=ctx.session_id, widget_id=camera_input_proto.id, newest_file_id=newest_file_id, active_file_ids=active_file_ids, ) self.dg._enqueue("camera_input", camera_input_proto) return cast(SomeUploadedSnapshotFile, widget_value)
def _select_slider( self, label: str, options: OptionSequence = [], value: Any = None, 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, disabled: bool = False, ctx: Optional[ScriptRunContext] = None, ) -> Any: key = to_key(key) check_callback_rules(self.dg, on_change) check_session_state_rules(default_value=value, key=key) opt = ensure_indexable(options) if len(opt) == 0: raise StreamlitAPIException("The `options` argument needs to be non-empty") is_range_value = isinstance(value, (list, tuple)) def as_index_list(v): is_range_value = isinstance(v, (list, tuple)) if is_range_value: slider_value = [index_(opt, val) for val in v] start, end = slider_value if start > end: slider_value = [end, start] return slider_value else: # Simplify future logic by always making value a list try: return [index_(opt, v)] except ValueError: if value is not None: raise return [0] # Convert element to index of the elements slider_value = as_index_list(value) slider_proto = SliderProto() slider_proto.label = label slider_proto.format = "%s" slider_proto.default[:] = slider_value slider_proto.min = 0 slider_proto.max = len(opt) - 1 slider_proto.step = 1 # default for index changes slider_proto.data_type = SliderProto.INT slider_proto.options[:] = [str(format_func(option)) for option in opt] slider_proto.form_id = current_form_id(self.dg) if help is not None: slider_proto.help = dedent(help) def deserialize_select_slider(ui_value, widget_id=""): if not ui_value: # Widget has not been used; fallback to the original value, ui_value = slider_value # The widget always returns floats, so convert to ints before indexing return_value = list(map(lambda x: opt[int(x)], ui_value)) # type: ignore[no-any-return] # If the original value was a list/tuple, so will be the output (and vice versa) return tuple(return_value) if is_range_value else return_value[0] def serialize_select_slider(v): return as_index_list(v) current_value, set_frontend_value = register_widget( "slider", slider_proto, user_key=key, on_change_handler=on_change, args=args, kwargs=kwargs, deserializer=deserialize_select_slider, serializer=serialize_select_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_select_slider(current_value) slider_proto.set_value = True self.dg._enqueue("slider", slider_proto) return current_value
def _selectbox( 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 arguments: disabled: bool = False, ctx: Optional[ScriptRunContext] = None, ) -> 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( "Selectbox Value has invalid type: %s" % type(index).__name__) if len(opt) > 0 and not 0 <= index < len(opt): raise StreamlitAPIException( "Selectbox index must be between 0 and length of options") selectbox_proto = SelectboxProto() selectbox_proto.label = label selectbox_proto.default = index selectbox_proto.options[:] = [ str(format_func(option)) for option in opt ] selectbox_proto.form_id = current_form_id(self.dg) if help is not None: selectbox_proto.help = dedent(help) def deserialize_select_box(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_select_box(v): if len(opt) == 0: return 0 return index_(opt, v) current_value, set_frontend_value = register_widget( "selectbox", selectbox_proto, user_key=key, on_change_handler=on_change, args=args, kwargs=kwargs, deserializer=deserialize_select_box, serializer=serialize_select_box, 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. selectbox_proto.disabled = disabled if set_frontend_value: selectbox_proto.value = serialize_select_box(current_value) selectbox_proto.set_value = True self.dg._enqueue("selectbox", selectbox_proto) return cast(str, current_value)
def _date_input( self, label: str, value=None, min_value=None, max_value=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, ) -> Union[date, Tuple[date, ...]]: 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 = datetime.now().date() single_value = isinstance(value, (date, datetime)) range_value = isinstance(value, (list, tuple)) and len(value) in (0, 1, 2) if not single_value and not range_value: raise StreamlitAPIException( "DateInput value should either be an date/datetime or a list/tuple of " "0 - 2 date/datetime values") if single_value: value = [value] value = [v.date() if isinstance(v, datetime) else v for v in value] if isinstance(min_value, datetime): min_value = min_value.date() elif min_value is None: if value: min_value = value[0] - relativedelta.relativedelta(years=10) else: min_value = date.today() - relativedelta.relativedelta( years=10) if isinstance(max_value, datetime): max_value = max_value.date() elif max_value is None: if value: max_value = value[-1] + relativedelta.relativedelta(years=10) else: max_value = date.today() + relativedelta.relativedelta( years=10) if value: start_value = value[0] end_value = value[-1] if (start_value < min_value) or (end_value > max_value): raise StreamlitAPIException( f"The default `value` of {value} " f"must lie between the `min_value` of {min_value} " f"and the `max_value` of {max_value}, inclusively.") date_input_proto = DateInputProto() date_input_proto.is_range = range_value if help is not None: date_input_proto.help = dedent(help) date_input_proto.label = label date_input_proto.default[:] = [ date.strftime(v, "%Y/%m/%d") for v in value ] date_input_proto.min = date.strftime(min_value, "%Y/%m/%d") date_input_proto.max = date.strftime(max_value, "%Y/%m/%d") date_input_proto.form_id = current_form_id(self.dg) date_input_proto.disabled = disabled def deserialize_date_input(ui_value, widget_id=""): if ui_value is not None: return_value = [ datetime.strptime(v, "%Y/%m/%d").date() for v in ui_value ] else: return_value = value return return_value[0] if single_value else tuple(return_value) def serialize_date_input(v): range_value = isinstance(v, (list, tuple)) to_serialize = list(v) if range_value else [v] return [date.strftime(v, "%Y/%m/%d") for v in to_serialize] current_value, set_frontend_value = register_widget( "date_input", date_input_proto, user_key=key, on_change_handler=on_change, args=args, kwargs=kwargs, deserializer=deserialize_date_input, serializer=serialize_date_input, ctx=ctx, ) if set_frontend_value: date_input_proto.value[:] = serialize_date_input(current_value) date_input_proto.set_value = True self.dg._enqueue("date_input", date_input_proto) return cast(date, current_value)
def _time_input( self, label: str, value=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, ) -> time: 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 = datetime.now().time().replace(second=0, microsecond=0) # 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().replace(second=0, microsecond=0) time_input_proto = TimeInputProto() time_input_proto.label = label time_input_proto.default = time.strftime(value, "%H:%M") time_input_proto.form_id = current_form_id(self.dg) time_input_proto.disabled = disabled if help is not None: time_input_proto.help = dedent(help) def deserialize_time_input(ui_value, widget_id=""): return (datetime.strptime(ui_value, "%H:%M").time() if ui_value is not None else value) def serialize_time_input(v): if isinstance(v, datetime): v = v.time() return time.strftime(v, "%H:%M") current_value, set_frontend_value = register_widget( "time_input", time_input_proto, user_key=key, on_change_handler=on_change, args=args, kwargs=kwargs, deserializer=deserialize_time_input, serializer=serialize_time_input, ctx=ctx, ) if set_frontend_value: time_input_proto.value = serialize_time_input(current_value) time_input_proto.set_value = True self.dg._enqueue("time_input", time_input_proto) return cast(time, current_value)
def _file_uploader( self, label: str, type: Optional[Union[str, List[str]]] = None, accept_multiple_files: bool = False, 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=None, key=key, writes_allowed=False) if type: if isinstance(type, str): type = [type] # May need a regex or a library to validate file types are valid # extensions. type = [ file_type if file_type[0] == "." else f".{file_type}" for file_type in type ] file_uploader_proto = FileUploaderProto() 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 if help is not None: file_uploader_proto.help = dedent(help) def deserialize_file_uploader( ui_value: Optional[FileUploaderStateProto], widget_id: str) -> SomeUploadedFiles: file_recs = self._get_file_recs(widget_id, ui_value) if len(file_recs) == 0: return_value: Optional[Union[List[UploadedFile], UploadedFile]] = ( [] if accept_multiple_files else None) else: files = [UploadedFile(rec) for rec in file_recs] return_value = files if accept_multiple_files else files[0] return return_value def serialize_file_uploader( files: SomeUploadedFiles) -> FileUploaderStateProto: state_proto = FileUploaderStateProto() ctx = get_script_run_ctx() if ctx is None: return state_proto # ctx.uploaded_file_mgr._file_id_counter stores the id to use for # the *next* uploaded file, so the current highest file id is the # counter minus 1. state_proto.max_file_id = ctx.uploaded_file_mgr._file_id_counter - 1 if not files: return state_proto elif not isinstance(files, list): files = [files] for f in files: file_info: UploadedFileInfoProto = state_proto.uploaded_file_info.add( ) file_info.id = f.id file_info.name = f.name file_info.size = f.size return state_proto # FileUploader's widget value is a list of file IDs # representing the current set of files that this uploader should # know about. widget_value, _ = register_widget( "file_uploader", file_uploader_proto, user_key=key, on_change_handler=on_change, args=args, kwargs=kwargs, deserializer=deserialize_file_uploader, serializer=serialize_file_uploader, ctx=ctx, ) file_uploader_state = serialize_file_uploader(widget_value) uploaded_file_info = file_uploader_state.uploaded_file_info if ctx is not None and len(uploaded_file_info) != 0: newest_file_id = file_uploader_state.max_file_id active_file_ids = [f.id for f in uploaded_file_info] ctx.uploaded_file_mgr.remove_orphaned_files( session_id=ctx.session_id, widget_id=file_uploader_proto.id, newest_file_id=newest_file_id, active_file_ids=active_file_ids, ) self.dg._enqueue("file_uploader", file_uploader_proto) return cast(SomeUploadedFiles, widget_value)
def _text_input( self, label: str, value: str = "", max_chars: Optional[int] = None, key: Optional[Key] = None, type: str = "default", help: Optional[str] = None, autocomplete: Optional[str] = None, on_change: Optional[WidgetCallback] = None, args: Optional[WidgetArgs] = None, kwargs: Optional[WidgetKwargs] = None, *, # keyword-only arguments: placeholder: Optional[str] = None, 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=None if value == "" else value, key=key) text_input_proto = TextInputProto() text_input_proto.label = label text_input_proto.default = str(value) text_input_proto.form_id = current_form_id(self.dg) text_input_proto.disabled = disabled if help is not None: text_input_proto.help = dedent(help) if max_chars is not None: text_input_proto.max_chars = max_chars if placeholder is not None: text_input_proto.placeholder = str(placeholder) if type == "default": text_input_proto.type = TextInputProto.DEFAULT elif type == "password": text_input_proto.type = TextInputProto.PASSWORD else: raise StreamlitAPIException( "'%s' is not a valid text_input type. Valid types are 'default' and 'password'." % type ) # Marshall the autocomplete param. If unspecified, this will be # set to "new-password" for password inputs. if autocomplete is None: autocomplete = "new-password" if type == "password" else "" text_input_proto.autocomplete = autocomplete def deserialize_text_input(ui_value, widget_id="") -> str: return str(ui_value if ui_value is not None else value) current_value, set_frontend_value = register_widget( "text_input", text_input_proto, user_key=key, on_change_handler=on_change, args=args, kwargs=kwargs, deserializer=deserialize_text_input, serializer=lambda x: x, ctx=ctx, ) if set_frontend_value: text_input_proto.value = current_value text_input_proto.set_value = True self.dg._enqueue("text_input", text_input_proto) return cast(str, current_value)
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 _multiselect( self, label: str, options: OptionSequence, default: Optional[Any] = None, 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 arguments: disabled: bool = False, ctx: Optional[ScriptRunContext] = None, ) -> List[Any]: key = to_key(key) check_callback_rules(self.dg, on_change) check_session_state_rules(default_value=default, key=key) opt = ensure_indexable(options) # Perform validation checks and return indices base on the default values. 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] indices = _check_and_convert_to_indices(opt, default) multiselect_proto = MultiSelectProto() multiselect_proto.label = label default_value = [] if indices is None else indices multiselect_proto.default[:] = default_value multiselect_proto.options[:] = [ str(format_func(option)) for option in opt ] multiselect_proto.form_id = current_form_id(self.dg) multiselect_proto.disabled = disabled if help is not None: multiselect_proto.help = dedent(help) def deserialize_multiselect(ui_value: Optional[List[int]], widget_id: str = "") -> List[str]: current_value = ui_value if ui_value is not None else default_value return [opt[i] for i in current_value] def serialize_multiselect(value): return _check_and_convert_to_indices(opt, value) current_value, set_frontend_value = register_widget( "multiselect", multiselect_proto, user_key=key, on_change_handler=on_change, args=args, kwargs=kwargs, deserializer=deserialize_multiselect, serializer=serialize_multiselect, ctx=ctx, ) if set_frontend_value: multiselect_proto.value[:] = _check_and_convert_to_indices( opt, current_value) multiselect_proto.set_value = True self.dg._enqueue("multiselect", multiselect_proto) return cast(List[str], current_value)
def _number_input( self, label: str, min_value: Optional[Number] = None, max_value: Optional[Number] = None, value: Union[NoValue, Number, None] = NoValue(), step: Optional[Number] = None, format: 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, ) -> Number: key = to_key(key) check_callback_rules(self.dg, on_change) check_session_state_rules( default_value=None if isinstance(value, NoValue) else value, key=key) # Ensure that all arguments are of the same type. number_input_args = [min_value, max_value, value, step] int_args = all( isinstance(a, (numbers.Integral, type(None), NoValue)) for a in number_input_args) float_args = all( isinstance(a, (float, type(None), NoValue)) for a in number_input_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`") # type: ignore if max_value is not None: JSNumber.validate_int_bounds(max_value, "`max_value`") # type: ignore if step is not None: JSNumber.validate_int_bounds(step, "`step`") # type: ignore JSNumber.validate_int_bounds(value, "`value`") # type: ignore 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) number_input_proto.disabled = disabled 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 def deserialize_number_input(ui_value, widget_id=""): return ui_value if ui_value is not None else value current_value, set_frontend_value = register_widget( "number_input", number_input_proto, user_key=key, on_change_handler=on_change, args=args, kwargs=kwargs, deserializer=deserialize_number_input, serializer=lambda x: x, ctx=ctx, ) if set_frontend_value: number_input_proto.value = current_value number_input_proto.set_value = True self.dg._enqueue("number_input", number_input_proto) return cast(Number, current_value)