Repository: bokeh/bokeh

Issue:
Implement _repr_pretty_ on HasProps and Model
This is IPython dependent, but who uses native Python's REPL anyway. This is beneficial over implementing `__str__` (and `__repr__`), because those two in Python are broken by design. Additionally, we get cycle detection for free, pretty printing (e.g. line breaking) and integration with pretty printing of other data structures (e.g. `list`, `dict`).



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
@@ -63,6 +63,7 @@ class Model(HasProps):
 import re
 import types
 from warnings import warn
+from operator import itemgetter
 
 from six import string_types, iteritems
 
@@ -922,6 +923,26 @@ def pprint_props(self, indent=0):
         for key, value in self.properties_with_values().items():
             print("%s%s: %r" % ("  "*indent, key, value))
 
+    def _repr_pretty_(self, p, cycle):
+        name = "%s.%s" % (self.__class__.__module__, self.__class__.__name__)
+
+        if cycle:
+            p.text("%s(...)" % name)
+        else:
+            with p.group(4, '%s(' % name, ')'):
+                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):
+                    if i == 0:
+                        p.breakable('')
+                    else:
+                        p.text(',')
+                        p.breakable()
+                    p.text(prop)
+                    p.text('=')
+                    p.pretty(value)
+
 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
@@ -5,6 +5,7 @@
 
 from contextlib import contextmanager
 from json import loads
+from operator import itemgetter
 
 from six import iteritems
 
@@ -346,6 +347,30 @@ def __str__(self):
         return "%s, ViewModel:%s, ref _id: %s" % (self.__class__.__name__,
                 self.__view_model__, getattr(self, "_id", None))
 
+    def _repr_pretty_(self, p, cycle):
+        name = "%s.%s" % (self.__class__.__module__, self.__class__.__name__)
+        _id = getattr(self, "_id", None)
+
+        if cycle:
+            p.text(name)
+            p.text('(id=')
+            p.pretty(_id)
+            p.text(', ...)')
+        else:
+            with p.group(4, '%s(' % name, ')'):
+                props = self.properties_with_values().items()
+                sorted_props = sorted(props, key=itemgetter(0))
+                all_props = [('id', _id)] + sorted_props
+                for i, (prop, value) in enumerate(all_props):
+                    if i == 0:
+                        p.breakable('')
+                    else:
+                        p.text(',')
+                        p.breakable()
+                    p.text(prop)
+                    p.text('=')
+                    p.pretty(value)
+
 def _find_some_document(models):
     from .document import Document
 


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,6 +15,8 @@
 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):
@@ -1512,6 +1514,58 @@ def test_HasProps_clone():
     c2 = p2.properties_with_values(include_defaults=False)
     assert c1 == c2
 
+def test_HasProps__repr_pretty_():
+    class Foo1(HasProps):
+        a = Int(12)
+        b = String("hello")
+
+    assert pretty(Foo1()) == "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])"
+
+    class Foo3(HasProps):
+        a = Int(12)
+        b = String("hello")
+        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)"
+
+    class Foo4(HasProps):
+        a = Int(12)
+        b = String("hello")
+        c = List(Int, [1, 2, 3])
+        d = Float(None)
+        e = Instance(Foo1, lambda: Foo1())
+
+    assert pretty(Foo4()) == """\
+bokeh.core.tests.test_properties.Foo4(
+    a=12,
+    b='hello',
+    c=[1, 2, 3],
+    d=None,
+    e=bokeh.core.tests.test_properties.Foo1(a=12, b='hello'))"""
+
+    class Foo5(HasProps):
+        foo6 = Any            # can't use Instance(".core.tests.test_properties.Foo6")
+
+    class Foo6(HasProps):
+        foo5 = Instance(Foo5)
+
+    f5 = Foo5()
+    f6 = Foo6(foo5=f5)
+    f5.foo6 = f6
+
+    assert pretty(f5) == """\
+bokeh.core.tests.test_properties.Foo5(
+    foo6=bokeh.core.tests.test_properties.Foo6(
+        foo5=bokeh.core.tests.test_properties.Foo5(...)))"""
+
 def test_titleprop_transforms_string_into_title_object():
     class Foo(HasProps):
         title = TitleProp
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
@@ -1,7 +1,9 @@
-from bokeh.model import _ModelInEmptyDocument
+from bokeh.model import Model, _ModelInEmptyDocument
+from bokeh.core.properties import Int, String, Float, Instance, List, Any
 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()
@@ -32,3 +34,84 @@ 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_():
+    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=[])"
+
+    class Foo2(Model):
+        a = Int(12)
+        b = String("hello")
+        c = List(Int, [1, 2, 3])
+
+    assert pretty(Foo2(id='xyz')) == """\
+bokeh.tests.test_model.Foo2(
+    id='xyz',
+    a=12,
+    b='hello',
+    c=[1, 2, 3],
+    name=None,
+    tags=[])"""
+
+    class Foo3(Model):
+        a = Int(12)
+        b = String("hello")
+        c = List(Int, [1, 2, 3])
+        d = Float(None)
+
+    assert pretty(Foo3(id='xyz')) == """\
+bokeh.tests.test_model.Foo3(
+    id='xyz',
+    a=12,
+    b='hello',
+    c=[1, 2, 3],
+    d=None,
+    name=None,
+    tags=[])"""
+
+    class Foo4(Model):
+        a = Int(12)
+        b = String("hello")
+        c = List(Int, [1, 2, 3])
+        d = Float(None)
+        e = Instance(Foo1, lambda: Foo1(id='xyz'))
+
+    assert pretty(Foo4(id='xyz')) == """\
+bokeh.tests.test_model.Foo4(
+    id='xyz',
+    a=12,
+    b='hello',
+    c=[1, 2, 3],
+    d=None,
+    e=bokeh.tests.test_model.Foo1(
+        id='xyz',
+        a=12,
+        b='hello',
+        name=None,
+        tags=[]),
+    name=None,
+    tags=[])"""
+
+    class Foo5(Model):
+        foo6 = Any            # can't use Instance(".tests.test_model.Foo6")
+
+    class Foo6(Model):
+        foo5 = Instance(Foo5)
+
+    f5 = Foo5(id='xyz')
+    f6 = Foo6(id='uvw', foo5=f5)
+    f5.foo6 = f6
+
+    assert pretty(f5) == """\
+bokeh.tests.test_model.Foo5(
+    id='xyz',
+    foo6=bokeh.tests.test_model.Foo6(
+        id='uvw',
+        foo5=bokeh.tests.test_model.Foo5(id='xyz', ...),
+        name=None,
+        tags=[]),
+    name=None,
+    tags=[])"""