diff -r 47bf09270027 Lib/test/test_traceback.py --- a/Lib/test/test_traceback.py Sun Dec 11 14:35:07 2016 -0800 +++ b/Lib/test/test_traceback.py Mon Dec 12 23:55:47 2016 -0800 @@ -19,6 +19,10 @@ test_tb = namedtuple('tb', ['tb_frame', 'tb_lineno', 'tb_next']) +class UnhashableError(Exception): + __hash__ = None + + class TracebackCases(unittest.TestCase): # For now, a very minimal set of tests. I want to be sure that # formatting of SyntaxErrors works based on changes for 2.1. @@ -463,6 +467,30 @@ err = self.get_report(Exception('')) self.assertIn('Exception\n', err) + def test_unhashable_exception(self): + # http://bugs.python.org/issue28603 + report = self.get_report(UnhashableError()) + self.assertIn('UnhashableError', report) + + try: + raise UnhashableError('err1') from UnhashableError('err2') + except UnhashableError as e: + report = self.get_report(e) + self.assertIn('UnhashableError', report) + self.assertIn('err1', report) + self.assertIn('err2', report) + + try: + try: + raise UnhashableError('err1') + except UnhashableError: + raise UnhashableError('err2') + except UnhashableError as e: + report = self.get_report(e) + self.assertIn('UnhashableError', report) + self.assertIn('err1', report) + self.assertIn('err2', report) + class PyExcReportingTests(BaseExceptionReportingTests, unittest.TestCase): # diff -r 47bf09270027 Lib/traceback.py --- a/Lib/traceback.py Sun Dec 11 14:35:07 2016 -0800 +++ b/Lib/traceback.py Mon Dec 12 23:55:47 2016 -0800 @@ -435,8 +435,8 @@ # need stub thunk objects just to glue it together. # Handle loops in __cause__ or __context__. if _seen is None: - _seen = set() - _seen.add(exc_value) + _seen = [] + _seen.append(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 exc_value.__cause__ is not None diff -r 47bf09270027 Python/pythonrun.c --- a/Python/pythonrun.c Sun Dec 11 14:35:07 2016 -0800 +++ b/Python/pythonrun.c Mon Dec 12 23:55:47 2016 -0800 @@ -807,13 +807,14 @@ if (seen != NULL) { /* Exception chaining */ - if (PySet_Add(seen, value) == -1) + if (PyList_Append(seen, value) == -1) { PyErr_Clear(); + } else if (PyExceptionInstance_Check(value)) { cause = PyException_GetCause(value); context = PyException_GetContext(value); if (cause) { - res = PySet_Contains(seen, cause); + res = PySequence_Contains(seen, cause); if (res == -1) PyErr_Clear(); if (res == 0) { @@ -825,7 +826,7 @@ } else if (context && !((PyBaseExceptionObject *)value)->suppress_context) { - res = PySet_Contains(seen, context); + res = PySequence_Contains(seen, context); if (res == -1) PyErr_Clear(); if (res == 0) { @@ -870,7 +871,7 @@ /* We choose to ignore seen being possibly NULL, and report at least the main exception (it could be a MemoryError). */ - seen = PySet_New(NULL); + seen = PyList_New(0); if (seen == NULL) PyErr_Clear(); print_exception_recursive(f, value, seen);