Repository: bokeh/bokeh

Issue:
Bokeh DateRangeSlider returns tuple of integers instead of dates
In 0.12.7 the DateRangeSlider `.value` returns a tuple of integers. Additionally, in order to convert them back to dates you have to divide by 1000 (i.e. `datetime.fromtimestamp(the_tuple[0]/1000)`).

Expected behavior: Return a tuple of Date objects.

```
from datetime import datetime
from bokeh.models.widgets import DateRangeSlider
from bokeh.io import curdoc

def date_range_update(attrname, old, new):
    print('-- range values:', date_slider.value)
    # Works
    d1 = datetime.fromtimestamp(date_slider.value[0] / 1000) 
    # Does not Work, gives error
    d2 = datetime.fromtimestamp(date_slider.value[0])

date_slider = DateRangeSlider(value=(date_start,date_end), start=date_start, end=date_end)
date_slider.on_change('value', date_range_update)

curdoc().add_root(date_slider)

```

#### Stack traceback and/or browser JavaScript console output

#### Screenshots or screencasts of the bug in action



Discussion:

Resolving patch:
diff --git a/bokeh/models/widgets/sliders.py b/bokeh/models/widgets/sliders.py
--- a/bokeh/models/widgets/sliders.py
+++ b/bokeh/models/widgets/sliders.py
@@ -3,6 +3,9 @@
 """
 from __future__ import absolute_import
 
+from datetime import datetime
+import numbers
+
 from ...core.has_props import abstract
 from ...core.properties import Bool, Int, Float, String, Date, Enum, Tuple, Instance, Color, Override
 from ...core.enums import SliderCallbackPolicy
@@ -121,6 +124,25 @@ class DateSlider(AbstractSlider):
 class DateRangeSlider(AbstractSlider):
     """ Slider-based date range selection widget. """
 
+    @property
+    def value_as_datetime(self):
+        ''' Convenience property to retrieve the value tuple as a tuple of
+        datetime objects.
+
+        '''
+        if self.value is None:
+            return None
+        v1, v2 = self.value
+        if isinstance(v1, numbers.Number):
+            d1 = datetime.utcfromtimestamp(v1 / 1000)
+        else:
+            d1 = v1
+        if isinstance(v2, numbers.Number):
+            d2 = datetime.utcfromtimestamp(v2 / 1000)
+        else:
+            d2 = v2
+        return d1, d2
+
     value = Tuple(Date, Date, help="""
     Initial or selected range.
     """)


Test patch:
diff --git a/bokeh/models/widgets/tests/__init__.py b/bokeh/models/widgets/tests/__init__.py
new file mode 100644
diff --git a/bokeh/models/widgets/tests/test_slider.py b/bokeh/models/widgets/tests/test_slider.py
new file mode 100644
--- /dev/null
+++ b/bokeh/models/widgets/tests/test_slider.py
@@ -0,0 +1,31 @@
+import bokeh.models.widgets.sliders as mws
+
+from datetime import datetime
+
+from bokeh.util.serialization import convert_datetime_type
+
+def test_daterangeslider_value_as_datetime_when_set_as_datetime():
+    start = datetime(2017, 8, 9, 0, 0)
+    end = datetime(2017, 8, 10, 0, 0)
+    s = mws.DateRangeSlider(start=start, end=end, value=(start, end))
+    assert s.value_as_datetime == (start, end)
+
+def test_daterangeslider_value_as_datetime_when_set_as_timestamp():
+    start = datetime(2017, 8, 9, 0, 0)
+    end = datetime(2017, 8, 10, 0, 0)
+    s = mws.DateRangeSlider(start=start, end=end,
+            # Bokeh serializes as ms since epoch, if they get set as numbers (e.g.)
+            # by client side update, this is the units they will be
+            value=(convert_datetime_type(start), convert_datetime_type(end)))
+    assert s.value_as_datetime == (start, end)
+
+def test_daterangeslider_value_as_datetime_when_set_mixed():
+    start = datetime(2017, 8, 9, 0, 0)
+    end = datetime(2017, 8, 10, 0, 0)
+    s = mws.DateRangeSlider(start=start, end=end,
+            value=(start, convert_datetime_type(end)))
+    assert s.value_as_datetime == (start, end)
+
+    s = mws.DateRangeSlider(start=start, end=end,
+            value=(convert_datetime_type(start), end))
+    assert s.value_as_datetime == (start, end)