Repository: holoviz/panel

Issue:
DatetimeRangeSlider returns datetime.date instead of datetime.datetime
#### ALL software version info
Python 3.10.13
Panel: 1.3.4
Pandas: 2.0.3
Bokeh: 3.3.1
Param: 2.0.1
MacOS 14.1

#### Description of expected behavior and the observed behavior

The DatetimeRangeSlider return value should be of type tuple of datetime.datetime. Since panel version 1.3.2 the return value is a tuple of type datetime.date after interacting with the widget (initially it is tuple of datetime.datetime). 

#### Complete, minimal, self-contained example code that reproduces the issue

Behaviour can be observed in the panel docs here: https://panel.holoviz.org/reference/widgets/DatetimeRangeSlider.html

The following example, extracted from the above mentioned link, is designed for execution in a notebook:

```
import datetime as dt
import panel as pn

pn.extension()

datetime_range_slider = pn.widgets.DatetimeRangeSlider(
    name='Datetime Range Slider',
    start=dt.datetime(2017, 1, 1), end=dt.datetime(2019, 1, 1),
    value=(dt.datetime(2017, 1, 1), dt.datetime(2018, 1, 10)),
)

datetime_range_slider

datetime_range_slider.value

```


#### Screenshots or screencasts of the bug in action

![image](https://github.com/holoviz/panel/assets/53038477/ab9de5ff-3fdd-4dbe-b531-219ebf88cc80)


- [ ] I may be interested in making a pull request to address this



Discussion:

Resolving patch:
diff --git a/panel/widgets/slider.py b/panel/widgets/slider.py
--- a/panel/widgets/slider.py
+++ b/panel/widgets/slider.py
@@ -697,6 +697,8 @@ class DateRangeSlider(_SliderBase):
 
     _widget_type: ClassVar[Type[Model]] = _BkDateRangeSlider
 
+    _property_conversion = staticmethod(value_as_date)
+
     def __init__(self, **params):
         if 'value' not in params:
             value_to_None = False
@@ -743,10 +745,10 @@ def _process_property_change(self, msg):
         msg = super()._process_property_change(msg)
         if 'value' in msg:
             v1, v2 = msg['value']
-            msg['value'] = (value_as_date(v1), value_as_date(v2))
+            msg['value'] = (self._property_conversion(v1), self._property_conversion(v2))
         if 'value_throttled' in msg:
             v1, v2 = msg['value_throttled']
-            msg['value_throttled'] = (value_as_date(v1), value_as_date(v2))
+            msg['value_throttled'] = (self._property_conversion(v1), self._property_conversion(v2))
         return msg
 
 
@@ -772,6 +774,8 @@ class DatetimeRangeSlider(DateRangeSlider):
     ... )
     """
 
+    _property_conversion = staticmethod(value_as_datetime)
+
     @property
     def _widget_type(self):
         try:


Test patch:
diff --git a/panel/tests/widgets/test_slider.py b/panel/tests/widgets/test_slider.py
--- a/panel/tests/widgets/test_slider.py
+++ b/panel/tests/widgets/test_slider.py
@@ -9,9 +9,9 @@
 
 from panel import config
 from panel.widgets import (
-    DateRangeSlider, DateSlider, DiscreteSlider, EditableFloatSlider,
-    EditableIntSlider, EditableRangeSlider, FloatSlider, IntSlider,
-    RangeSlider, StaticText,
+    DateRangeSlider, DateSlider, DatetimeRangeSlider, DiscreteSlider,
+    EditableFloatSlider, EditableIntSlider, EditableRangeSlider, FloatSlider,
+    IntSlider, RangeSlider, StaticText,
 )
 
 
@@ -200,6 +200,45 @@ def test_date_range_slider(document, comm):
         assert widget.value == (1613088000000, 1620777600000)
 
 
+def test_datetime_range_slider(document, comm):
+    datetime_slider = DatetimeRangeSlider(name='DatetimeRangeSlider',
+                                  value=(datetime(2018, 9, 2), datetime(2018, 9, 4)),
+                                  start=datetime(2018, 9, 1), end=datetime(2018, 9, 10))
+    widget = datetime_slider.get_root(document, comm=comm)
+    assert isinstance(widget, datetime_slider._widget_type)
+    assert widget.title == 'DatetimeRangeSlider'
+    assert widget.value == (1535846400000, 1536019200000)
+    assert widget.start == 1535760000000
+    assert widget.end == 1536537600000
+    epoch = datetime(1970, 1, 1)
+    widget.value = ((datetime(2018, 9, 3)-epoch).total_seconds()*1000,
+                    (datetime(2018, 9, 6)-epoch).total_seconds()*1000)
+    datetime_slider._process_events({'value': widget.value})
+    assert datetime_slider.value == (datetime(2018, 9, 3), datetime(2018, 9, 6))
+    value_throttled = ((datetime(2018, 9, 3)-epoch).total_seconds()*1000,
+                    (datetime(2018, 9, 6)-epoch).total_seconds()*1000)
+    datetime_slider._process_events({'value_throttled': value_throttled})
+    assert datetime_slider.value == (datetime(2018, 9, 3), datetime(2018, 9, 6))
+
+    datetime_slider.value = (datetime(2018, 9, 4), datetime(2018, 9, 6))
+    assert widget.value == (1536019200000, 1536192000000)
+    # Testing throttled mode
+    epoch_time = lambda dt: (dt - epoch).total_seconds() * 1000
+    epoch_times = lambda *dts: tuple(map(epoch_time, dts))
+    with config.set(throttled=True):
+        datetime_slider._process_events(
+            {'value': epoch_times(datetime(2021, 2, 15), datetime(2021, 5, 15))}
+        )
+        assert datetime_slider.value == (datetime(2018, 9, 4), datetime(2018, 9, 6))  # no change
+        datetime_slider._process_events(
+            {'value_throttled': epoch_times(datetime(2021, 2, 15), datetime(2021, 5, 15))}
+        )
+        assert datetime_slider.value == (datetime(2021, 2, 15), datetime(2021, 5, 15))
+
+        datetime_slider.value = (datetime(2021, 2, 12), datetime(2021, 5, 12))
+        assert widget.value == (1613088000000, 1620777600000)
+
+
 def test_discrete_slider(document, comm):
     discrete_slider = DiscreteSlider(name='DiscreteSlider', value=1,
                                      options=[0.1, 1, 10, 100])