""" The input widgets generally allow entering arbitrary information into a text field or similar. """ import ast import json from base64 import b64decode from datetime import datetime, date import numpy as np import param from bokeh.models.formatters import TickFormatter from bokeh.models.widgets import ( CheckboxGroup as _BkCheckboxGroup, ColorPicker as _BkColorPicker, DatePicker as _BkDatePicker, Div as _BkDiv, TextInput as _BkTextInput, PasswordInput as _BkPasswordInput, Spinner as _BkSpinner, FileInput as _BkFileInput, TextAreaInput as _BkTextAreaInput, NumericInput as _BkNumericInput ) from ..config import config from ..layout import Column from ..util import param_reprs from .base import Widget, CompositeWidget from ..models import DatetimePicker as _bkDatetimePicker class TextInput(Widget): """ The `TextInput` widget allows entering any string using a text input box. Reference: https://panel.holoviz.org/reference/widgets/TextInput.html :Example: >>> TextInput(name='Name', placeholder='Enter your name here ...') """ max_length = param.Integer(default=5000, doc=""" Max count of characters in the input field.""") placeholder = param.String(default='', doc=""" Placeholder for empty input field.""") value = param.String(default='', allow_None=True, doc=""" Initial or entered text value updated when key is pressed.""") value_input = param.String(default='', allow_None=True, doc=""" Initial or entered text value updated on every key press.""") _widget_type = _BkTextInput class PasswordInput(TextInput): """ The `PasswordInput` allows entering any string using an obfuscated text input box. Reference: https://panel.holoviz.org/reference/widgets/PasswordInput.html :Example: >>> PasswordInput( ... name='Password', placeholder='Enter your password here...' ... ) """ _widget_type = _BkPasswordInput class TextAreaInput(TextInput): """ The `TextAreaInput` allows entering any multiline string using a text input box. Lines are joined with the newline character `\n`. Reference: https://panel.holoviz.org/reference/widgets/TextAreaInput.html :Example: >>> TextAreaInput( ... name='Description', placeholder='Enter your description here...' ... ) """ _widget_type = _BkTextAreaInput class FileInput(Widget): """ The `FileInput` allows the user to upload one or more files to the server. It makes the filename, MIME type and (bytes) content available in Python. Please note - you can in fact *drag and drop* files onto the `FileInput`. - you easily save the files using the `save` method. Reference: https://panel.holoviz.org/reference/widgets/FileInput.html :Example: >>> FileInput(accept='.png,.jpeg', multiple=True) """ accept = param.String(default=None) filename = param.ClassSelector(default=None, class_=(str, list), is_instance=True) mime_type = param.ClassSelector(default=None, class_=(str, list), is_instance=True) multiple = param.Boolean(default=False) value = param.Parameter(default=None) _widget_type = _BkFileInput _source_transforms = {'value': "'data:' + source.mime_type + ';base64,' + value"} _rename = {'name': None, 'filename': None} def _process_param_change(self, msg): msg = super()._process_param_change(msg) if 'value' in msg: msg.pop('value') if 'mime_type' in msg: msg.pop('mime_type') return msg def _filter_properties(self, properties): properties = super()._filter_properties(properties) return properties + ['value', 'mime_type', 'filename'] def _process_property_change(self, msg): msg = super()._process_property_change(msg) if 'value' in msg: if isinstance(msg['value'], str): msg['value'] = b64decode(msg['value']) else: msg['value'] = [b64decode(content) for content in msg['value']] return msg def save(self, filename): """ Saves the uploaded FileInput data object(s) to file(s) or BytesIO object(s). Arguments --------- filename (str or list[str]): File path or file-like object """ value = self.value if isinstance(filename, list) and not isinstance(value, list): raise TypeError( "FileInput contains a list of files but only a single " "filename was given. Please provide a list of filenames or " "file-like objects." ) elif not isinstance(filename, list) and isinstance(value, list): raise TypeError( "FileInput contains a single files but a list of " "filenames was given. Please provide a single filename " "or file-like object." ) elif not isinstance(value, list): value = [self.value] elif not isinstance(filename, list): filename = [filename] for val, fn in zip(value, filename): if isinstance(fn, str): with open(fn, 'wb') as f: f.write(val) else: fn.write(val) class StaticText(Widget): """ The `StaticText` widget displays a text value, but does not allow editing it. Reference: https://panel.holoviz.org/reference/widgets/StaticText.html :Example: >>> StaticText(name='Model', value='animagen2') """ style = param.Dict(default=None, doc=""" Dictionary of CSS property:value pairs to apply to this Div.""") value = param.Parameter(default=None, doc=""" The current value""") _format = '{title}: {value}' _rename = {'name': None, 'value': 'text'} _target_transforms = {'value': 'target.text.split(": ")[0]+": "+value'} _source_transforms = {'value': 'value.split(": ")[1]'} _widget_type = _BkDiv def _process_param_change(self, msg): msg = super()._process_property_change(msg) if 'value' in msg: text = str(msg.pop('value')) partial = self._format.replace('{value}', '').format(title=self.name) if self.name: text = self._format.format(title=self.name, value=text.replace(partial, '')) msg['text'] = text return msg class DatePicker(Widget): """ The `DatePicker` allows selecting selecting a `date` value using a text box and a date-picking utility. Reference: https://panel.holoviz.org/reference/widgets/DatePicker.html :Example: >>> DatePicker( ... value=date(2025,1,1), ... start=date(2025,1,1), end=date(2025,12,31), ... name='Date' ... ) """ value = param.CalendarDate(default=None, doc=""" The current value""") start = param.CalendarDate(default=None, doc=""" Inclusive lower bound of the allowed date selection""") end = param.CalendarDate(default=None, doc=""" Inclusive upper bound of the allowed date selection""") disabled_dates = param.List(default=None, class_=(date, str)) enabled_dates = param.List(default=None, class_=(date, str)) _source_transforms = {} _rename = {'start': 'min_date', 'end': 'max_date', 'name': 'title'} _widget_type = _BkDatePicker def _process_property_change(self, msg): msg = super()._process_property_change(msg) if 'value' in msg: if isinstance(msg['value'], str): msg['value'] = datetime.date(datetime.strptime(msg['value'], '%Y-%m-%d')) return msg class _DatetimePickerBase(Widget): disabled_dates = param.List(default=None, class_=(date, str), doc=""" Dates to make unavailable for selection.""") enabled_dates = param.List(default=None, class_=(date, str), doc=""" Dates to make available for selection.""") enable_time = param.Boolean(default=True, doc=""" Enable editing of the time in the widget.""") enable_seconds = param.Boolean(default=True, doc=""" Enable editing of the seconds in the widget.""") end = param.Date(default=None, doc=""" Inclusive upper bound of the allowed date selection. Note that while the parameter accepts datetimes the time portion of the range is ignored.""") military_time = param.Boolean(default=True, doc=""" Whether to display time in 24 hour format.""") start = param.Date(default=None, doc=""" Inclusive lower bound of the allowed date selection. Note that while the parameter accepts datetimes the time portion of the range is ignored.""") _source_transforms = {'value': None, 'start': None, 'end': None, 'mode': None} _rename = {'start': 'min_date', 'end': 'max_date', 'name': 'title'} _widget_type = _bkDatetimePicker __abstract = True def __init__(self, **params): super().__init__(**params) self._update_value_bounds() @staticmethod def _date_to_datetime(x): if isinstance(x, date): return datetime(x.year, x.month, x.day) @param.depends('start', 'end', watch=True) def _update_value_bounds(self): self.param.value.bounds = ( self._date_to_datetime(self.start), self._date_to_datetime(self.end), ) self.param.value._validate(self.value) def _process_property_change(self, msg): msg = super()._process_property_change(msg) if 'value' in msg: msg['value'] = self._serialize_value(msg['value']) return msg def _process_param_change(self, msg): msg = super()._process_param_change(msg) if 'value' in msg: msg['value'] = self._deserialize_value(msg['value']) return msg class DatetimePicker(_DatetimePickerBase): """ The `DatetimePicker` allows selecting selecting a `datetime` value using a textbox and a datetime-picking utility. Reference: https://panel.holoviz.org/reference/widgets/DatetimePicker.html :Example: >>> DatetimePicker( ... value=datetime(2025,1,1,22,0), ... start=date(2025,1,1), end=date(2025,12,31), ... military_time=True, name='Date and time' ... ) """ value = param.Date(default=None) mode = param.String('single', constant=True) def _serialize_value(self, value): if isinstance(value, str) and value: value = datetime.strptime(value, r'%Y-%m-%d %H:%M:%S') # Hour, minute and seconds can be increased after end is reached. # This forces the hours, minute and second to be 0. end = self._date_to_datetime(self.end) if end is not None and value > end: value = end return value def _deserialize_value(self, value): if isinstance(value, (datetime, date)): value = value.strftime(r'%Y-%m-%d %H:%M:%S') return value class DatetimeRangePicker(_DatetimePickerBase): """ The `DatetimeRangePicker` allows selecting selecting a `datetime` range using a text box and a datetime-range-picking utility. Reference: https://panel.holoviz.org/reference/widgets/DatetimeRangePicker.html :Example: >>> DatetimeRangePicker( ... value=(datetime(2025,1,1,22,0), datetime(2025,1,2,22,0)), ... start=date(2025,1,1), end=date(2025,12,31), ... military_time=True, name='Datetime Range' ... ) """ value = param.DateRange(default=None, doc=""" The current value""") mode = param.String('range', constant=True) def _serialize_value(self, value): if isinstance(value, str) and value: value = [ datetime.strptime(value, r'%Y-%m-%d %H:%M:%S') for value in value.split(' to ') ] # Hour, minute and seconds can be increased after end is reached. # This forces the hours, minute and second to be 0. end = self._date_to_datetime(self.end) if end is not None and value[0] > end: value[0] = end if end is not None and value[1] > end: value[1] = end value = tuple(value) return value def _deserialize_value(self, value): if isinstance(value, tuple): value = " to ".join(v.strftime(r'%Y-%m-%d %H:%M:%S') for v in value) if value is None: value = "" return value class ColorPicker(Widget): """ The `ColorPicker` widget allows selecting a hexidecimal RGB color value using the browser’s color-picking widget. Reference: https://panel.holoviz.org/reference/widgets/ColorPicker.html :Example: >>> ColorPicker(name='Color', value='#99ef78') """ value = param.Color(default=None, doc=""" The selected color""") _widget_type = _BkColorPicker _rename = {'value': 'color', 'name': 'title'} class _NumericInputBase(Widget): value = param.Number(default=0, allow_None=True, doc=""" The current value of the spinner.""") placeholder = param.String(default='0', doc=""" Placeholder for empty input field.""") format = param.ClassSelector(default=None, class_=(str, TickFormatter,), doc=""" Allows defining a custom format string or bokeh TickFormatter.""") start = param.Parameter(default=None, allow_None=True, doc=""" Optional minimum allowable value.""") end = param.Parameter(default=None, allow_None=True, doc=""" Optional maximum allowable value.""") _rename = {'name': 'title', 'start': 'low', 'end': 'high'} _widget_type = _BkNumericInput __abstract = True class _IntInputBase(_NumericInputBase): value = param.Integer(default=0, allow_None=True, doc=""" The current value of the spinner.""") start = param.Integer(default=None, allow_None=True, doc=""" Optional minimum allowable value.""") end = param.Integer(default=None, allow_None=True, doc=""" Optional maximum allowable value.""") mode = param.String(default='int', constant=True, doc=""" Define the type of number which can be enter in the input""") __abstract = True class _FloatInputBase(_NumericInputBase): value = param.Number(default=0, allow_None=True, doc=""" The current value of the spinner.""") start = param.Number(default=None, allow_None=True, doc=""" Optional minimum allowable value.""") end = param.Number(default=None, allow_None=True, doc=""" Optional maximum allowable value.""") mode = param.String(default='float', constant=True, doc=""" Define the type of number which can be enter in the input""") __abstract = True class _SpinnerBase(_NumericInputBase): page_step_multiplier = param.Integer(default=10, bounds=(0, None), doc=""" Defines the multiplication factor applied to step when the page up and page down keys are pressed.""") wheel_wait = param.Integer(default=100, doc=""" Defines the debounce time in ms before updating `value_throttled` when the mouse wheel is used to change the input.""") _widget_type = _BkSpinner __abstract = True def __init__(self, **params): if params.get('value') is None: value = params.get('start', self.value) if value is not None: params['value'] = value if 'value' in params and 'value_throttled' in self.param: params['value_throttled'] = params['value'] super().__init__(**params) def __repr__(self, depth=0): return '{cls}({params})'.format(cls=type(self).__name__, params=', '.join(param_reprs(self, ['value_throttled']))) def _update_model(self, events, msg, root, model, doc, comm): if 'value_throttled' in msg: del msg['value_throttled'] return super()._update_model(events, msg, root, model, doc, comm) def _process_param_change(self, msg): # Workaround for -inf serialization errors if 'value' in msg and msg['value'] == float('-inf'): msg['value'] = None msg['value_throttled'] = None return super()._process_param_change(msg) def _process_property_change(self, msg): if config.throttled: if "value" in msg: del msg["value"] if "value_throttled" in msg: msg["value"] = msg["value_throttled"] return super()._process_property_change(msg) class IntInput(_SpinnerBase, _IntInputBase): """ The `IntInput` allows selecting an integer value using a spinbox. It behaves like a slider except that lower and upper bounds are optional and a specific value can be entered. The value can be changed using the keyboard (up, down, page up, page down), mouse wheel and arrow buttons. Reference: https://panel.holoviz.org/reference/widgets/IntInput.html :Example: >>> IntInput(name='Value', value=100, start=0, end=1000, step=10) """ step = param.Integer(default=1, doc=""" The step size.""") value_throttled = param.Integer(default=None, constant=True, doc=""" The current value. Updates only on `` or when the widget looses focus.""") class FloatInput(_SpinnerBase, _FloatInputBase): """ The `FloatInput` allows selecting a floating point value using a spinbox. It behaves like a slider except that the lower and upper bounds are optional and a specific value can be entered. The value can be changed using the keyboard (up, down, page up, page down), mouse wheel and arrow buttons. Reference: https://panel.holoviz.org/reference/widgets/FloatInput.html :Example: >>> FloatInput(name='Value', value=5., step=1e-1, start=0, end=10) """ step = param.Number(default=0.1, doc=""" The step size.""") value_throttled = param.Number(default=None, constant=True, doc=""" The current value. Updates only on `` or when the widget looses focus.""") class NumberInput(_SpinnerBase): def __new__(self, **params): param_list = ["value", "start", "stop", "step"] if all(isinstance(params.get(p, 0), int) for p in param_list): return IntInput(**params) else: return FloatInput(**params) # Backward compatibility Spinner = NumberInput class LiteralInput(Widget): """ The `LiteralInput` allows declaring Python literals using a text input widget. A *literal* is some specific primitive value of type `str` , `int`, `float`, `bool` etc or a `dict`, `list`, `tuple`, `set` etc of primitive values. Optionally the literal `type` may be declared. Reference: https://panel.holoviz.org/reference/widgets/LiteralInput.html :Example: >>> LiteralInput(name='Dictionary', value={'key': [1, 2, 3]}, type=dict) """ serializer = param.ObjectSelector(default='ast', objects=['ast', 'json'], doc=""" The serialization (and deserialization) method to use. 'ast' uses ast.literal_eval and 'json' uses json.loads and json.dumps. """) type = param.ClassSelector(default=None, class_=(type, tuple), is_instance=True) value = param.Parameter(default=None) _rename = {'name': 'title', 'type': None, 'serializer': None} _source_transforms = {'value': """JSON.parse(value.replace(/'/g, '"'))"""} _target_transforms = {'value': """JSON.stringify(value).replace(/,/g, ",").replace(/:/g, ": ")"""} _widget_type = _BkTextInput def __init__(self, **params): super().__init__(**params) self._state = '' self._validate(None) self._callbacks.append(self.param.watch(self._validate, 'value')) def _validate(self, event): if self.type is None: return new = self.value if not isinstance(new, self.type) and new is not None: if event: self.value = event.old types = repr(self.type) if isinstance(self.type, tuple) else self.type.__name__ raise ValueError('LiteralInput expected %s type but value %s ' 'is of type %s.' % (types, new, type(new).__name__)) def _process_property_change(self, msg): msg = super()._process_property_change(msg) new_state = '' if 'value' in msg: value = msg.pop('value') try: if self.serializer == 'json': value = json.loads(value) else: value = ast.literal_eval(value) except Exception: new_state = ' (invalid)' value = self.value else: if self.type and not isinstance(value, self.type): vtypes = self.type if isinstance(self.type, tuple) else (self.type,) typed_value = None for vtype in vtypes: try: typed_value = vtype(value) except Exception: pass else: break if typed_value is None and value is not None: new_state = ' (wrong type)' value = self.value else: value = typed_value msg['value'] = value msg['name'] = msg.get('title', self.name).replace(self._state, '') + new_state self._state = new_state self.param.trigger('name') return msg def _process_param_change(self, msg): msg = super()._process_param_change(msg) if 'value' in msg: value = msg['value'] if isinstance(value, str): value = repr(value) elif self.serializer == 'json': value = json.dumps(value) else: value = '' if value is None else str(value) msg['value'] = value msg['title'] = self.name return msg class ArrayInput(LiteralInput): """ The `ArrayInput` allows rendering and editing NumPy arrays in a text input widget. Arrays larger than the `max_array_size` will be summarized and editing will be disabled. Reference: https://panel.holoviz.org/reference/widgets/ArrayInput.html :Example: >>> To be determined ... """ max_array_size = param.Number(default=1000, doc=""" Arrays larger than this limit will be allowed in Python but will not be serialized into JavaScript. Although such large arrays will thus not be editable in the widget, such a restriction helps avoid overwhelming the browser and lets other widgets remain usable.""") _rename = dict(LiteralInput._rename, max_array_size=None) _source_transforms = {'value': None} def __init__(self, **params): super().__init__(**params) self._auto_disabled = False def _process_property_change(self, msg): msg = super()._process_property_change(msg) if 'value' in msg and isinstance(msg['value'], list): msg['value'] = np.asarray(msg['value']) return msg def _process_param_change(self, msg): if msg.get('disabled', False): self._auto_disabled = False value = msg.get('value') if value is None: return super()._process_param_change(msg) if value.size <= self.max_array_size: msg['value'] = value.tolist() # If array is no longer larger than max_array_size # unset disabled if self.disabled and self._auto_disabled: self.disabled = False msg['disabled'] = False self._auto_disabled = False else: msg['value'] = np.array2string( msg['value'], separator=',', threshold=self.max_array_size ) if not self.disabled: self.param.warning( f"Number of array elements ({value.size}) exceeds " f"`max_array_size` ({self.max_array_size}), editing " "will be disabled." ) self.disabled = True msg['disabled'] = True self._auto_disabled = True return super()._process_param_change(msg) class DatetimeInput(LiteralInput): """ The `DatetimeInput` allows specifying Python `datetime` like values using a text input widget. An optional `type` may be declared. Reference: https://panel.holoviz.org/reference/widgets/DatetimeInput.html :Example: >>> DatetimeInput(name='Datetime', value=datetime(2019, 2, 8)) """ value = param.Date(default=None, doc=""" The current value""") start = param.Date(default=None, doc=""" Inclusive lower bound of the allowed date selection""") end = param.Date(default=None, doc=""" Inclusive upper bound of the allowed date selection""") format = param.String(default='%Y-%m-%d %H:%M:%S', doc=""" Datetime format used for parsing and formatting the datetime.""") type = datetime _source_transforms = {'value': None, 'start': None, 'end': None} _rename = {'format': None, 'type': None, 'name': 'title', 'start': None, 'end': None, 'serializer': None} def __init__(self, **params): super().__init__(**params) self.param.watch(self._validate, 'value') self._validate(None) def _validate(self, event): new = self.value if new is not None and ((self.start is not None and self.start > new) or (self.end is not None and self.end < new)): value = datetime.strftime(new, self.format) start = datetime.strftime(self.start, self.format) end = datetime.strftime(self.end, self.format) if event: self.value = event.old raise ValueError('DatetimeInput value must be between {start} and {end}, ' 'supplied value is {value}'.format(start=start, end=end, value=value)) def _process_property_change(self, msg): msg = Widget._process_property_change(self, msg) new_state = '' if 'value' in msg: value = msg.pop('value') try: value = datetime.strptime(value, self.format) except Exception: new_state = ' (invalid)' value = self.value else: if value is not None and ((self.start is not None and self.start > value) or (self.end is not None and self.end < value)): new_state = ' (out of bounds)' value = self.value msg['value'] = value msg['name'] = msg.get('title', self.name).replace(self._state, '') + new_state self._state = new_state return msg def _process_param_change(self, msg): msg = Widget._process_param_change(self, msg) if 'value' in msg: value = msg['value'] if value is None: value = '' else: value = datetime.strftime(msg['value'], self.format) msg['value'] = value msg['title'] = self.name return msg class DatetimeRangeInput(CompositeWidget): """ The `DatetimeRangeInput` widget allows selecting a `datetime` range using two `DatetimeInput` widgets, which return a `tuple` range. Reference: https://panel.holoviz.org/reference/widgets/DatetimeRangeInput.html :Example: >>> DatetimeRangeInput( ... name='Datetime Range', ... value=(datetime(2017, 1, 1), datetime(2018, 1, 10)), ... start=datetime(2017, 1, 1), end=datetime(2019, 1, 1), ... ) """ value = param.Tuple(default=(None, None), length=2, doc=""" The current value""") start = param.Date(default=None, doc=""" Inclusive lower bound of the allowed date selection""") end = param.Date(default=None, doc=""" Inclusive upper bound of the allowed date selection""") format = param.String(default='%Y-%m-%d %H:%M:%S', doc=""" Datetime format used for parsing and formatting the datetime.""") _composite_type = Column def __init__(self, **params): self._text = StaticText(margin=(5, 0, 0, 0), style={'white-space': 'nowrap'}) self._start = DatetimeInput(sizing_mode='stretch_width', margin=(5, 0, 0, 0)) self._end = DatetimeInput(sizing_mode='stretch_width', margin=(5, 0, 0, 0)) if 'value' not in params: params['value'] = (params['start'], params['end']) super().__init__(**params) self._msg = '' self._composite.extend([self._text, self._start, self._end]) self._updating = False self.param.watch(self._update_widgets, [p for p in self.param if p != 'name']) self._update_widgets() self._update_label() @param.depends('name', '_start.name', '_end.name', watch=True) def _update_label(self): self._text.value = f'{self.name}{self._start.name}{self._end.name}{self._msg}' @param.depends('_start.value', '_end.value', watch=True) def _update(self): if self._updating: return if (self._start.value is not None and self._end.value is not None and self._start.value > self._end.value): self._msg = ' (start of range must be <= end)' self._update_label() return elif self._msg: self._msg = '' self._update_label() try: self._updating = True self.value = (self._start.value, self._end.value) finally: self._updating = False def _update_widgets(self, *events): filters = [event.name for event in events] if events else list(self.param) if 'name' in filters: filters.remove('name') if self._updating: return try: self._updating = True params = {k: v for k, v in self.param.values().items() if k in filters} start_params = dict(params, value=self.value[0]) end_params = dict(params, value=self.value[1]) self._start.param.update(**start_params) self._end.param.update(**end_params) finally: self._updating = False class Checkbox(Widget): """ The `Checkbox` allows toggling a single condition between `True`/`False` states by ticking a checkbox. This widget is interchangeable with the `Toggle` widget. Reference: https://panel.holoviz.org/reference/widgets/Checkbox.html :Example: >>> Checkbox(name='Works with the tools you know and love', value=True) """ value = param.Boolean(default=False, doc=""" The current value""") _supports_embed = True _rename = {'value': 'active', 'name': 'labels'} _source_transforms = {'value': "value.indexOf(0) >= 0", 'name': "value[0]"} _target_transforms = {'value': "value ? [0] : []", 'name': "[value]"} _widget_type = _BkCheckboxGroup def _process_property_change(self, msg): msg = super()._process_property_change(msg) if 'value' in msg: msg['value'] = 0 in msg.pop('value') if 'name' in msg: msg['name'] = [msg['name']] return msg def _process_param_change(self, msg): msg = super()._process_param_change(msg) if 'active' in msg: msg['active'] = [0] if msg.pop('active', None) else [] if 'labels' in msg: msg['labels'] = [msg.pop('labels')] return msg def _get_embed_state(self, root, values=None, max_opts=3): return (self, self._models[root.ref['id']][0], [False, True], lambda x: str(0 in x.active).lower(), 'active', "String(cb_obj.active.indexOf(0) >= 0)")