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