Repository: bokeh/bokeh

Issue:
Add support for _repr_html_ to HasProps and Model
`_repr_html_` was originally implemented on `Plot` to render a plot immediately without need for `show()`, at al. This didn't work well, so the code was commented out to be revised in future. Recently, `_repr_pretty_` was implemented to get insight into objects' properties. This works nicely across the board, but could work better in the notebook.

`_repr_html` should be added, so that it produces a shortened version of what `_repr_pretty_` produces, with a `...` button which would expand object's properties, showing more `...` buttons for embedded objects.

This is a followup on #5153.



Discussion:
@damianavila, do you know if something like this was done or attempted in other projects? I will be implementing this, but I don't want to do it for numpy arrays, lists, dicts and so on. For now I didn't find anything useful around the internet and IPython's documentation, but I'm not sure if my questions are wrongly formulated or I'm the first one to want this kind of behavior.


Resolving patch:
diff --git a/bokeh/core/properties.py b/bokeh/core/properties.py
--- a/bokeh/core/properties.py
+++ b/bokeh/core/properties.py
@@ -61,11 +61,12 @@ class Model(HasProps):
 import inspect
 import numbers
 import re
+import sys
 import types
 from warnings import warn
 from operator import itemgetter
 
-from six import string_types, iteritems
+from six import string_types, iteritems, StringIO
 
 from ..util.dependencies import import_optional
 from ..util.future import with_metaclass
@@ -74,6 +75,7 @@ class Model(HasProps):
 from . import enums
 
 pd = import_optional('pandas')
+IPython = import_optional('IPython')
 
 def field(name):
     ''' Convenience function do explicitly mark a field specification for
@@ -918,12 +920,12 @@ def apply_theme(self, property_values):
     def unapply_theme(self):
         self.apply_theme(property_values=dict())
 
-    def pprint_props(self, indent=0):
-        """ Prints the properties of this object, nicely formatted """
-        for key, value in self.properties_with_values().items():
-            print("%s%s: %r" % ("  "*indent, key, value))
+    def __str__(self):
+        return "%s(...)" % self.__class__.__name__
+
+    __repr__ = __str__
 
-    def _repr_pretty_(self, p, cycle):
+    def _bokeh_repr_pretty_(self, p, cycle):
         name = "%s.%s" % (self.__class__.__module__, self.__class__.__name__)
 
         if cycle:
@@ -943,6 +945,38 @@ def _repr_pretty_(self, p, cycle):
                     p.text('=')
                     p.pretty(value)
 
+    def pretty(self, verbose=False, max_width=79, newline='\n'):
+        """ Pretty print the object's representation. """
+        if not IPython:
+            cls = self.__class.__
+            raise RuntimeError("%s.%s.pretty() requires IPython" % (cls.__module__, cls.__name__))
+        else:
+            stream = StringIO()
+            printer = BokehPrettyPrinter(stream, verbose, max_width, newline)
+            printer.pretty(self)
+            printer.flush()
+            return stream.getvalue()
+
+    def pprint(self, verbose=False, max_width=79, newline='\n'):
+        """ Like `pretty` but print to stdout. """
+        if not IPython:
+            cls = self.__class.__
+            raise RuntimeError("%s.%s.pretty() requires IPython" % (cls.__module__, cls.__name__))
+        else:
+            printer = BokehPrettyPrinter(sys.stdout, verbose, max_width, newline)
+            printer.pretty(self)
+            printer.flush()
+            sys.stdout.write(newline)
+            sys.stdout.flush()
+
+if IPython:
+    from IPython.lib.pretty import RepresentationPrinter
+
+    class BokehPrettyPrinter(RepresentationPrinter):
+        def __init__(self, output, verbose=False, max_width=79, newline='\n'):
+            super(BokehPrettyPrinter, self).__init__(output, verbose, max_width, newline)
+            self.type_pprinters[HasProps] = lambda obj, p, cycle: obj._bokeh_repr_pretty_(p, cycle)
+
 class PrimitiveProperty(PropertyDescriptor):
     """ A base class for simple property types.
 
diff --git a/bokeh/model.py b/bokeh/model.py
--- a/bokeh/model.py
+++ b/bokeh/model.py
@@ -344,10 +344,11 @@ def to_json_string(self, include_defaults):
         return serialize_json(json_like)
 
     def __str__(self):
-        return "%s, ViewModel:%s, ref _id: %s" % (self.__class__.__name__,
-                self.__view_model__, getattr(self, "_id", None))
+        return "%s(id=%r, ...)" % (self.__class__.__name__, getattr(self, "_id", None))
 
-    def _repr_pretty_(self, p, cycle):
+    __repr__ = __str__
+
+    def _bokeh_repr_pretty_(self, p, cycle):
         name = "%s.%s" % (self.__class__.__module__, self.__class__.__name__)
         _id = getattr(self, "_id", None)
 
@@ -371,6 +372,58 @@ def _repr_pretty_(self, p, cycle):
                     p.text('=')
                     p.pretty(value)
 
+    def _repr_html_(self):
+        module = self.__class__.__module__
+        name = self.__class__.__name__
+
+        _id = getattr(self, "_id", None)
+
+        cls_name = make_id()
+
+        def row(c):
+            return '<div style="display: table-row;">' + c + '</div>'
+        def hidden_row(c):
+            return '<div class="%s" style="display: none;">%s</div>' % (cls_name, c)
+        def cell(c):
+            return '<div style="display: table-cell;">' + c + '</div>'
+
+        html = ''
+        html += '<div style="display: table;">'
+
+        ellipsis_id = make_id()
+        ellipsis = '<span id="%s" style="cursor: pointer;">&hellip;)</span>' % ellipsis_id
+
+        prefix = cell('<b title="%s.%s">%s</b>(' % (module, name, name))
+        html += row(prefix + cell('id' + '&nbsp;=&nbsp;' + repr(_id) + ', ' + ellipsis))
+
+        props = self.properties_with_values().items()
+        sorted_props = sorted(props, key=itemgetter(0))
+        all_props = sorted_props
+        for i, (prop, value) in enumerate(all_props):
+            end = ')' if i == len(all_props)-1 else ','
+            html += hidden_row(cell("") + cell(prop + '&nbsp;=&nbsp;' + repr(value) + end))
+
+        html += '</div>'
+        html += """
+<script>
+(function() {
+  var expanded = false;
+  var ellipsis = document.getElementById("%(ellipsis_id)s");
+  ellipsis.addEventListener("click", function() {
+    var rows = document.getElementsByClassName("%(cls_name)s");
+    for (var i = 0; i < rows.length; i++) {
+      var el = rows[i];
+      el.style.display = expanded ? "none" : "table-row";
+    }
+    ellipsis.innerHTML = expanded ? "&hellip;)" : "&lsaquo;&lsaquo;&lsaquo;";
+    expanded = !expanded;
+  });
+})();
+</script>
+""" % dict(ellipsis_id=ellipsis_id, cls_name=cls_name)
+
+        return html
+
 def _find_some_document(models):
     from .document import Document
 
diff --git a/bokeh/models/layouts.py b/bokeh/models/layouts.py
--- a/bokeh/models/layouts.py
+++ b/bokeh/models/layouts.py
@@ -66,15 +66,10 @@ class LayoutDOM(Model):
 
     """)
 
-    # TODO: (mp) Not yet, because it breaks plotting/notebook examples.
-    # Rename to _repr_html_ if we decide to enable this by default.
-    def __repr_html__(self):
-        return notebook_div(self)
-
     @property
     def html(self):
         from IPython.core.display import HTML
-        return HTML(self.__repr_html__())
+        return HTML(notebook_div(self))
 
 
 class Spacer(LayoutDOM):


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
@@ -15,8 +15,6 @@
 from bokeh.models import Plot
 from bokeh.models.annotations import Title
 
-from IPython.lib.pretty import pretty
-
 class Basictest(unittest.TestCase):
 
     def test_simple_class(self):
@@ -1514,19 +1512,19 @@ def test_HasProps_clone():
     c2 = p2.properties_with_values(include_defaults=False)
     assert c1 == c2
 
-def test_HasProps__repr_pretty_():
+def test_HasProps_pretty():
     class Foo1(HasProps):
         a = Int(12)
         b = String("hello")
 
-    assert pretty(Foo1()) == "bokeh.core.tests.test_properties.Foo1(a=12, b='hello')"
+    assert Foo1().pretty() == "bokeh.core.tests.test_properties.Foo1(a=12, b='hello')"
 
     class Foo2(HasProps):
         a = Int(12)
         b = String("hello")
         c = List(Int, [1, 2, 3])
 
-    assert pretty(Foo2()) == "bokeh.core.tests.test_properties.Foo2(a=12, b='hello', c=[1, 2, 3])"
+    assert Foo2().pretty() == "bokeh.core.tests.test_properties.Foo2(a=12, b='hello', c=[1, 2, 3])"
 
     class Foo3(HasProps):
         a = Int(12)
@@ -1534,7 +1532,7 @@ class Foo3(HasProps):
         c = List(Int, [1, 2, 3])
         d = Float(None)
 
-    assert pretty(Foo3()) == "bokeh.core.tests.test_properties.Foo3(a=12, b='hello', c=[1, 2, 3], d=None)"
+    assert Foo3().pretty() == "bokeh.core.tests.test_properties.Foo3(a=12, b='hello', c=[1, 2, 3], d=None)"
 
     class Foo4(HasProps):
         a = Int(12)
@@ -1543,7 +1541,7 @@ class Foo4(HasProps):
         d = Float(None)
         e = Instance(Foo1, lambda: Foo1())
 
-    assert pretty(Foo4()) == """\
+    assert Foo4().pretty() == """\
 bokeh.core.tests.test_properties.Foo4(
     a=12,
     b='hello',
@@ -1561,7 +1559,7 @@ class Foo6(HasProps):
     f6 = Foo6(foo5=f5)
     f5.foo6 = f6
 
-    assert pretty(f5) == """\
+    assert f5.pretty() == """\
 bokeh.core.tests.test_properties.Foo5(
     foo6=bokeh.core.tests.test_properties.Foo6(
         foo5=bokeh.core.tests.test_properties.Foo5(...)))"""
diff --git a/bokeh/models/tests/test_renderers.py b/bokeh/models/tests/test_renderers.py
--- a/bokeh/models/tests/test_renderers.py
+++ b/bokeh/models/tests/test_renderers.py
@@ -5,7 +5,6 @@
 from bokeh.plotting import figure
 from bokeh.models.ranges import DataRange1d
 
-
 class TestGlyphRenderer(unittest.TestCase):
     def test_warning_about_colons_in_column_labels_for_axis(self):
         invalid_labels = ['0', '1', '2:0']
@@ -24,8 +23,7 @@ def test_warning_about_colons_in_column_labels_for_axis(self):
             'Category labels cannot contain colons',
             '[range:x_range] [first_value: 2:0] '
             '[range:y_range] [first_value: 2:0] '
-            '[renderer: Figure, ViewModel:Plot, ref _id: '
-            '%s]' % plot._id
+            '[renderer: Figure(id=%r, ...)]' % plot._id
         )])
 
     def test_validates_colons_only_in_factorial_range(self):
@@ -43,12 +41,8 @@ def test_validates_colons_only_in_factorial_range(self):
             'MALFORMED_CATEGORY_LABEL',
             'Category labels cannot contain colons',
             '[range:y_range] [first_value: 2:0] '
-            '[renderer: Figure, ViewModel:Plot, ref _id: '
-            '%s]' % plot._id
+            '[renderer: Figure(id=%r, ...)]' % plot._id
         )])
 
-
-
-
 if __name__ == '__main__':
     unittest.main()
diff --git a/bokeh/tests/test_model.py b/bokeh/tests/test_model.py
--- a/bokeh/tests/test_model.py
+++ b/bokeh/tests/test_model.py
@@ -3,8 +3,6 @@
 from bokeh.plotting import figure
 from bokeh.io import curdoc
 
-from IPython.lib.pretty import pretty
-
 def test_model_in_empty_document_sets_a_new_document_on_model_and_then_restores():
     doc = curdoc()
     plot = figure()
@@ -35,19 +33,19 @@ def test_model_in_empty_document_unsets_curdoc_on_model_references_and_then_rest
     # Check old document is replaced
     assert a_ref._document == doc
 
-def test_Model__repr_pretty_():
+def test_Model_pretty():
     class Foo1(Model):
         a = Int(12)
         b = String("hello")
 
-    assert pretty(Foo1(id='xyz')) == "bokeh.tests.test_model.Foo1(id='xyz', a=12, b='hello', name=None, tags=[])"
+    assert Foo1(id='xyz').pretty() == "bokeh.tests.test_model.Foo1(id='xyz', a=12, b='hello', name=None, tags=[])"
 
     class Foo2(Model):
         a = Int(12)
         b = String("hello")
         c = List(Int, [1, 2, 3])
 
-    assert pretty(Foo2(id='xyz')) == """\
+    assert Foo2(id='xyz').pretty() == """\
 bokeh.tests.test_model.Foo2(
     id='xyz',
     a=12,
@@ -62,7 +60,7 @@ class Foo3(Model):
         c = List(Int, [1, 2, 3])
         d = Float(None)
 
-    assert pretty(Foo3(id='xyz')) == """\
+    assert Foo3(id='xyz').pretty() == """\
 bokeh.tests.test_model.Foo3(
     id='xyz',
     a=12,
@@ -79,7 +77,7 @@ class Foo4(Model):
         d = Float(None)
         e = Instance(Foo1, lambda: Foo1(id='xyz'))
 
-    assert pretty(Foo4(id='xyz')) == """\
+    assert Foo4(id='xyz').pretty() == """\
 bokeh.tests.test_model.Foo4(
     id='xyz',
     a=12,
@@ -105,7 +103,7 @@ class Foo6(Model):
     f6 = Foo6(id='uvw', foo5=f5)
     f5.foo6 = f6
 
-    assert pretty(f5) == """\
+    assert f5.pretty() == """\
 bokeh.tests.test_model.Foo5(
     id='xyz',
     foo6=bokeh.tests.test_model.Foo6(