Repository: holoviz/panel

Issue:
Spinner widget doesn't honor bounds when created from Param object
The `Spinner` widget allows you to set a `start` and `end` bound. The widget does not let a user set the number outside the bounds:

```python
pn.widgets.Spinner(start=0, end=5)
```

When creating a `Spinner` widget from a `param.Number` I would have expected the `start` and `end` to be derived from the bounds of the param object.

```python
class Demo(param.Parameterized):
    a = param.Number(default=5, bounds=(0, 5))
    
    
    def panel(self):
        return pn.widgets.Spinner.from_param(self.param.a)
```

However, in this case if a user tries to specify a number outside of the bounds the widget allows it and then it raises an error:

<details>

```python
Traceback (most recent call last):
  File "~/miniconda/envs/myenv/lib/python3.7/site-packages/pyviz_comms/__init__.py", line 325, in _handle_msg
    self._on_msg(msg)
  File "~/miniconda/envs/myenv/lib/python3.7/site-packages/panel/viewable.py", line 273, in _on_msg
    doc.unhold()
  File "~/miniconda/envs/myenv/lib/python3.7/site-packages/bokeh/document/document.py", line 669, in unhold
    self._trigger_on_change(event)
  File "~/miniconda/envs/myenv/lib/python3.7/site-packages/bokeh/document/document.py", line 1180, in _trigger_on_change
    self._with_self_as_curdoc(event.callback_invoker)
  File "~/miniconda/envs/myenv/lib/python3.7/site-packages/bokeh/document/document.py", line 1198, in _with_self_as_curdoc
    return f()
  File "~/miniconda/envs/myenv/lib/python3.7/site-packages/bokeh/util/callback_manager.py", line 161, in invoke
    callback(attr, old, new)
  File "~/miniconda/envs/myenv/lib/python3.7/site-packages/panel/reactive.py", line 301, in _comm_change
    self._process_events({attr: new})
  File "~/miniconda/envs/myenv/lib/python3.7/site-packages/panel/reactive.py", line 262, in _process_events
    self.param.set_param(**self_events)
  File "~/miniconda/envs/myenv/lib/python3.7/site-packages/param/parameterized.py", line 1526, in set_param
    self_._batch_call_watchers()
  File "~/miniconda/envs/myenv/lib/python3.7/site-packages/param/parameterized.py", line 1665, in _batch_call_watchers
    self_._execute_watcher(watcher, events)
  File "~/miniconda/envs/myenv/lib/python3.7/site-packages/param/parameterized.py", line 1627, in _execute_watcher
    watcher.fn(*args, **kwargs)
  File "~/miniconda/envs/myenv/lib/python3.7/site-packages/panel/param.py", line 438, in link_widget
    self.object.param.set_param(**{p_name: change.new})
  File "~/miniconda/envs/myenv/lib/python3.7/site-packages/param/parameterized.py", line 1519, in set_param
    setattr(self_or_cls, k, v)
  File "~/miniconda/envs/myenv/lib/python3.7/site-packages/param/parameterized.py", line 316, in _f
    instance_param.__set__(obj, val)
  File "~/miniconda/envs/myenv/lib/python3.7/site-packages/param/parameterized.py", line 318, in _f
    return f(self, obj, val)
  File "~/miniconda/envs/myenv/lib/python3.7/site-packages/param/__init__.py", line 624, in __set__
    super(Dynamic,self).__set__(obj,val)
  File "~/miniconda/envs/myenv/lib/python3.7/site-packages/param/parameterized.py", line 318, in _f
    return f(self, obj, val)
  File "~/miniconda/envs/myenv/lib/python3.7/site-packages/param/parameterized.py", line 940, in __set__
    self._validate(val)
  File "~/miniconda/envs/myenv/lib/python3.7/site-packages/param/__init__.py", line 908, in _validate
    self._validate_bounds(val, self.bounds, self.inclusive_bounds)
  File "~/miniconda/envs/myenv/lib/python3.7/site-packages/param/__init__.py", line 872, in _validate_bounds
    "not %s." % (self.name, vmax, val))
ValueError: Parameter 'a' must be at most 5, not 6.
```

</details>

![image](https://user-images.githubusercontent.com/6454472/133641653-1d6aff54-4516-432f-8901-d60313b11f8b.png)


Even if you manually set the `start` and `end` values in the `from_param` call you get the same error:

```python
class Demo(param.Parameterized):
    a = param.Number(default=5, bounds=(0, 5))
    
    
    def panel(self):
        return pn.widgets.Spinner.from_param(self.param.a, start=self.param.a.bounds[0], end=self.param.a.bounds[1])
```

It seems like the `start` and `end` values are being passed through:

![image](https://user-images.githubusercontent.com/6454472/133642499-601ae52f-f3f5-4d30-bed7-96040ffd349f.png)

But if you set `start` and `end` after the widget is created then it works as expected:

![image](https://user-images.githubusercontent.com/6454472/133643052-526bf610-9381-44a0-a07a-a76edf9ab3f8.png)



Discussion:
This probably goes without saying, but I would expect the `Spinner` object to honor the `softbounds` if it is specified, otherwise it should honor the `bounds`.
> When creating a Spinner widget from a param.Number I would have expected the start and end to be derived from the bounds of the param object.

I agree; that sounds like a bug!

> This probably goes without saying, but I would expect the Spinner object to honor the softbounds if it is specified, otherwise it should honor the bounds.

Softbounds were designed for a slider widget where _some_ bounds are required before displaying anything. For a spinner widget, I would expect softbounds to be ignored; they are only hints, and if the user wants the value to go outside the soft bounds, why not let them?
As a workaround you could use `pn.widgets.IntInput.from_param(...)` instead of `pn.widgets.Spinner`.

Resolving patch:
diff --git a/panel/widgets/input.py b/panel/widgets/input.py
--- a/panel/widgets/input.py
+++ b/panel/widgets/input.py
@@ -296,6 +296,12 @@ class _NumericInputBase(Widget):
     format = param.ClassSelector(default=None, class_=string_types+(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


Test patch:
diff --git a/panel/tests/test_param.py b/panel/tests/test_param.py
--- a/panel/tests/test_param.py
+++ b/panel/tests/test_param.py
@@ -9,7 +9,7 @@
 from panel.pane import Pane, PaneBase, Matplotlib, Bokeh, HTML
 from panel.layout import Tabs, Row
 from panel.param import Param, ParamMethod, ParamFunction, JSONInit
-from panel.widgets import LiteralInput, RangeSlider
+from panel.widgets import LiteralInput, NumberInput, RangeSlider
 from panel.tests.util import mpl_available, mpl_figure
 
 
@@ -1221,3 +1221,16 @@ class Test(param.Parameterized):
     assert isinstance(p._widgets['num'], LiteralInput)
     assert p._widgets['num'] in p._widget_box
 
+
+def test_numberinput_bounds():
+
+    class Test(param.Parameterized):
+        num = param.Number(default=5, bounds=(0, 5))
+
+    test = Test()
+    p = Param(test, widgets={'num': NumberInput})
+
+    numinput = p.layout[1]
+
+    assert numinput.start == 0
+    assert numinput.end == 5