Repository: ipython/ipython

Issue:
Objects with unicode-encoded repr failed to get formatter for printing in list with floats in it.
How to reproduce:

```
class UTest(object):                                                                                                                                                                                                                
    def __repr__(self):
        return unicode(self).encode('utf8')   # returns str object as it should
    def __unicode__(self):
        return u'тест'


In [61]: u = UTest()

In [62]: [u, 123]
Out[62]: [тест, 123]

In [63]: [u, 123.0]
Out[63]: ---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-63-d76b6d0c9bea> in <module>()
----> 1 [u, 123.0]

/usr/lib64/python2.7/site-packages/IPython/core/displayhook.pyc in __call__(self, result)
    251             self.write_output_prompt()
    252             format_dict, md_dict = self.compute_format_data(result)
--> 253             self.write_format_data(format_dict, md_dict)
    254             self.update_user_ns(result)
    255             self.log_output(format_dict)

/usr/lib64/python2.7/site-packages/IPython/core/displayhook.pyc in write_format_data(self, format_dict, md_dict)
    172         # newline, even if all the prompt separators are ''. This is the
    173         # standard IPython behavior.
--> 174         result_repr = format_dict['text/plain']
    175         if '\n' in result_repr:
    176             # So that multi-line strings line up with the left column of

KeyError: 'text/plain'


## looks like we don't have 'text/plain' in format_dict, lets check it
In [64]: from IPython.core.formatters import DisplayFormatter

In [65]: d = DisplayFormatter()

In [66]: d.format([u, 123.0])
Out[66]: ({}, {})

In [67]: d.format([u, 123])
Out[67]: ({u'text/plain': '[\xd1\x82\xd0\xb5\xd1\x81\xd1\x82, 123]'}, {})

In [68]: d.format({123.0: u})                                                                                                                                                                                                          
Out[68]: ({}, {})

In [69]: d.format({123: u})
Out[69]: ({u'text/plain': '{123: \xd1\x82\xd0\xb5\xd1\x81\xd1\x82}'}, {})

```

Well, that's it, formatter returns empty format_dict specifically for object with unicode-encoded repr and float number in lists and dicts.
I tested this on 2.2.0, 2.3.0, 2.4.0, they all affected.



Discussion:

Resolving patch:
diff --git a/IPython/core/formatters.py b/IPython/core/formatters.py
--- a/IPython/core/formatters.py
+++ b/IPython/core/formatters.py
@@ -27,14 +27,9 @@
     ForwardDeclaredInstance,
 )
 from IPython.utils.py3compat import (
-    unicode_to_str, with_metaclass, PY3, string_types, unicode_type,
+    with_metaclass, string_types, unicode_type,
 )
 
-if PY3:
-    from io import StringIO
-else:
-    from StringIO import StringIO
-
 
 #-----------------------------------------------------------------------------
 # The main DisplayFormatter class
@@ -681,13 +676,13 @@ def __call__(self, obj):
         if not self.pprint:
             return repr(obj)
         else:
-            # This uses use StringIO, as cStringIO doesn't handle unicode.
-            stream = StringIO()
-            # self.newline.encode() is a quick fix for issue gh-597. We need to
-            # ensure that stream does not get a mix of unicode and bytestrings,
-            # or it will cause trouble.
+            # handle str and unicode on Python 2
+            # io.StringIO only accepts unicode,
+            # cStringIO doesn't handle unicode on py2,
+            # StringIO allows str, unicode but only ascii str
+            stream = pretty.CUnicodeIO()
             printer = pretty.RepresentationPrinter(stream, self.verbose,
-                self.max_width, unicode_to_str(self.newline),
+                self.max_width, self.newline,
                 max_seq_length=self.max_seq_length,
                 singleton_pprinters=self.singleton_printers,
                 type_pprinters=self.type_printers,
diff --git a/IPython/lib/pretty.py b/IPython/lib/pretty.py
--- a/IPython/lib/pretty.py
+++ b/IPython/lib/pretty.py
@@ -111,12 +111,9 @@ def _repr_pretty_(self, p, cycle):
 import datetime
 from collections import deque
 
-from IPython.utils.py3compat import PY3
+from IPython.utils.py3compat import PY3, cast_unicode
 
-if PY3:
-    from io import StringIO
-else:
-    from StringIO import StringIO
+from io import StringIO
 
 
 __all__ = ['pretty', 'pprint', 'PrettyPrinter', 'RepresentationPrinter',
@@ -137,11 +134,20 @@ def _safe_getattr(obj, attr, default=None):
     except Exception:
         return default
 
+if PY3:
+    CUnicodeIO = StringIO
+else:
+    class CUnicodeIO(StringIO):
+        """StringIO that casts str to unicode on Python 2"""
+        def write(self, text):
+            return super(CUnicodeIO, self).write(cast_unicode(text))
+
+
 def pretty(obj, verbose=False, max_width=79, newline='\n', max_seq_length=MAX_SEQ_LENGTH):
     """
     Pretty print the object's representation.
     """
-    stream = StringIO()
+    stream = CUnicodeIO()
     printer = RepresentationPrinter(stream, verbose, max_width, newline, max_seq_length)
     printer.pretty(obj)
     printer.flush()


Test patch:
diff --git a/IPython/lib/tests/test_pretty.py b/IPython/lib/tests/test_pretty.py
--- a/IPython/lib/tests/test_pretty.py
+++ b/IPython/lib/tests/test_pretty.py
@@ -1,3 +1,4 @@
+# coding: utf-8
 """Tests for IPython.lib.pretty."""
 
 # Copyright (c) IPython Development Team.
@@ -5,13 +6,11 @@
 
 from __future__ import print_function
 
-# Third-party imports
 import nose.tools as nt
 
-# Our own imports
 from IPython.lib import pretty
 from IPython.testing.decorators import skip_without
-from IPython.utils.py3compat import PY3
+from IPython.utils.py3compat import PY3, unicode_to_str
 
 if PY3:
     from io import StringIO
@@ -238,6 +237,21 @@ def test_metaclass_repr():
     nt.assert_equal(output, "[CUSTOM REPR FOR CLASS ClassWithMeta]")
 
 
+def test_unicode_repr():
+    u = u"üniço∂é"
+    ustr = unicode_to_str(u)
+    
+    class C(object):
+        def __repr__(self):
+            return ustr
+    
+    c = C()
+    p = pretty.pretty(c)
+    nt.assert_equal(p, u)
+    p = pretty.pretty([c])
+    nt.assert_equal(p, u'[%s]' % u)
+
+
 def test_basic_class():
     def type_pprint_wrapper(obj, p, cycle):
         if obj is MyObj: