Repository: bokeh/bokeh

Issue:
Update of ipython to 6.3.0 breaks unit tests
CI unit test build suddenly failing after recent update to ipython 6.3:
```
______________________________ test_Model_pretty _______________________________
    def test_Model_pretty():
>       class Foo1(Model):
bokeh/tests/test_model.py:8: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
meta_cls = <class 'bokeh.model.MetaModel'>, class_name = 'Foo1'
bases = (<class 'bokeh.model.Model'>,)
class_dict = {'__container_props__': set(), '__module__': 'bokeh.tests.test_model', '__properties__': set(), '__properties_with_refs__': set(), ...}
    def __new__(meta_cls, class_name, bases, class_dict):
        '''
    
            Raises:
                Warning
    
            '''
    
        # use an explicitly provided view model name if there is one
        if "__view_model__" not in class_dict:
            class_dict["__view_model__"] = class_name
    
        # call the parent metaclass to create the new model type
        newcls = super(MetaModel, meta_cls).__new__(meta_cls, class_name, bases, class_dict)
    
        # update the mapping of view model names to classes, checking for any duplicates
        # and handling any subtype relationships or custom implementations
        entry = class_dict.get("__subtype__", class_dict["__view_model__"])
        if entry in MetaModel.model_class_reverse_map and not hasattr(newcls, "__implementation__"):
            raise Warning("Duplicate __view_model__ or __subtype__ declaration of '%s' for " \
                          "class %s.  Previous definition: %s" % \
                          (entry, class_name,
>                          MetaModel.model_class_reverse_map[entry]))
E           Warning: Duplicate __view_model__ or __subtype__ declaration of 'Foo1' for class Foo1.  Previous definition: <class 'bokeh.tests.test_model.test_Model_pretty.<locals>.Foo1'>

```
confirmed locally that this package difference triggers the failure.

One option is to pin the version but would like to identify the root cause as this connection is surprising and unexpected. 


Discussion:
Well this just gets more annoying, locally the same test is actually failing for a different reason:
```

    def test_Model_pretty():
        class Foo1(Model):
            pass
>       assert Foo1(id='1').pretty() == """\
    bokeh.tests.test_model.Foo1(
        id='1',
        js_event_callbacks={},
        js_property_callbacks={},
        name=None,
        subscribed_events=[],
        tags=[])"""
E       assert "Foo1(id='1', ...)" == 'bokeh.tests.test_m...s=[],\n    tags=[])'
E         - Foo1(id='1', ...)
E         + bokeh.tests.test_model.Foo1(
E         +     id='1',
E         +     js_event_callbacks={},
E         +     js_property_callbacks={},
E         +     name=None,
E         +     subscribed_events=[],
E         +     tags=[])

tests/test_model.py:10: AssertionError
```
However, I do see the original CI error, if I run exactly as on travis:
```
py.test -m unit --rerun=5 --cov=bokeh --cov-config=bokeh/.coveragerc bokeh/tests/test_model.py
```
OH, I think the assertion error triggers a retry, which triggers the fail (declaring same model twice in the same process) 
I guess ipython has made some change to their pretty printer
Lets remove all the `rerun`s while we are at this. Unit test are much more dependable now, and this seems to cloud underlying issues.
I'm fine with that, I will do that in this PR. 
@mattpap so the output that is being generated now is the output that corresponds to this branch in our `_repr_pretty`:
```
if cycle:
    p.text(name)
    p.text('(id=')
    p.pretty(_id)
    p.text(', ...)')
```
ideas?
```
In [2]: class Foo1(Model):
   ...:     pass
   ...:
   ...:

In [3]: m = Foo1(id='1')

In [4]: m.pretty()
Out[4]: "Foo1(id='1', ...)"
```
The API didn't change (based on a cursory look), so either cycle detection algorithm changed or they have a regression. I'm not sure right now. I will need to debug this, but I can do it in the morning.
It's hitting this in the ipython code:
```
->	                        if cls is not object \
399  	                                and callable(cls.__dict__.get('__repr__')):
```
presumably because `Model` is a metaclass:
```
(Pdb) cls
<class 'bokeh.model.Model'>
(Pdb) cls is not object
```
will try to compare to old version and maybe raise an issue on ipython tracker
@mattpap my earlier hypothesis was wrong, it's not the cycle branch, it's getting shunted to `__repr__` inside ipython's code now. I'll chase it down and contact them about it. I'll probably pin ipython for now tho. 
@bryevdv, this is the offending change: https://github.com/ipython/ipython/commit/9fbb32b9dc7fc7075aae6b7be7c1b0642abbb8f0.
The problem is that now `__repr__` takes precedence over our custom printer defined with `type_pprinters`.
ref: https://github.com/ipython/ipython/issues/11066
For now we could just pin. There's not alot of good options for the long term tho:

* greatly pare back the tests, e.g. just check for a small `.startswith`. We get whatever a given ipython version gives us.
* Use the hacky `singleton_pprinters` workaround in the linked issue, only gets top level objects pretty printed, but would be consistent wrt ipython versions.
* copy over and reimplement a working `pretty.py` to replace the ipython code wholesale to restore the full functionality 
* ditch all the ipython methods altogeher and  just implement a more sophisticated `__repr__` by hand
another option is to delete the `__repr__ = __str__` lines in our code, and not define a custom repr, but then non-ipython usage is left with
```
In [2]: repr(m)
Out[2]: '<__main__.Foo1 object at 0x111e84ac8>'
```
OK, this is pretty obnoxious, but it works:
```
diff --git a/bokeh/core/has_props.py b/bokeh/core/has_props.py
index f0c22fdce..363224ba8 100644
--- a/bokeh/core/has_props.py
+++ b/bokeh/core/has_props.py
@@ -273,7 +273,8 @@ class HasProps(with_metaclass(MetaHasProps, object)):
     def __str__(self):
         return "%s(...)" % self.__class__.__name__

-    __repr__ = __str__
+    def __repr__(self):
+        return self.__str__()

     def equals(self, other):
         ''' Structural equality of models.
@@ -661,7 +662,14 @@ class HasProps(with_metaclass(MetaHasProps, object)):
         else:
             stream = StringIO()
             printer = _BokehPrettyPrinter(stream, verbose, max_width, newline)
+
+            # temporarily deleting the old repr to make ipython not use it, see:
+            # https://github.com/ipython/ipython/issues/11066
+            old_repr = self.__repr__
+            self.__repr__ = None
             printer.pretty(self)
+            self.__repr__ = old_repr
+
             printer.flush()
             return stream.getvalue()

diff --git a/bokeh/model.py b/bokeh/model.py
index 2ddfa0136..94d48d00e 100644
--- a/bokeh/model.py
+++ b/bokeh/model.py
@@ -222,9 +222,6 @@ class Model(with_metaclass(MetaModel, HasProps, PropertyCallbackManager, EventCa
     def __str__(self):
         return "%s(id=%r, ...)" % (self.__class__.__name__, getattr(self, "_id", None))

-    __repr__ = __str__
-
-
     name = String(help="""
     An arbitrary, user-supplied name for this model.

```

Resolving patch:
diff --git a/bokeh/core/has_props.py b/bokeh/core/has_props.py
--- a/bokeh/core/has_props.py
+++ b/bokeh/core/has_props.py
@@ -646,24 +646,18 @@ def pretty(self, verbose=False, max_width=79, newline='\n'):
 
         '''
         IPython = import_optional('IPython')
+        cls = self.__class__
 
         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._repr_pretty(p, cycle)
-
-        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 = RepresentationPrinter(stream, verbose, max_width, newline)
             printer.pretty(self)
             printer.flush()
             return stream.getvalue()
+        else:
+            raise RuntimeError("%s.%s.pretty() requires IPython" % (cls.__module__, cls.__name__))
 
     def pprint(self, verbose=False, max_width=79, newline='\n'):
         ''' Print a "pretty" string representation of the object to stdout.
@@ -723,7 +717,7 @@ def _clone(self):
         '''
         return self.__class__(**self._property_values)
 
-    def _repr_pretty(self, p, cycle):
+    def _repr_pretty_(self, p, cycle):
         '''
 
         '''
diff --git a/bokeh/model.py b/bokeh/model.py
--- a/bokeh/model.py
+++ b/bokeh/model.py
@@ -649,7 +649,7 @@ def cell(c):
 
         return html
 
-    def _repr_pretty(self, p, cycle):
+    def _repr_pretty_(self, p, cycle):
         '''
 
         '''


Test patch: