Repository: bokeh/bokeh

Issue:
GMapPlot Error AttributeError("'BasicProperty' object has no attribute 'from_json'",)
possibly  a bug in the serialization of MapOptions 

<img width="988" alt="screen shot 2016-09-06 at 9 57 33 am" src="https://cloud.githubusercontent.com/assets/7247772/18350802/3606acf8-759d-11e6-871a-a91007a5001f.png">
<img width="835" alt="screen shot 2016-09-06 at 9 59 17 am" src="https://cloud.githubusercontent.com/assets/7247772/18350801/3601ab5e-759d-11e6-9ca9-7cd14410c55f.png">

Bokeh version 0.12.1 
 OSX 10.11.6 

 I am getting an AttributeError("'BasicProperty' object has no attribute 'from_json'",) and I don't know where its coming from. Attached are the log files from terminal and pycharm. 

 Here is some example code that gives the same error. 

```
 from bokeh.plotting import Figure, ColumnDataSource, show, vplot 
 from bokeh.io import output_file 
 from bokeh.models import (Slider, CustomJS, GMapPlot, 
                           GMapOptions, DataRange1d, Circle, Line) 
 import numpy as np 
 from bokeh.plotting import curdoc, figure 
 from bokeh.layouts import row, column, widgetbox 

 from bokeh.client import push_session 

 output_file("path.html") 

 # Create path around roundabout 
 r = 0.000192 

 x1 = np.linspace(-1,1,100)*r 
 x2 = np.linspace(1,-1,100)*r 
 x = np.hstack((x1,x2)) 

 f = lambda x : np.sqrt(r**2 - x**2) 

 y1 = f(x1) 
 y2 = -f(x2) 
 y = np.hstack((y1,y2)) 

 init_x = 40.233688 
 init_y = -111.646784 

 lon = init_x + x 
 lat = init_y + y 

 # Initialize data sources. 
 location = ColumnDataSource(data=dict(x=[lon[0]], y=[lat[0]])) 
 path = ColumnDataSource(data=dict(x=lon, y=lat)) 

 # Initialize figure, path, and point 
 """I haven't been able to verify that the GMapPlot code below works, but 
 this should be the right thing to do. The zoom may be totally wrong, 
 but my latlng points should be a path around a roundabout. 
 """ 
 options = GMapOptions(lat=40.233681, lng=-111.646595, map_type="roadmap", zoom=15) 
 fig = GMapPlot(x_range=DataRange1d(), y_range=DataRange1d(), map_options=options,api_key='XXXXXXXXXXXXXXXX') 

 # fig = Figure(plot_height=600, plot_width=600) 

 c = Circle(x='x', y='y', size=10) 
 p = Line(x='x', y='y') 

 fig.add_glyph(location, c) 
 fig.add_glyph(path, p) 

 # Slider callback 
 callback = CustomJS(args=dict(location=location, path=path), code=""" 
     var loc = location.get('data'); 
     var p = path.get('data'); 

     t = cb_obj.get('value'); 

     /* set the point location to the path location that 
        corresponds to the slider position */ 
     loc['x'][0] = p['x'][t]; 
     loc['y'][0] = p['y'][t]; 

     location.trigger('change'); 
 """) 

 # The way I have written this, 'start' has to be 0 and 
 #  'end' has to be the length of the array of path points. 
 slider = Slider(start=0, end=200, step=1, callback=callback) 

 layout = column(fig, slider) 
 curdoc().clear() 
 curdoc().add_root(layout) 
 session = push_session(curdoc()) 

 session.show() 
 session.loop_until_closed() 
```

Sarah Bird - Response 

I'm not sure what's going on, but I've started a little debugging

If you run bokeh-serve with --log-level=debug

You see the source of the error is:
  File "/Users/caged/Dev/bokeh/bokeh/bokeh/core/properties.py", line 1236, in from_json
    attrs[name] = prop.from_json(value, models)
AttributeError: 'BasicProperty' object has no attribute 'from_json'

I stuck a print statement in to print out "prop" and it always prints out a GMapOptions value - although not always the same one - which makes sense because GMapOptions a dictionary.

e.g.

```
zoom:Int
map_type:Enum('satellite', 'roadmap', 'terrain', 'hybrid')
lng:Float
lat:Float
```

I think there's some kind of serialization problem with GMapPlot and the server (although maybe it's wider). Hopefully Bryan has some thoughts.

Bryan Van de Ven - Response

Based on Sarah's observations, I'd agree this seems like a bug that is particular to the serialization of MapOptions. In which case the next step is to file a bug report on GitHub. Fixing it will probably take some time, however, as there are many priorities and few people to work on them. As an immediate workaround I would suggest looking at using a regular Plot with a TileProvider layer:

```
http://bokeh.pydata.org/en/latest/docs/user_guide/geo.html#tile-providers
```

There are a few tile providers built-in already:

```
http://bokeh.pydata.org/en/latest/docs/reference/tile_providers.html
```

CartoDB is probably a good choice. 



Discussion:

Resolving patch:
diff --git a/bokeh/core/properties.py b/bokeh/core/properties.py
--- a/bokeh/core/properties.py
+++ b/bokeh/core/properties.py
@@ -702,6 +702,18 @@ def __init__(self, **properties):
         for name, value in properties.items():
             setattr(self, name, value)
 
+    def equals(self, other):
+        """ Structural equality of models. """
+        # NOTE: don't try to use this to implement __eq__. Because then
+        # you will be tempted to implement __hash__, which would interfere
+        # with mutability of models. However, not implementing __hash__
+        # will make bokeh unusable in Python 3, where proper implementation
+        # of __hash__ is required when implementing __eq__.
+        if not isinstance(other, self.__class__):
+            return False
+        else:
+            return self.properties_with_values() == other.properties_with_values()
+
     def __setattr__(self, name, value):
         # self.properties() below can be expensive so avoid it
         # if we're just setting a private underscore field
@@ -1231,7 +1243,7 @@ def from_json(self, json, models=None):
                 attrs = {}
 
                 for name, value in iteritems(json):
-                    prop = self.instance_type.lookup(name)
+                    prop = self.instance_type.lookup(name).descriptor
                     attrs[name] = prop.from_json(value, models)
 
                 # XXX: this doesn't work when Instance(Superclass) := Subclass()


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
@@ -1161,6 +1161,16 @@ def test_Instance(self):
         self.assertFalse(prop.is_valid(Bar()))
         self.assertFalse(prop.is_valid(Baz()))
 
+    def test_Instance_from_json(self):
+        class MapOptions(HasProps):
+            lat = Float
+            lng = Float
+            zoom = Int(12)
+
+        v1 = Instance(MapOptions).from_json(dict(lat=1, lng=2))
+        v2 = MapOptions(lat=1, lng=2)
+        self.assertTrue(v1.equals(v2))
+
     def test_Interval(self):
         with self.assertRaises(TypeError):
             prop = Interval()
@@ -1469,6 +1479,31 @@ def test_MinMaxBounds_with_datetime(self):
         # Invalid values
         self.assertFalse(prop.is_valid((datetime.date(2012, 10, 1), 22)))
 
+def test_HasProps_equals():
+    class Foo(HasProps):
+        x = Int(12)
+        y = String("hello")
+        z = List(Int, [1,2,3])
+
+    class FooUnrelated(HasProps):
+        x = Int(12)
+        y = String("hello")
+        z = List(Int, [1,2,3])
+
+    v = Foo().equals(Foo())
+    assert v is True
+
+    v = Foo(x=1).equals(Foo(x=1))
+    assert v is True
+
+    v = Foo(x=1).equals(Foo(x=2))
+    assert v is False
+
+    v = Foo(x=1).equals(1)
+    assert v is False
+
+    v = Foo().equals(FooUnrelated())
+    assert v is False
 
 def test_HasProps_clone():
     p1 = Plot(plot_width=1000)
@@ -1477,7 +1512,6 @@ def test_HasProps_clone():
     c2 = p2.properties_with_values(include_defaults=False)
     assert c1 == c2
 
-
 def test_titleprop_transforms_string_into_title_object():
     class Foo(HasProps):
         title = TitleProp