Repository: bokeh/bokeh

Issue:
Support subtyping for view models
Desire to do:

```
class Figure(Plot):
    __model__ = "Figure"
    __view_model__ = "Plot"

    # no new properties allowed!

    # but new methods OK
    def foo(self, x, y):
        pass

    def bar(self):
        pass
```

Instances of `Figure` should serialize to `Plot`, because BokehJS does not and should not care or know about `Figure`, but it should be possible to deserialize back to `Figure` on the python side, as a nicety.

The would be technically accomplished by adding an attribute to the serialized JSON, alongside "type", that is completely ignored by BokehJS, but is used to choose the correct python model on deserialization. 

This capability would be immediately useful for `Figure` and also for a `Columns` object that the `bokeh.charts` interface could use to adapt various user inputs to, but still easily serialize to `ColumnDataSource`.

@mattpap @pzwang please comment. This is definitely technically feasible but I would like opinions on the merits. 



Discussion:
If we don't do this, the alternatives for the two cases above:
- `Figure` this could also be accomplished by having `Figure` contain a plot, `fig.plot` or something. This is not great, but perhaps not terrible. There would be a symmetry with `chart.plot` and that could be exploited in the documentation. 
- `Columns` here I think composition would be much clunkier to use so you'd be left with adding a bunch of methods and functionality to `ColumnDataSource` to make things work. 

Alternatively we add `Figure` and `Columns` as bona fide models on the JS side but I don't think this is advised. There are arguably too many already. Thinking more, if we did this, charts could also become `Plot` subtypes and that would be nice as well. 

If I were to clarify the use-case/justification it would be that subtypes enable alternative "stylized" creation patterns for the existing base objects. 

The `__model__` vs. `__view_model__` thing looks to me like premature optimisation. As to forbidding new properties, we could introduce `@final` class decorator, which would do necessary checks (perhaps a better named could be found to avoid confusion with Java's `final` keyword, which has different semantics from what is proposed here).  


Resolving patch:
diff --git a/bokeh/document.py b/bokeh/document.py
--- a/bokeh/document.py
+++ b/bokeh/document.py
@@ -230,7 +230,7 @@ def load(self, *objs, **kwargs):
 
         for obj in objs:
             obj_id = obj['attributes']['id'] # XXX: obj['id']
-            obj_type = obj['type']
+            obj_type = obj.get('subtype', obj['type'])
             obj_attrs = obj['attributes']
 
             if "doc" in obj_attrs:
diff --git a/bokeh/plot_object.py b/bokeh/plot_object.py
--- a/bokeh/plot_object.py
+++ b/bokeh/plot_object.py
@@ -34,10 +34,10 @@ def __new__(cls, class_name, bases, class_dict):
 
         # Create the new class
         newcls = super(Viewable,cls).__new__(cls, class_name, bases, class_dict)
-        entry = class_dict["__view_model__"]
+        entry = class_dict.get("__subtype__", class_dict["__view_model__"])
         # Add it to the reverse map, but check for duplicates first
         if entry in Viewable.model_class_reverse_map:
-            raise Warning("Duplicate __view_model__ declaration of '%s' for " \
+            raise Warning("Duplicate __view_model__ or __subtype__ declaration of '%s' for " \
                           "class %s.  Previous definition: %s" % \
                           (entry, class_name,
                            Viewable.model_class_reverse_map[entry]))
@@ -94,10 +94,18 @@ def __init__(self, **kwargs):
 
     @property
     def ref(self):
-        return {
-            'type': self.__view_model__,
-            'id': self._id,
-        }
+
+        if "__subtype__" in self.__class__.__dict__:
+            return {
+                'type': self.__view_model__,
+                'subtype': self.__subtype__,
+                'id': self._id,
+            }
+        else:
+            return {
+                'type': self.__view_model__,
+                'id': self._id,
+            }
 
     def setup_events(self):
         pass


Test patch: