From cccae0b94deb718a4706ed0e9868459e2fac3181 Mon Sep 17 00:00:00 2001 From: Zane Bitter Date: Tue, 7 Nov 2017 11:31:22 -0500 Subject: [PATCH] bpo-28603 Fix tracebacks for unhashable exceptions TracebackException checks for loops between exceptions to prevent an infinite traceback. It does this by putting the already-seen exception into a set. This means that unhashable exception objects will cause an error - an error that itself can likely not be printed because of the presence of the unhashable exception in the chain. In this case, we don't actually care about equality of the objects as defined by the class designer; we want to check that we don't encounter the self-same exception object, from a chain that is necessarily all in memory at the same time. We can trivially do so by comparing identities instead of equality. --- traceback2/__init__.py | 6 +++--- traceback2/tests/test_traceback.py | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/traceback2/__init__.py b/traceback2/__init__.py index c68958e..acf9b81 100644 --- a/traceback2/__init__.py +++ b/traceback2/__init__.py @@ -438,11 +438,11 @@ def __init__(self, exc_type, exc_value, exc_traceback, limit=None, # Handle loops in __cause__ or __context__. if _seen is None: _seen = set() - _seen.add(exc_value) + _seen.add(id(exc_value)) # Gracefully handle (the way Python 2.4 and earlier did) the case of # being called with no type or value (None, None, None). if (exc_value and getattr(exc_value, '__cause__', None) is not None - and exc_value.__cause__ not in _seen): + and id(exc_value.__cause__) not in _seen): cause = TracebackException( type(exc_value.__cause__), exc_value.__cause__, @@ -454,7 +454,7 @@ def __init__(self, exc_type, exc_value, exc_traceback, limit=None, else: cause = None if (exc_value and getattr(exc_value, '__context__', None) is not None - and exc_value.__context__ not in _seen): + and id(exc_value.__context__) not in _seen): context = TracebackException( type(exc_value.__context__), exc_value.__context__, diff --git a/traceback2/tests/test_traceback.py b/traceback2/tests/test_traceback.py index adfd1eb..6aaa88d 100644 --- a/traceback2/tests/test_traceback.py +++ b/traceback2/tests/test_traceback.py @@ -747,6 +747,30 @@ def test_context(self): self.assertEqual(exc_info[0], exc.exc_type) self.assertEqual(str(exc_info[1]), str(exc)) + def test_unhashable(self): + class UnhashableException(Exception): + __hash__ = None + + def __eq__(self, other): + return True + + ex1 = UnhashableException('ex1') + ex2 = UnhashableException('ex2') + try: + raise_from(ex2, ex1) + except UnhashableException: + try: + raise ex1 + except UnhashableException: + exc_info = sys.exc_info() + exc = traceback.TracebackException(*exc_info) + formatted = list(exc.format()) + if six.PY2: + self.assertIn('UnhashableException: ex1\n', formatted[2]) + else: + self.assertIn('UnhashableException: ex2\n', formatted[3]) + self.assertIn('UnhashableException: ex1\n', formatted[7]) + def test_limit(self): def recurse(n): if n: