Repository: bokeh/bokeh

Issue:
Support CamelCase CSS names
Following on from a discussion on the mailling list, https://groups.google.com/a/continuum.io/forum/#!topic/bokeh/BVAPlqMyI1Y.

@bryevdv mentioned that "One thing to mention is that "Black" (capital B) is not recognized as a color, all color names are lower case."

But:
- The link we point to in the docs: http://www.w3schools.com/cssref/css_colornames.asp
- The link we go to in our OpenURL example: http://www.colors.commutercreative.com/
- And, the OpenURL example: https://github.com/bokeh/bokeh/blob/master/examples/glyphs/colors.py

all use CamelCase colors. 

I have been trying to use them and just assumed it was broken. 

Turns out we need to lowercase them - I can't see this in the docs.

Either we need a warning for this, or, and I think better, we should support the CamelCase which seems to be the convention.



Discussion:
I'm changing this to bug, because we appear to support them sometimes and not others.

``` python
from bokeh.io import output_notebook, show
from bokeh.plotting import figure
from bokeh.properties import value
output_notebook()
f = figure(x_range=(0,3), y_range=(0,4))
f.circle([1], [1], radius=0.2, fill_color=["LemonChiffon"])  # Works
f.circle([2], [1], radius=0.2, fill_color=["lemonchiffon"])  # Works

f.circle([1], [2], radius=0.2, fill_color="LemonChiffon")  # Does not work
f.circle([2], [2], radius=0.2, fill_color="lemonchiffon")  # Works

f.circle([1], [3], radius=0.2, fill_color=value("LemonChiffon"))  # Does not work
f.circle([2], [3], radius=0.2, fill_color=value("lemonchiffon"))  # Does not work

show(f)
```

![screenshot from 2015-07-14 19 08 55](https://cloud.githubusercontent.com/assets/1796208/8679603/d46a2426-2a5b-11e5-942a-b4fe2122d4a2.png)

(note the warning comes from the line `f.circle([1], [2], radius=0.2, fill_color="LemonChiffon")`)


Resolving patch:
diff --git a/bokeh/enums.py b/bokeh/enums.py
--- a/bokeh/enums.py
+++ b/bokeh/enums.py
@@ -7,9 +7,20 @@
 from . import colors, icons, palettes
 
 class Enumeration(object):
-    pass
 
-def enumeration(*values):
+    __slots__ = ()
+
+    def __contains__(self, value):
+        if not self._case_sensitive:
+            value = value.lower()
+        return value in self._values
+
+    def __str__(self):
+        return "Enumeration(%s)" % ", ".join(self._values)
+
+    __repr__ = __str__
+
+def enumeration(*values, **kwargs):
     if not (values and all(isinstance(value, string_types) and value for value in values)):
         raise ValueError("expected a non-empty sequence of strings, got %s" % values)
 
@@ -18,9 +29,9 @@ def enumeration(*values):
 
     attrs = dict([ (value, value) for value in values ])
     attrs.update({
-        "__slots__": [],
         "_values": list(values),
         "_default": values[0],
+        "_case_sensitive": kwargs.get("case_sensitive", True),
     })
 
     return type("Enumeration", (Enumeration,), attrs)()
@@ -44,7 +55,7 @@ def enumeration(*values):
 Orientation = enumeration("top_right", "top_left", "bottom_left", "bottom_right")
 DashPattern = enumeration("solid", "dashed", "dotted", "dotdash", "dashdot")
 ButtonType = enumeration("default", "primary", "success", "warning", "danger", "link")
-NamedColor = enumeration(*colors.__colors__)
+NamedColor = enumeration(*colors.__colors__, case_sensitive=False)
 NamedIcon = enumeration(*icons.__icons__)
 Palette = enumeration(*palettes.__palettes__)
 MapType = enumeration("satellite", "roadmap", "terrain", "hybrid")
diff --git a/bokeh/properties.py b/bokeh/properties.py
--- a/bokeh/properties.py
+++ b/bokeh/properties.py
@@ -861,7 +861,7 @@ def __str__(self):
     def __or__(self, other):
         return self.__class__(*(self.type_params + [other]), default=self._default, help=self.help)
 
-class Enum(Property):
+class Enum(String):
     """ An Enum with a list of allowed values. The first value in the list is
     the default value, unless a default is provided with the "default" keyword
     argument.
@@ -870,16 +870,21 @@ def __init__(self, enum, *values, **kwargs):
         if not (not values and isinstance(enum, enums.Enumeration)):
             enum = enums.enumeration(enum, *values)
 
-        self.allowed_values = enum._values
+        self._enum = enum
 
         default = kwargs.get("default", enum._default)
         help = kwargs.get("help")
+
         super(Enum, self).__init__(default=default, help=help)
 
+    @property
+    def allowed_values(self):
+        return self._enum._values
+
     def validate(self, value):
         super(Enum, self).validate(value)
 
-        if not (value is None or value in self.allowed_values):
+        if not (value is None or value in self._enum):
             raise ValueError("invalid value for %s: %r; allowed values are %s" % (self.name, value, nice_join(self.allowed_values)))
 
     def __str__(self):
@@ -1197,7 +1202,7 @@ def isconst(cls, arg):
         well-formed hexadecimal color value.
         """
         return isinstance(arg, string_types) and \
-               ((len(arg) == 7 and arg[0] == "#") or arg in enums.NamedColor._values)
+               ((len(arg) == 7 and arg[0] == "#") or arg in enums.NamedColor)
 
     @classmethod
     def is_color_tuple(cls, val):


Test patch:
diff --git a/bokeh/tests/test_properties.py b/bokeh/tests/test_properties.py
--- a/bokeh/tests/test_properties.py
+++ b/bokeh/tests/test_properties.py
@@ -210,6 +210,19 @@ class Foo(HasProps):
         self.assertEqual(f.col, "forestgreen")
         self.assertDictEqual(desc.to_dict(f), {"value": "forestgreen"})
 
+    def test_case_insensitive_named_value(self):
+        class Foo(HasProps):
+            col = ColorSpec("colorfield")
+        desc = Foo.__dict__["col"]
+        f = Foo()
+
+        f.col = "RED"
+        self.assertEqual(f.col, "RED")
+        self.assertDictEqual(desc.to_dict(f), {"value": "RED"})
+        f.col = "ForestGreen"
+        self.assertEqual(f.col, "ForestGreen")
+        self.assertDictEqual(desc.to_dict(f), {"value": "ForestGreen"})
+
     def test_named_value_set_none(self):
         class Foo(HasProps):
             col = ColorSpec("colorfield")
@@ -710,6 +723,13 @@ def test_Enum(self):
         self.assertFalse(prop.is_valid(" round"))
         self.assertFalse(prop.is_valid(" bevel"))
 
+        from bokeh.enums import NamedColor
+        prop = Enum(NamedColor)
+
+        self.assertTrue(prop.is_valid("red"))
+        self.assertTrue(prop.is_valid("Red"))
+        self.assertTrue(prop.is_valid("RED"))
+
     def test_Color(self):
         prop = Color()
 
@@ -744,7 +764,7 @@ def test_Color(self):
         self.assertFalse(prop.is_valid("#00AaFff"))
 
         self.assertTrue(prop.is_valid("blue"))
-        self.assertFalse(prop.is_valid("BLUE"))
+        self.assertTrue(prop.is_valid("BLUE"))
         self.assertFalse(prop.is_valid("foobar"))
 
     def test_Align(self):