From 652c95d267f0c543be14f816f5b584273f79ef68 Mon Sep 17 00:00:00 2001 From: Robert Collins Date: Mon, 9 Mar 2015 13:31:29 +1300 Subject: [PATCH] Handle objects with broken __unicode__ Because Python 2 objects may have __unicode__ broken, and we need unicode output to avoid implicit decodes, explicitly handle __unicode__. --- README.rst | 7 ++++++- traceback2/__init__.py | 20 +++++++++++++++----- traceback2/tests/test_traceback.py | 12 ++++++++++-- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 98146fa..3d279dd 100644 --- a/README.rst +++ b/README.rst @@ -6,7 +6,12 @@ Profit. Things to be aware of! -In Python 2.x, unlike traceback, traceback2 creates unicode output. +In Python 2.x, unlike traceback, traceback2 creates unicode output (because it +depends on the linecache2 module). Exception frame clearing silently does nothing if the interpreter in use does not support it. + +traceback2._some_str, which while not an official API is so old its likely in +use behaves similarly to the Python3 version - objects where unicode(obj) fails +but str(object) works will be shown as b'thestrvaluerepr'. diff --git a/traceback2/__init__.py b/traceback2/__init__.py index ebbfa52..c7a9864 100644 --- a/traceback2/__init__.py +++ b/traceback2/__init__.py @@ -4,7 +4,7 @@ import sys import operator import linecache2 as linecache -from six import u +from six import u, PY2 __all__ = ['extract_stack', 'extract_tb', 'format_exception', 'format_exception_only', 'format_list', 'format_stack', @@ -141,14 +141,24 @@ def format_exception_only(etype, value): def _format_final_exc_line(etype, value): valuestr = _some_str(value) if value == 'None' or value is None or not valuestr: - line = "%s\n" % etype + line = u("%s\n") % etype else: - line = "%s: %s\n" % (etype, valuestr) + line = u("%s: %s\n") % (etype, valuestr) return line def _some_str(value): try: - return str(value) + if PY2: + # If there is a working __unicode__, great. + # Otherwise see if we can get a bytestring... + # Otherwise we fallback to unprintable. + try: + return unicode(value) + except: + return "b%s" % repr(str(value)) + else: + # For Python3, bytestrings don't implicit decode, so its trivial. + return str(value) except: return '' % type(value).__name__ @@ -508,7 +518,7 @@ class TracebackException: stype = getattr(self.exc_type, '__qualname__', self.exc_type.__name__) smod = u(self.exc_type.__module__) - if smod not in ("__main__", "builtins"): + if smod not in ("__main__", "builtins", "exceptions"): stype = smod + u('.') + stype if not issubclass(self.exc_type, SyntaxError): diff --git a/traceback2/tests/test_traceback.py b/traceback2/tests/test_traceback.py index 3030df4..fdb450b 100644 --- a/traceback2/tests/test_traceback.py +++ b/traceback2/tests/test_traceback.py @@ -12,7 +12,7 @@ import contextlib2 as contextlib import fixtures import linecache2 as linecache import six -from six import text_type, u +from six import b, text_type, u try: from six import raise_from except ImportError: @@ -153,6 +153,14 @@ class SyntaxTracebackCases(testtools.TestCase): str_name = '.'.join([X.__module__, qualname(X)]) self.assertEqual(err[0], "%s: %s\n" % (str_name, str_value)) + def test_format_exception_only_undecodable__str__(self): + # This won't decode via the ascii codec. + X = Exception(u('\u5341').encode('shift-jis')) + err = traceback.format_exception_only(type(X), X) + self.assertEqual(len(err), 1) + str_value = "b'\\x8f\\\\'" + self.assertEqual(err[0], "Exception: %s\n" % str_value) + def test_without_exception(self): err = traceback.format_exception_only(None, None) self.assertEqual(err, ['None\n']) @@ -619,7 +627,7 @@ class TestStack(unittest.TestCase): traceback.walk_stack(None), capture_locals=True, limit=1) s = some_inner(3, 4) self.assertEqual( - [' File "' + FNAME + '", line 619, ' + [' File "' + FNAME + '", line 627, ' 'in some_inner\n' ' traceback.walk_stack(None), capture_locals=True, limit=1)\n' ' a = 1\n'