Repository: bokeh/bokeh

Issue:
MapOptions JSON encoding problem
When modifying the map.py example to push the plot to a server instead of writing a flat html file, the following error occurs. 

```
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1817, in wsgi_app
    response = self.full_dispatch_request()
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1477, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1381, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1475, in full_dispatch_request
    rv = self.dispatch_request()
  File "/usr/local/lib/python2.7/dist-packages/flask/app.py", line 1461, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/usr/local/lib/python2.7/dist-packages/bokeh/server/views/bbauth.py", line 25, in wrapper
    return func(docid, *args, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/bokeh/server/views/backbone.py", line 125, in bulk_upsert
    clientdoc.load(*data, events='none', dirty=True)
  File "/usr/local/lib/python2.7/dist-packages/bokeh/document.py", line 250, in load
    m.update(**props)
  File "/usr/local/lib/python2.7/dist-packages/bokeh/plot_object.py", line 247, in update
    setattr(self, k, v)
  File "/usr/local/lib/python2.7/dist-packages/bokeh/properties.py", line 82, in __set__
    self.validate(value)
  File "/usr/local/lib/python2.7/dist-packages/bokeh/properties.py", line 821, in validate
    (self.instance_type.__name__, value, type(value).__name__))
ValueError: expected an instance of type MapOptions, got {u'lat': 30.2861, u'map_type': u'satellite', u'lng': -97.7394, u'zoom': 15} of type dict

**Code:**

document = Document()
session = Session()
output_server('maps', session=session)

x_range = Range1d()
y_range = Range1d()

map_options = MapOptions(lat=30.2861, lng=-97.7394, zoom=15)

plot = GMapPlot(
    x_range=x_range, y_range=y_range,
    map_options=map_options,
    title = "Austin"
)
plot.map_options.map_type="hybrid"

source = ColumnDataSource(
    data=dict(
        lat=[30.2861, 30.2855, 30.2869],
        lon=[-97.7394, -97.7390, -97.7405],
        fill=['orange', 'blue', 'green']
    )
)
document.add(plot)
session.store_document(document)
```



Discussion:

Resolving patch:
diff --git a/bokeh/models/ranges.py b/bokeh/models/ranges.py
--- a/bokeh/models/ranges.py
+++ b/bokeh/models/ranges.py
@@ -15,11 +15,6 @@ class Range1d(Range):
 class DataRange(Range):
     sources = List(Instance(ColumnsRef))
 
-    def finalize(self, models):
-        props = super(DataRange, self).finalize(models)
-        props['sources'] = [ ColumnsRef(**source) for source in props['sources'] ]
-        return props
-
 class DataRange1d(DataRange):
     """ Represents an auto-fitting range in a scalar dimension. """
     rangepadding = Float(0.1)
diff --git a/bokeh/plot_object.py b/bokeh/plot_object.py
--- a/bokeh/plot_object.py
+++ b/bokeh/plot_object.py
@@ -162,10 +162,13 @@ def finalize(self, models):
         """Convert any references into instances
         models is a dict of id->model mappings
         """
-        if hasattr(self, "_ref_props"):
-            return resolve_json(self._ref_props, models)
-        else:
-            return {}
+        attrs = {}
+
+        for name, json in iteritems(getattr(self, "_ref_props", {})):
+            prop = self.__class__.lookup(name)
+            attrs[name] = prop.from_json(json, models=models)
+
+        return attrs
 
     @classmethod
     def collect_plot_objects(cls, *input_objs):
diff --git a/bokeh/properties.py b/bokeh/properties.py
--- a/bokeh/properties.py
+++ b/bokeh/properties.py
@@ -24,6 +24,9 @@
 class _NotSet(object):
     pass
 
+class DeserializationError(Exception):
+    pass
+
 class Property(object):
     ''' Base class for all type properties. '''
     def __init__(self, default=None):
@@ -61,6 +64,9 @@ def matches(self, new, old):
             logger.debug("could not compare %s and %s for property %s (Reason: %s)", new, old, self.name, e)
         return False
 
+    def from_json(self, json, models=None):
+        return json
+
     def transform(self, value):
         return value
 
@@ -500,6 +506,10 @@ def clone(self):
         """
         return self.__class__(**self.to_dict())
 
+    @classmethod
+    def lookup(cls, name):
+        return lookup_descriptor(cls, name)
+
     @classmethod
     def properties_with_refs(cls):
         """ Returns a set of the names of this object's properties that
@@ -701,6 +711,14 @@ def __get__(self, obj, type=None):
         setattr(obj, self._name, val)
         return val
 
+    def from_json(self, json, models=None):
+        if json is None:
+            return None
+        elif isinstance(json, list):
+            return [ self.item_type.from_json(item, models) for item in json ]
+        else:
+            raise DeserializationError("%s expected a list or None, got %s" % (self, json))
+
 class Dict(ContainerProperty):
     """ If a default value is passed in, then a shallow copy of it will be
     used for each new use of this property.
@@ -733,6 +751,14 @@ def validate(self, value):
     def __str__(self):
         return "%s(%s, %s)" % (self.__class__.__name__, self.keys_type, self.values_type)
 
+    def from_json(self, json, models=None):
+        if json is None:
+            return None
+        elif isinstance(json, dict):
+            return { self.keys_type.from_json(key, models): self.values_type.from_json(value, models) for key, value in iteritems(json) }
+        else:
+            raise DeserializationError("%s expected a dict or None, got %s" % (self, json))
+
 class Tuple(ContainerProperty):
     ''' Tuple type property. '''
     def __init__(self, tp1, tp2, *type_params, **kwargs):
@@ -754,6 +780,14 @@ def validate(self, value):
     def __str__(self):
         return "%s(%s)" % (self.__class__.__name__, ", ".join(map(str, self.type_params)))
 
+    def from_json(self, json, models=None):
+        if json is None:
+            return None
+        elif isinstance(json, list):
+            return tuple(type_param.from_json(item, models) for type_param, item in zip(self.type_params, json))
+        else:
+            raise DeserializationError("%s expected a list or None, got %s" % (self, json))
+
 class Array(ContainerProperty):
     """ Whatever object is passed in as a default value, np.asarray() is
     called on it to create a copy for the default value for each use of
@@ -823,6 +857,34 @@ def validate(self, value):
     def __str__(self):
         return "%s(%s)" % (self.__class__.__name__, self.instance_type.__name__)
 
+    def from_json(self, json, models=None):
+        if json is None:
+            return None
+        elif isinstance(json, dict):
+            from .plot_object import PlotObject
+            if issubclass(self.instance_type, PlotObject):
+                if models is None:
+                    raise DeserializationError("%s can't deserialize without models" % self)
+                else:
+                    model = models.get(json["id"])
+
+                    if model is not None:
+                        return model
+                    else:
+                        raise DeserializationError("% failed to deserilize reference to %s" % (self, json))
+            else:
+                attrs = {}
+
+                for name, value in iteritems(json):
+                    prop = self.instance_type.lookup(name)
+                    attrs[name] = prop.from_json(value, models)
+
+                # XXX: this doesn't work when Instance(Superclass) := Subclass()
+                # Serialization dict must carry type information to resolve this.
+                return self.instance_type(**attrs)
+        else:
+            raise DeserializationError("%s expected a dict or None, got %s" % (self, json))
+
 class This(Property):
     """ A reference to an instance of the class being defined. """
     pass


Test patch: