diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 89980ae6f8573a..ba749f61d51bab 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -2062,15 +2062,101 @@ def h(count=10): actual = stderr_g.getvalue().splitlines() self.assertEqual(actual, expected) + def _check_previous_line_repeated(self, render_exc): + # See https://github.com/python/cpython/issues/128327 + def fib(number: int) -> int: + # wrong implementation + assert number > 0 + if number == 1: + return 1 + return fib(number - 1) + fib(number - 2) + + lineno_fib = fib.__code__.co_firstlineno + + with captured_output("stderr") as stderr_fib5: + try: + fib(5) + except AssertionError: + render_exc() + else: + self.fail("no error raised") + result_fib5 = [ + 'Traceback (most recent call last):', + f' File "{__file__}", line {lineno_fib + 11}, in _check_previous_line_repeated', + ' fib(5)', + ' ~~~^^^', + f' File "{__file__}", line {lineno_fib + 5}, in fib', + ' return fib(number - 1) + fib(number - 2)', + ' ~~~^^^^^^^^^^^^', + f' File "{__file__}", line {lineno_fib + 5}, in fib', + ' return fib(number - 1) + fib(number - 2)', + ' ~~~^^^^^^^^^^^^', + f' File "{__file__}", line {lineno_fib + 5}, in fib', + ' return fib(number - 1) + fib(number - 2)', + ' ~~~^^^^^^^^^^^^', + ] + if self.DEBUG_RANGES: + result_fib5.append(f' File "{__file__}", line {lineno_fib + 5}, in fib') + result_fib5.append(' return fib(number - 1) + fib(number - 2)') + result_fib5.append(' ~~~^^^^^^^^^^^^') + else: + result_fib5.append(' [Previous line repeated 1 more time]') + result_fib5.append(f' File "{__file__}", line {lineno_fib + 2}, in fib') + result_fib5.append(' assert number > 0') + result_fib5.append(' ^^^^^^^^^^') + result_fib5.append('AssertionError') + expected = self._maybe_filter_debug_ranges(result_fib5) + actual = stderr_fib5.getvalue().splitlines() + self.assertEqual(actual, expected) + + with captured_output("stderr") as stderr_fib6: + try: + fib(6) + except AssertionError: + render_exc() + else: + self.fail("no error raised") + result_fib6 = [ + 'Traceback (most recent call last):', + f' File "{__file__}", line {lineno_fib + 47}, in _check_previous_line_repeated', + ' fib(6)', + ' ~~~^^^', + f' File "{__file__}", line {lineno_fib + 5}, in fib', + ' return fib(number - 1) + fib(number - 2)', + ' ~~~^^^^^^^^^^^^', + f' File "{__file__}", line {lineno_fib + 5}, in fib', + ' return fib(number - 1) + fib(number - 2)', + ' ~~~^^^^^^^^^^^^', + f' File "{__file__}", line {lineno_fib + 5}, in fib', + ' return fib(number - 1) + fib(number - 2)', + ' ~~~^^^^^^^^^^^^', + ] + if self.DEBUG_RANGES: + result_fib6.append(' [Previous line repeated 1 more time]') + result_fib6.append(f' File "{__file__}", line {lineno_fib + 5}, in fib') + result_fib6.append(' return fib(number - 1) + fib(number - 2)') + result_fib6.append(' ~~~^^^^^^^^^^^^') + else: + result_fib6.append(' [Previous line repeated 2 more times]') + result_fib6.append(f' File "{__file__}", line {lineno_fib + 2}, in fib') + result_fib6.append(' assert number > 0') + result_fib6.append(' ^^^^^^^^^^') + result_fib6.append('AssertionError') + expected = self._maybe_filter_debug_ranges(result_fib6) + actual = stderr_fib6.getvalue().splitlines() + self.assertEqual(actual, expected) + @requires_debug_ranges() def test_recursive_traceback(self): if self.DEBUG_RANGES: self._check_recursive_traceback_display(traceback.print_exc) + self._check_previous_line_repeated(traceback.print_exc) else: from _testcapi import exception_print def render_exc(): exception_print(sys.exception()) self._check_recursive_traceback_display(render_exc) + self._check_previous_line_repeated(render_exc) def test_format_stack(self): def fmt(): diff --git a/Lib/traceback.py b/Lib/traceback.py index 31c73efcef5a52..db406cf72e9090 100644 --- a/Lib/traceback.py +++ b/Lib/traceback.py @@ -741,26 +741,20 @@ def format(self, **kwargs): """ colorize = kwargs.get("colorize", False) result = [] - last_file = None - last_line = None - last_name = None + last_formatted_frame = None count = 0 for frame_summary in self: formatted_frame = self.format_frame_summary(frame_summary, colorize=colorize) if formatted_frame is None: continue - if (last_file is None or last_file != frame_summary.filename or - last_line is None or last_line != frame_summary.lineno or - last_name is None or last_name != frame_summary.name): + if last_formatted_frame is None or last_formatted_frame != formatted_frame: if count > _RECURSIVE_CUTOFF: count -= _RECURSIVE_CUTOFF result.append( f' [Previous line repeated {count} more ' f'time{"s" if count > 1 else ""}]\n' ) - last_file = frame_summary.filename - last_line = frame_summary.lineno - last_name = frame_summary.name + last_formatted_frame = formatted_frame count = 0 count += 1 if count > _RECURSIVE_CUTOFF: diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-02-10-19-52-55.gh-issue-128327.Lx73HD.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-02-10-19-52-55.gh-issue-128327.Lx73HD.rst new file mode 100644 index 00000000000000..32e359e539ffa3 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-02-10-19-52-55.gh-issue-128327.Lx73HD.rst @@ -0,0 +1 @@ +Improve the condition of showing [Previous line repeated %d more time(s)]. Now repeated frames must have the same error locations (indicated by caret (^) and/or tilde (~) characters).