From 95c4d0b1ad6690920597d41401cdee880ce3193c 2015-11-14 02:36:49 From: Scott Sanderson Date: 2015-11-14 02:36:49 Subject: [PATCH] BUG: Fix pprint failure on non-string __qualname__ or __name__. Fixes a bug where pprint would fail to correctly render a class whose __qualname__ is not a string, or a class that doesn't have a __qualname__ and whose __name__ is not a string. --- diff --git a/IPython/lib/pretty.py b/IPython/lib/pretty.py index fd3056f..028c3b2 100644 --- a/IPython/lib/pretty.py +++ b/IPython/lib/pretty.py @@ -85,7 +85,7 @@ import re import datetime from collections import deque -from IPython.utils.py3compat import PY3, cast_unicode +from IPython.utils.py3compat import PY3, cast_unicode, string_types from IPython.utils.encoding import get_stream_enc from io import StringIO @@ -671,7 +671,16 @@ def _type_pprint(obj, p, cycle): return mod = _safe_getattr(obj, '__module__', None) - name = _safe_getattr(obj, '__qualname__', obj.__name__) + try: + name = obj.__qualname__ + if not isinstance(name, string_types): + # This can happen if the type implements __qualname__ as a property + # or other descriptor in Python 2. + raise Exception("Try __name__") + except Exception: + name = obj.__name__ + if not isinstance(name, string_types): + name = '' if mod in (None, '__builtin__', 'builtins', 'exceptions'): p.text(name) diff --git a/IPython/lib/tests/test_pretty.py b/IPython/lib/tests/test_pretty.py index 9530813..6995495 100644 --- a/IPython/lib/tests/test_pretty.py +++ b/IPython/lib/tests/test_pretty.py @@ -11,7 +11,7 @@ from collections import Counter, defaultdict, deque, OrderedDict import nose.tools as nt from IPython.lib import pretty -from IPython.testing.decorators import skip_without +from IPython.testing.decorators import skip_without, py2_only from IPython.utils.py3compat import PY3, unicode_to_str if PY3: @@ -272,6 +272,83 @@ def test_basic_class(): nt.assert_true(type_pprint_wrapper.called) +# This is only run on Python 2 because in Python 3 the language prevents you +# from setting a non-unicode value for __qualname__ on a metaclass, and it +# doesn't respect the descriptor protocol if you subclass unicode and implement +# __get__. +@py2_only +def test_fallback_to__name__on_type(): + # Test that we correctly repr types that have non-string values for + # __qualname__ by falling back to __name__ + + class Type(object): + __qualname__ = 5 + + # Test repring of the type. + stream = StringIO() + printer = pretty.RepresentationPrinter(stream) + + printer.pretty(Type) + printer.flush() + output = stream.getvalue() + + # If __qualname__ is malformed, we should fall back to __name__. + expected = '.'.join([__name__, Type.__name__]) + nt.assert_equal(output, expected) + + # Clear stream buffer. + stream.buf = '' + + # Test repring of an instance of the type. + instance = Type() + printer.pretty(instance) + printer.flush() + output = stream.getvalue() + + # Should look like: + # + prefix = '<' + '.'.join([__name__, Type.__name__]) + ' at 0x' + nt.assert_true(output.startswith(prefix)) + + +@py2_only +def test_fail_gracefully_on_bogus__qualname__and__name__(): + # Test that we correctly repr types that have non-string values for both + # __qualname__ and __name__ + + class Meta(type): + __name__ = 5 + + class Type(object): + __metaclass__ = Meta + __qualname__ = 5 + + stream = StringIO() + printer = pretty.RepresentationPrinter(stream) + + printer.pretty(Type) + printer.flush() + output = stream.getvalue() + + # If we can't find __name__ or __qualname__ just use a sentinel string. + expected = '.'.join([__name__, '']) + nt.assert_equal(output, expected) + + # Clear stream buffer. + stream.buf = '' + + # Test repring of an instance of the type. + instance = Type() + printer.pretty(instance) + printer.flush() + output = stream.getvalue() + + # Should look like: + # at 0x7f7658ae07d0> + prefix = '<' + '.'.join([__name__, '']) + ' at 0x' + nt.assert_true(output.startswith(prefix)) + + def test_collections_defaultdict(): # Create defaultdicts with cycles a = defaultdict() diff --git a/IPython/testing/decorators.py b/IPython/testing/decorators.py index 9862887..33394ba 100644 --- a/IPython/testing/decorators.py +++ b/IPython/testing/decorators.py @@ -48,7 +48,7 @@ from .ipunittest import ipdoctest, ipdocstring from IPython.external.decorators import * # For onlyif_cmd_exists decorator -from IPython.utils.py3compat import string_types, which +from IPython.utils.py3compat import string_types, which, PY2, PY3 #----------------------------------------------------------------------------- # Classes and functions @@ -336,6 +336,9 @@ skip_known_failure = knownfailureif(True,'This test is known to fail') known_failure_py3 = knownfailureif(sys.version_info[0] >= 3, 'This test is known to fail on Python 3.') +py2_only = skipif(PY3, "This test only runs on Python 2.") +py3_only = skipif(PY2, "This test only runs on Python 3.") + # A null 'decorator', useful to make more readable code that needs to pick # between different decorators based on OS or other conditions null_deco = lambda f: f diff --git a/IPython/utils/py3compat.py b/IPython/utils/py3compat.py index 5d370c0..4f0ed41 100644 --- a/IPython/utils/py3compat.py +++ b/IPython/utils/py3compat.py @@ -289,6 +289,9 @@ else: exec(compiler(scripttext, filename, 'exec'), glob, loc) +PY2 = not PY3 + + def annotate(**kwargs): """Python 3 compatible function annotation for Python 2.""" if not kwargs: