diff --git a/panel/param.py b/panel/param.py index 7a81e9e8f4..ac3693c628 100644 --- a/panel/param.py +++ b/panel/param.py @@ -488,17 +488,24 @@ def widget(self_or_cls, p_name: str, parameterized: param.Parameterized | None = kw['options'] = options if hasattr(p_obj, 'get_soft_bounds'): bounds = p_obj.get_soft_bounds() - if bounds[0] is not None: - kw['start'] = bounds[0] - if bounds[1] is not None: - kw['end'] = bounds[1] + is_int = isinstance(p_obj, param.Integer) + start, end = bounds + istart, iend = getattr(p_obj, 'inclusive_bounds', (True, True)) + if is_int and not istart: + start += 1 + if start is not None: + kw['start'] = start + if is_int and not iend: + end -= 1 + if end is not None: + kw['end'] = end if (('start' not in kw or 'end' not in kw) and not isinstance(p_obj, (param.Date, param.CalendarDate))): # Do not change widget class if mapping was overridden if not widget_class_overridden: if isinstance(p_obj, param.Number): widget_class = self_or_cls.input_widgets[float] - if isinstance(p_obj, param.Integer): + if is_int: widget_class = self_or_cls.input_widgets[int] elif not issubclass(widget_class, LiteralInput): widget_class = self_or_cls.input_widgets['literal'] @@ -543,11 +550,27 @@ def link_widget(change): p_key = p_name if config.nthreads is None else (threading.get_ident(), p_name) if p_key in updating: return + new = change.new + reset = False + if isinstance(p_obj, param.Number) and not isinstance(p_obj, param.Integer): + istart, iend = getattr(p_obj, 'inclusive_bounds', (True, True)) + bounds = p_obj.get_soft_bounds() + if (not istart and new == bounds[0]) or (not iend and new == bounds[1]): + new = change.old + reset = True + elif isinstance(p_obj, param.Range): + istart, iend = getattr(p_obj, 'inclusive_bounds', (True, True)) + bounds = p_obj.get_soft_bounds() + if (not istart and new[0] == bounds[0]) or (not iend and new[1] == bounds[1]): + new = change.old + reset = True try: updating.append(p_key) - parameterized.param.update(**{p_name: change.new}) + parameterized.param.update(**{p_name: new}) finally: updating.remove(p_key) + if reset: + widget.value = new if hasattr(param, 'Event') and isinstance(p_obj, param.Event): def event(change): diff --git a/panel/tests/test_param.py b/panel/tests/test_param.py index 0e2b180226..486f32d7e4 100644 --- a/panel/tests/test_param.py +++ b/panel/tests/test_param.py @@ -431,6 +431,69 @@ class Test(param.Parameterized): assert select.disabled == False +def test_integer_param_exclusive_bounds(document, comm): + class Test(param.Parameterized): + a = param.Integer(default=1, bounds=(0, 10), inclusive_bounds=(False, False)) + + test = Test() + test_pane = Param(test) + model = test_pane.get_root(document, comm=comm) + + widget = model.children[1] + assert isinstance(widget, Slider) + assert widget.start == 1 + assert widget.end == 9 + assert widget.value == 1 + assert widget.disabled == False + +def test_number_param_exclusive_bounds(document, comm): + class Test(param.Parameterized): + a = param.Number(default=1, bounds=(0, 10), inclusive_bounds=(False, False)) + + test = Test() + test_pane = Param(test) + model = test_pane.get_root(document, comm=comm) + + widget = model.children[1] + assert isinstance(widget, Slider) + assert widget.start == 0 + assert widget.end == 10 + assert widget.value == 1 + assert widget.disabled == False + + widget.value = 0.1 + assert test.a == 0.1 + + widget.value = 0 + assert widget.value == 0.1 + + widget.value = 10 + assert widget.value == 0.1 + +def test_range_param_exclusive_bounds(document, comm): + class Test(param.Parameterized): + a = param.Range(default=(1, 9), bounds=(0, 10), inclusive_bounds=(False, False)) + + test = Test() + test_pane = Param(test) + model = test_pane.get_root(document, comm=comm) + + widget = model.children[1] + assert isinstance(widget, BkRangeSlider) + assert widget.start == 0 + assert widget.end == 10 + assert widget.value == (1, 9) + assert widget.disabled == False + + widget.value = (0.1, 9.9) + assert test.a == (0.1, 9.9) + + widget.value = (0, 9.9) + assert widget.value == (0.1, 9.9) + + widget.value = (0.1, 10) + assert widget.value == (0.1, 9.9) + def test_number_input_none_support(): class Test(param.Parameterized) : number = param.Number(default=0, allow_None=True)