Repository: bokeh/bokeh

Issue:
screen distance spec fields fail with bokeh.client
The following code fails. When it leaves the client, a "units" key is added to the `size` spec, but the server rejects this since `ScreenDistanceSpec` is a `NumberSpec`. 

```
from __future__ import print_function

import numpy as np

from bokeh.client import push_session
from bokeh.document import Document
from bokeh.models import (
    ColumnDataSource, DataRange1d, Plot, Circle, WidgetBox, Row, Button, TapTool
)

document = Document()
session = push_session(document)


N = 9

x = np.linspace(-2, 2, N)
y = x**2

source1 = ColumnDataSource(dict(x = x, y = y, size = [20]*N))
xdr1 = DataRange1d()
ydr1 = DataRange1d()
plot1 = Plot(x_range=xdr1, y_range=ydr1, plot_width=400, plot_height=400)
plot1.title.text = "Plot1"
plot1.tools.append(TapTool(plot=plot1))
plot1.add_glyph(source1, Circle(x="x", y="y", size="size", fill_color="red"))

source2 = ColumnDataSource(dict(x = x, y = y, color = ["blue"]*N))
xdr2 = DataRange1d()
ydr2 = DataRange1d()
plot2 = Plot(x_range=xdr2, y_range=ydr2, plot_width=400, plot_height=400)
plot2.title.text = "Plot2"
plot2.tools.append(TapTool(plot=plot2))
plot2.add_glyph(source2, Circle(x="x", y="y", size=20, fill_color="color"))

def on_selection_change1(attr, _, inds):
    color = ["blue"]*N
    if inds['1d']['indices']:
        indices = inds['1d']['indices']
        for i in indices:
            color[i] = "red"
    source2.data["color"] = color

source1.on_change('selected', on_selection_change1)

def on_selection_change2(attr, _, inds):
    inds = inds['1d']['indices']
    if inds:
        [index] = inds
        size = [10]*N
        size[index] = 40
    else:
        size = [20]*N
    source1.data["size"] = size

source2.on_change('selected', on_selection_change2)

reset = Button(label="Reset")

def on_reset_click():
    source1.selected = {
        '0d': {'flag': False, 'indices': []},
        '1d': {'indices': []},
        '2d': {'indices': {}}
    }
    source2.selected = {
        '0d': {'flag': False, 'indices': []},
        '1d': {'indices': []},
        '2d': {'indices': {}}
    }

reset.on_click(on_reset_click)

widgetBox = WidgetBox(children=[reset], width=150)
row = Row(children=[widgetBox, plot1, plot2])

document.add_root(row)
session.show(row)

if __name__ == "__main__":
    document.validate()
    print("\npress ctrl-C to exit")
    session.loop_until_closed()
```


Discussion:
Probably should make screen/data distance specs, that just happen to only have one valid unit. The following patch gets most of the way there, but tests fail due to new `size_units` props, etc. Maybe just update those tests. 

```
diff --git a/bokeh/core/properties.py b/bokeh/core/properties.py
index 42626c4be..b3cb68c47 100644
--- a/bokeh/core/properties.py
+++ b/bokeh/core/properties.py
@@ -1662,7 +1662,7 @@ class DistanceSpec(UnitsSpec):
             pass
         return super(DistanceSpec, self).prepare_value(cls, name, value)

-class ScreenDistanceSpec(NumberSpec):
+class ScreenDistanceSpec(UnitsSpec):
     ''' A |DataSpec| property that accepts numeric fixed values for screen
     distances, and also provides an associated units property that reports
     ``"screen"`` as the units.
@@ -1671,6 +1671,9 @@ class ScreenDistanceSpec(NumberSpec):
         Units are always ``"screen"``.

     '''
+    def __init__(self, default=None, help=None):
+        super(ScreenDistanceSpec, self).__init__(default=default, units_type=Enum(enums.enumeration("screen")), units_default="screen", help=help)
+
     def prepare_value(self, cls, name, value):
         try:
             if value is not None and value < 0:
@@ -1679,12 +1682,7 @@ class ScreenDistanceSpec(NumberSpec):
             pass
         return super(ScreenDistanceSpec, self).prepare_value(cls, name, value)

-    def to_serializable(self, obj, name, val):
-        d = super(ScreenDistanceSpec, self).to_serializable(obj, name, val)
-        d["units"] = "screen"
-        return d
-
-class DataDistanceSpec(NumberSpec):
+class DataDistanceSpec(UnitsSpec):
     ''' A |DataSpec| property that accepts numeric fixed values for data-space
     distances, and also provides an associated units property that reports
     ``"data"`` as the units.
@@ -1693,6 +1691,10 @@ class DataDistanceSpec(NumberSpec):
         Units are always ``"data"``.

     '''
+    def __init__(self, default=None, help=None):
+        super(DataDistanceSpec, self).__init__(default=default, units_type=Enum(enums.enumeration("data")), units_default="data", help=help)
+
+
     def prepare_value(self, cls, name, value):
         try:
             if value is not None and value < 0:
@@ -1701,11 +1703,6 @@ class DataDistanceSpec(NumberSpec):
             pass
         return super(DataDistanceSpec, self).prepare_value(cls, name, value)

-    def to_serializable(self, obj, name, val):
-        d = super(DataDistanceSpec, self).to_serializable(obj, name, val)
-        d["units"] = "data"
-        return d
-
 class ColorSpec(DataSpec):
     ''' A |DataSpec| property that accepts |Color| fixed values.

```


Resolving patch:
diff --git a/bokeh/core/properties.py b/bokeh/core/properties.py
--- a/bokeh/core/properties.py
+++ b/bokeh/core/properties.py
@@ -1731,7 +1731,7 @@ def prepare_value(self, cls, name, value):
             pass
         return super(DistanceSpec, self).prepare_value(cls, name, value)
 
-class ScreenDistanceSpec(NumberSpec):
+class ScreenDistanceSpec(UnitsSpec):
     ''' A |DataSpec| property that accepts numeric fixed values for screen
     distances, and also provides an associated units property that reports
     ``"screen"`` as the units.
@@ -1740,6 +1740,10 @@ class ScreenDistanceSpec(NumberSpec):
         Units are always ``"screen"``.
 
     '''
+
+    def __init__(self, default=None, help=None):
+        super(ScreenDistanceSpec, self).__init__(default=default, units_type=Enum(enums.enumeration("screen")), units_default="screen", help=help)
+
     def prepare_value(self, cls, name, value):
         try:
             if value is not None and value < 0:
@@ -1748,12 +1752,38 @@ def prepare_value(self, cls, name, value):
             pass
         return super(ScreenDistanceSpec, self).prepare_value(cls, name, value)
 
+    def make_descriptors(self, base_name):
+        ''' Return a list of ``PropertyDescriptor`` instances to install on a
+        class, in order to delegate attribute access to this property.
+
+        Unlike simpler property types, ``UnitsSpec`` returns multiple
+        descriptors to install. In particular, descriptors for the base
+        property as well as the associated units property are returned.
+
+        Args:
+            name (str) : the name of the property these descriptors are for
+
+        Returns:
+            list[PropertyDescriptor]
+
+        The descriptors returned are collected by the ``MetaHasProps``
+        metaclass and added to ``HasProps`` subclasses during class creation.
+        '''
+        units_props = self._units_type.make_descriptors("unused")
+        return [ UnitsSpecPropertyDescriptor(base_name, self, units_props[0]) ]
+
     def to_serializable(self, obj, name, val):
-        d = super(ScreenDistanceSpec, self).to_serializable(obj, name, val)
-        d["units"] = "screen"
+        d = super(UnitsSpec, self).to_serializable(obj, name, val)
+        if d is not None and 'units' not in d:
+            # d is a PropertyValueDict at this point, we need to convert it to
+            # a plain dict if we are going to modify its value, otherwise a
+            # notify_change that should not happen will be triggered
+            d = dict(d)
+            d["units"] = "screen"
         return d
 
-class DataDistanceSpec(NumberSpec):
+
+class DataDistanceSpec(UnitsSpec):
     ''' A |DataSpec| property that accepts numeric fixed values for data-space
     distances, and also provides an associated units property that reports
     ``"data"`` as the units.
@@ -1762,6 +1792,9 @@ class DataDistanceSpec(NumberSpec):
         Units are always ``"data"``.
 
     '''
+    def __init__(self, default=None, help=None):
+        super(DataDistanceSpec, self).__init__(default=default, units_type=Enum(enums.enumeration("data")), units_default="data", help=help)
+
     def prepare_value(self, cls, name, value):
         try:
             if value is not None and value < 0:
@@ -1770,9 +1803,34 @@ def prepare_value(self, cls, name, value):
             pass
         return super(DataDistanceSpec, self).prepare_value(cls, name, value)
 
+    def make_descriptors(self, base_name):
+        ''' Return a list of ``PropertyDescriptor`` instances to install on a
+        class, in order to delegate attribute access to this property.
+
+        Unlike simpler property types, ``UnitsSpec`` returns multiple
+        descriptors to install. In particular, descriptors for the base
+        property as well as the associated units property are returned.
+
+        Args:
+            name (str) : the name of the property these descriptors are for
+
+        Returns:
+            list[PropertyDescriptor]
+
+        The descriptors returned are collected by the ``MetaHasProps``
+        metaclass and added to ``HasProps`` subclasses during class creation.
+        '''
+        units_props = self._units_type.make_descriptors("unused")
+        return [ UnitsSpecPropertyDescriptor(base_name, self, units_props[0]) ]
+
     def to_serializable(self, obj, name, val):
-        d = super(DataDistanceSpec, self).to_serializable(obj, name, val)
-        d["units"] = "data"
+        d = super(UnitsSpec, self).to_serializable(obj, name, val)
+        if d is not None and 'units' not in d:
+            # d is a PropertyValueDict at this point, we need to convert it to
+            # a plain dict if we are going to modify its value, otherwise a
+            # notify_change that should not happen will be triggered
+            d = dict(d)
+            d["units"] = "data"
         return d
 
 class ColorSpec(DataSpec):


Test patch:
diff --git a/bokeh/core/tests/test_properties.py b/bokeh/core/tests/test_properties.py
--- a/bokeh/core/tests/test_properties.py
+++ b/bokeh/core/tests/test_properties.py
@@ -13,7 +13,7 @@
     Regex, Seq, List, Dict, Tuple, Instance, Any, Interval, Either,
     Enum, Color, DashPattern, Size, Percent, Angle, AngleSpec, StringSpec,
     DistanceSpec, FontSizeSpec, Override, Include, MinMaxBounds,
-    DataDistanceSpec, ScreenDistanceSpec, ColumnData)
+    DataDistanceSpec, ScreenDistanceSpec, ColumnData, UnitsSpec)
 
 from bokeh.core.property.containers import PropertyValueColumnData, PropertyValueDict, PropertyValueList
 
@@ -1769,18 +1769,35 @@ class Foo(HasProps):
             f.x = dict(field="foo", units="junk")
 
 def test_dataspec_dict_to_serializable():
-    for typ in (NumberSpec, StringSpec, FontSizeSpec, ColorSpec, DataDistanceSpec, ScreenDistanceSpec):
+    for typ in (NumberSpec, StringSpec, FontSizeSpec, ColorSpec):
         class Foo(HasProps):
             x = typ("x")
         foo = Foo(x=dict(field='foo'))
         props = foo.properties_with_values(include_defaults=False)
-        if typ is DataDistanceSpec:
-            assert props['x']['units'] == 'data'
-        elif typ is ScreenDistanceSpec:
-            assert props['x']['units'] == 'screen'
         assert props['x']['field'] == 'foo'
         assert props['x'] is not foo.x
 
+def test_DataDistanceSpec():
+    assert issubclass(DataDistanceSpec, UnitsSpec)
+    class Foo(HasProps):
+        x = DataDistanceSpec("x")
+    foo = Foo(x=dict(field='foo'))
+    props = foo.properties_with_values(include_defaults=False)
+    assert props['x']['units'] == 'data'
+    assert props['x']['field'] == 'foo'
+    assert props['x'] is not foo.x
+
+def test_ScreenDistanceSpec():
+    assert issubclass(ScreenDistanceSpec, UnitsSpec)
+    class Foo(HasProps):
+        x = ScreenDistanceSpec("x")
+    foo = Foo(x=dict(field='foo'))
+    props = foo.properties_with_values(include_defaults=False)
+    assert props['x']['units'] == 'screen'
+    assert props['x']['field'] == 'foo'
+    assert props['x'] is not foo.x
+
+
 def test_strict_unitspec_key_values():
     class FooUnits(HasProps):
         x = DistanceSpec("x")