Skip to content

Commit f1cb105

Browse files
authored
Handle exclusive bounds on Param widgets (#8165)
* Handle exclusive bounds on Param widgets * Small refactor
1 parent d29b824 commit f1cb105

2 files changed

Lines changed: 92 additions & 6 deletions

File tree

panel/param.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -488,17 +488,24 @@ def widget(self_or_cls, p_name: str, parameterized: param.Parameterized | None =
488488
kw['options'] = options
489489
if hasattr(p_obj, 'get_soft_bounds'):
490490
bounds = p_obj.get_soft_bounds()
491-
if bounds[0] is not None:
492-
kw['start'] = bounds[0]
493-
if bounds[1] is not None:
494-
kw['end'] = bounds[1]
491+
is_int = isinstance(p_obj, param.Integer)
492+
start, end = bounds
493+
istart, iend = getattr(p_obj, 'inclusive_bounds', (True, True))
494+
if is_int and not istart:
495+
start += 1
496+
if start is not None:
497+
kw['start'] = start
498+
if is_int and not iend:
499+
end -= 1
500+
if end is not None:
501+
kw['end'] = end
495502
if (('start' not in kw or 'end' not in kw) and
496503
not isinstance(p_obj, (param.Date, param.CalendarDate))):
497504
# Do not change widget class if mapping was overridden
498505
if not widget_class_overridden:
499506
if isinstance(p_obj, param.Number):
500507
widget_class = self_or_cls.input_widgets[float]
501-
if isinstance(p_obj, param.Integer):
508+
if is_int:
502509
widget_class = self_or_cls.input_widgets[int]
503510
elif not issubclass(widget_class, LiteralInput):
504511
widget_class = self_or_cls.input_widgets['literal']
@@ -543,11 +550,27 @@ def link_widget(change):
543550
p_key = p_name if config.nthreads is None else (threading.get_ident(), p_name)
544551
if p_key in updating:
545552
return
553+
new = change.new
554+
reset = False
555+
if isinstance(p_obj, param.Number) and not isinstance(p_obj, param.Integer):
556+
istart, iend = getattr(p_obj, 'inclusive_bounds', (True, True))
557+
bounds = p_obj.get_soft_bounds()
558+
if (not istart and new == bounds[0]) or (not iend and new == bounds[1]):
559+
new = change.old
560+
reset = True
561+
elif isinstance(p_obj, param.Range):
562+
istart, iend = getattr(p_obj, 'inclusive_bounds', (True, True))
563+
bounds = p_obj.get_soft_bounds()
564+
if (not istart and new[0] == bounds[0]) or (not iend and new[1] == bounds[1]):
565+
new = change.old
566+
reset = True
546567
try:
547568
updating.append(p_key)
548-
parameterized.param.update(**{p_name: change.new})
569+
parameterized.param.update(**{p_name: new})
549570
finally:
550571
updating.remove(p_key)
572+
if reset:
573+
widget.value = new
551574

552575
if hasattr(param, 'Event') and isinstance(p_obj, param.Event):
553576
def event(change):

panel/tests/test_param.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,69 @@ class Test(param.Parameterized):
431431
assert select.disabled == False
432432

433433

434+
def test_integer_param_exclusive_bounds(document, comm):
435+
class Test(param.Parameterized):
436+
a = param.Integer(default=1, bounds=(0, 10), inclusive_bounds=(False, False))
437+
438+
test = Test()
439+
test_pane = Param(test)
440+
model = test_pane.get_root(document, comm=comm)
441+
442+
widget = model.children[1]
443+
assert isinstance(widget, Slider)
444+
assert widget.start == 1
445+
assert widget.end == 9
446+
assert widget.value == 1
447+
assert widget.disabled == False
448+
449+
def test_number_param_exclusive_bounds(document, comm):
450+
class Test(param.Parameterized):
451+
a = param.Number(default=1, bounds=(0, 10), inclusive_bounds=(False, False))
452+
453+
test = Test()
454+
test_pane = Param(test)
455+
model = test_pane.get_root(document, comm=comm)
456+
457+
widget = model.children[1]
458+
assert isinstance(widget, Slider)
459+
assert widget.start == 0
460+
assert widget.end == 10
461+
assert widget.value == 1
462+
assert widget.disabled == False
463+
464+
widget.value = 0.1
465+
assert test.a == 0.1
466+
467+
widget.value = 0
468+
assert widget.value == 0.1
469+
470+
widget.value = 10
471+
assert widget.value == 0.1
472+
473+
def test_range_param_exclusive_bounds(document, comm):
474+
class Test(param.Parameterized):
475+
a = param.Range(default=(1, 9), bounds=(0, 10), inclusive_bounds=(False, False))
476+
477+
test = Test()
478+
test_pane = Param(test)
479+
model = test_pane.get_root(document, comm=comm)
480+
481+
widget = model.children[1]
482+
assert isinstance(widget, BkRangeSlider)
483+
assert widget.start == 0
484+
assert widget.end == 10
485+
assert widget.value == (1, 9)
486+
assert widget.disabled == False
487+
488+
widget.value = (0.1, 9.9)
489+
assert test.a == (0.1, 9.9)
490+
491+
widget.value = (0, 9.9)
492+
assert widget.value == (0.1, 9.9)
493+
494+
widget.value = (0.1, 10)
495+
assert widget.value == (0.1, 9.9)
496+
434497
def test_number_input_none_support():
435498
class Test(param.Parameterized) :
436499
number = param.Number(default=0, allow_None=True)

0 commit comments

Comments
 (0)