Repository: bokeh/bokeh

Issue:
Plot title ignores sizing and appears italicized if number is provided but no units are specified
When setting `plot.title.text_font_size` to a number without providing any units (IE  `plot.title.text='20'`), the title defaults to 10pt and the text appears italicized regardless of the value set in plot.title.text_font_style (though it does respect the `plot.title.text_font_align` attribute). This behaviour doesn't match the behaviour if a non-number value is provided (which would cause the plot not to render).

If there is no default unit for sizing, I would expect that the behaviour would be the same as if I had provided a completely erroneous value (IE `plot.title.text_font_size='apples'`)



Discussion:
@mattpap what do you think about adding `validate` to `FontSizeSpec` here:

https://github.com/bokeh/bokeh/blob/master/bokehjs/src/coffee/core/properties.coffee#L257-L258

Also, why is the deprecation warning on the python side not getting triggered? When is `prepare_value` called? 

https://github.com/bokeh/bokeh/blob/master/bokeh/core/properties.py#L1726-L1733

It should tell users to use `value` or `[...]`


Resolving patch:
diff --git a/bokeh/core/properties.py b/bokeh/core/properties.py
--- a/bokeh/core/properties.py
+++ b/bokeh/core/properties.py
@@ -1549,7 +1549,7 @@ class FontSizeSpec(DataSpec):
     https://drafts.csswg.org/css-values/#lengths
 
     '''
-    _font_size_re = re.compile("^[0-9]+(\.[0-9]+)?(%|em|ex|ch|ic|rem|vw|vh|vi|vb|vmin|vmax|cm|mm|q|in|pc|pt|px)$", re.I)
+    _font_size_re = re.compile(r"^[0-9]+(.[0-9]+)?(%|em|ex|ch|ic|rem|vw|vh|vi|vb|vmin|vmax|cm|mm|q|in|pc|pt|px)$", re.I)
 
     def __init__(self, default, help=None, key_type=_FieldValueTransform):
         super(FontSizeSpec, self).__init__(key_type, List(String), default=default, help=help)
@@ -1559,6 +1559,18 @@ def prepare_value(self, cls, name, value):
             value = dict(value=value)
         return super(FontSizeSpec, self).prepare_value(cls, name, value)
 
+    def validate(self, value):
+        super(FontSizeSpec, self).validate(value)
+
+        if isinstance(value, dict) and 'value' in value:
+            value = value['value']
+
+        if isinstance(value, string_types):
+            if len(value) == 0:
+                raise ValueError("empty string is not a valid font size value")
+            elif value[0].isdigit() and self._font_size_re.match(value) is None:
+                raise ValueError("%r is not a valid font size value" % value)
+
 _FieldValueTransformUnits = Enum("field", "value", "transform", "units")
 
 class UnitsSpec(NumberSpec):


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
@@ -573,6 +573,39 @@ class Foo(HasProps):
             self.assertEqual(a.x, f)
             self.assertEqual(a.lookup('x').serializable_value(a), dict(field=f))
 
+    def test_bad_font_size_values(self):
+        class Foo(HasProps):
+            x = FontSizeSpec(default=None)
+
+        a = Foo()
+
+        with self.assertRaises(ValueError):
+            a.x = "6"
+
+        with self.assertRaises(ValueError):
+            a.x = 6
+
+        with self.assertRaises(ValueError):
+            a.x = ""
+
+    def test_fields(self):
+        class Foo(HasProps):
+            x = FontSizeSpec(default=None)
+
+        a = Foo()
+
+        a.x = "_120"
+        self.assertEqual(a.x, "_120")
+
+        a.x = dict(field="_120")
+        self.assertEqual(a.x, dict(field="_120"))
+
+        a.x = "foo"
+        self.assertEqual(a.x, "foo")
+
+        a.x = dict(field="foo")
+        self.assertEqual(a.x, dict(field="foo"))
+
 class TestAngleSpec(unittest.TestCase):
     def test_default_none(self):
         class Foo(HasProps):