diff --git a/IPython/core/tests/test_ultratb.py b/IPython/core/tests/test_ultratb.py index e77f444..c00b4d3 100644 --- a/IPython/core/tests/test_ultratb.py +++ b/IPython/core/tests/test_ultratb.py @@ -296,6 +296,25 @@ except Exception: tt.AssertPrints("ValueError", suppress=False): ip.run_cell(self.SUPPRESS_CHAINING_CODE) + def test_plain_direct_cause_error(self): + with tt.AssertPrints(["KeyError", "NameError", "direct cause"]): + ip.run_cell("%xmode Plain") + ip.run_cell(self.DIRECT_CAUSE_ERROR_CODE) + ip.run_cell("%xmode Verbose") + + def test_plain_exception_during_handling_error(self): + with tt.AssertPrints(["KeyError", "NameError", "During handling"]): + ip.run_cell("%xmode Plain") + ip.run_cell(self.EXCEPTION_DURING_HANDLING_CODE) + ip.run_cell("%xmode Verbose") + + def test_plain_suppress_exception_chaining(self): + with tt.AssertNotPrints("ZeroDivisionError"), \ + tt.AssertPrints("ValueError", suppress=False): + ip.run_cell("%xmode Plain") + ip.run_cell(self.SUPPRESS_CHAINING_CODE) + ip.run_cell("%xmode Verbose") + class RecursionTest(unittest.TestCase): DEFINITIONS = """ diff --git a/IPython/core/ultratb.py b/IPython/core/ultratb.py index 0864ba4..eef16cc 100644 --- a/IPython/core/ultratb.py +++ b/IPython/core/ultratb.py @@ -530,6 +530,30 @@ class TBTools(colorable.Colorable): ostream = property(_get_ostream, _set_ostream) + def get_parts_of_chained_exception(self, evalue): + def get_chained_exception(exception_value): + cause = getattr(exception_value, '__cause__', None) + if cause: + return cause + if getattr(exception_value, '__suppress_context__', False): + return None + return getattr(exception_value, '__context__', None) + + chained_evalue = get_chained_exception(evalue) + + if chained_evalue: + return chained_evalue.__class__, chained_evalue, chained_evalue.__traceback__ + + def prepare_chained_exception_message(self, cause): + direct_cause = "\nThe above exception was the direct cause of the following exception:\n" + exception_during_handling = "\nDuring handling of the above exception, another exception occurred:\n" + + if cause: + message = [[direct_cause]] + else: + message = [[exception_during_handling]] + return message + def set_colors(self, *args, **kw): """Shorthand access to the color table scheme selector method.""" @@ -603,7 +627,7 @@ class ListTB(TBTools): self.ostream.write(self.text(etype, value, elist)) self.ostream.write('\n') - def structured_traceback(self, etype, value, elist, tb_offset=None, + def structured_traceback(self, etype, evalue, etb=None, tb_offset=None, context=5): """Return a color formatted string with the traceback info. @@ -612,15 +636,16 @@ class ListTB(TBTools): etype : exception type Type of the exception raised. - value : object + evalue : object Data stored in the exception - elist : list - List of frames, see class docstring for details. + etb : object + If list: List of frames, see class docstring for details. + If Traceback: Traceback of the exception. tb_offset : int, optional Number of frames in the traceback to skip. If not given, the - instance value is used (set in constructor). + instance evalue is used (set in constructor). context : int, optional Number of lines of context information to print. @@ -629,6 +654,19 @@ class ListTB(TBTools): ------- String with formatted exception. """ + # This is a workaround to get chained_exc_ids in recursive calls + # etb should not be a tuple if structured_traceback is not recursive + if isinstance(etb, tuple): + etb, chained_exc_ids = etb + else: + chained_exc_ids = set() + + if isinstance(etb, list): + elist = etb + elif etb is not None: + elist = self._extract_tb(etb) + else: + elist = [] tb_offset = self.tb_offset if tb_offset is None else tb_offset Colors = self.Colors out_list = [] @@ -641,9 +679,25 @@ class ListTB(TBTools): (Colors.normalEm, Colors.Normal) + '\n') out_list.extend(self._format_list(elist)) # The exception info should be a single entry in the list. - lines = ''.join(self._format_exception_only(etype, value)) + lines = ''.join(self._format_exception_only(etype, evalue)) out_list.append(lines) + exception = self.get_parts_of_chained_exception(evalue) + + if exception and not id(exception[1]) in chained_exc_ids: + chained_exception_message = self.prepare_chained_exception_message( + evalue.__cause__)[0] + etype, evalue, etb = exception + # Trace exception to avoid infinite 'cause' loop + chained_exc_ids.add(id(exception[1])) + chained_exceptions_tb_offset = 0 + out_list = ( + self.structured_traceback( + etype, evalue, (etb, chained_exc_ids), + chained_exceptions_tb_offset, context) + + chained_exception_message + + out_list) + return out_list def _format_list(self, extracted_list): @@ -763,7 +817,7 @@ class ListTB(TBTools): etype : exception type value : exception value """ - return ListTB.structured_traceback(self, etype, value, []) + return ListTB.structured_traceback(self, etype, value) def show_exception_only(self, etype, evalue): """Only print the exception type and message, without a traceback. @@ -1013,16 +1067,6 @@ class VerboseTB(TBTools): _format_traceback_lines(lnum, index, lines, Colors, lvals, _line_format))) - def prepare_chained_exception_message(self, cause): - direct_cause = "\nThe above exception was the direct cause of the following exception:\n" - exception_during_handling = "\nDuring handling of the above exception, another exception occurred:\n" - - if cause: - message = [[direct_cause]] - else: - message = [[exception_during_handling]] - return message - def prepare_header(self, etype, long_version=False): colors = self.Colors # just a shorthand + quicker name lookup colorsnormal = colors.Normal # used a lot @@ -1117,20 +1161,6 @@ class VerboseTB(TBTools): info('\nUnfortunately, your original traceback can not be constructed.\n') return None - def get_parts_of_chained_exception(self, evalue): - def get_chained_exception(exception_value): - cause = getattr(exception_value, '__cause__', None) - if cause: - return cause - if getattr(exception_value, '__suppress_context__', False): - return None - return getattr(exception_value, '__context__', None) - - chained_evalue = get_chained_exception(evalue) - - if chained_evalue: - return chained_evalue.__class__, chained_evalue, chained_evalue.__traceback__ - def structured_traceback(self, etype, evalue, etb, tb_offset=None, number_of_lines_of_context=5): """Return a nice text document describing the traceback.""" @@ -1294,9 +1324,8 @@ class FormattedTB(VerboseTB, ListTB): # out-of-date source code. self.check_cache() # Now we can extract and format the exception - elist = self._extract_tb(tb) return ListTB.structured_traceback( - self, etype, value, elist, tb_offset, number_of_lines_of_context + self, etype, value, tb, tb_offset, number_of_lines_of_context ) def stb2text(self, stb):