From 9c85917dad236d2b8c44acc66293d5b478d4c845 Mon Sep 17 00:00:00 2001 From: Jakub Kuczys Date: Thu, 22 Jun 2023 01:42:01 +0200 Subject: [PATCH] Handle exception chaining and groups in Dev's traceback handling (#6178) --- redbot/core/dev_commands.py | 99 +++++++++++++++---------- tests/core/test_dev_commands.py | 126 ++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 37 deletions(-) diff --git a/redbot/core/dev_commands.py b/redbot/core/dev_commands.py index 5033dcd63..b0deb5c58 100644 --- a/redbot/core/dev_commands.py +++ b/redbot/core/dev_commands.py @@ -337,49 +337,74 @@ class DevOutput: pass else: exc.lineno -= line_offset + if sys.version_info >= (3, 10) and exc.end_lineno is not None: + exc.end_lineno -= line_offset else: exc.lineno -= line_offset + if sys.version_info >= (3, 10) and exc.end_lineno is not None: + exc.end_lineno -= line_offset - traceback_exc = traceback.TracebackException(exc_type, exc, tb) + top_traceback_exc = traceback.TracebackException(exc_type, exc, tb) py311_or_above = sys.version_info >= (3, 11) - stack_summary = traceback_exc.stack - for idx, frame_summary in enumerate(stack_summary): - try: - source_lines, line_offset = self.source_cache[frame_summary.filename] - except KeyError: - continue - lineno = frame_summary.lineno - if lineno is None: - continue + queue = [ # actually a stack but 'stack' is easy to confuse with actual traceback stack + top_traceback_exc, + ] + seen = {id(top_traceback_exc)} + while queue: + traceback_exc = queue.pop() - try: - # line numbers are 1-based, the list indexes are 0-based - line = source_lines[lineno - 1] - except IndexError: - # the frame might be pointing at a different source code, ignore... - continue - lineno -= line_offset - # support for enhanced error locations in tracebacks - if py311_or_above: - end_lineno = frame_summary.end_lineno - if end_lineno is not None: - end_lineno -= line_offset - frame_summary = traceback.FrameSummary( - frame_summary.filename, - lineno, - frame_summary.name, - line=line, - end_lineno=end_lineno, - colno=frame_summary.colno, - end_colno=frame_summary.end_colno, - ) - else: - frame_summary = traceback.FrameSummary( - frame_summary.filename, lineno, frame_summary.name, line=line - ) - stack_summary[idx] = frame_summary + # handle exception groups; this uses getattr() to support `exceptiongroup` backport lib + exceptions: List[traceback.TracebackException] = ( + getattr(traceback_exc, "exceptions", None) or [] + ) + # handle exception chaining + if traceback_exc.__cause__ is not None: + exceptions.append(traceback_exc.__cause__) + if traceback_exc.__context__ is not None: + exceptions.append(traceback_exc.__context__) + for te in exceptions: + if id(te) not in seen: + queue.append(te) + seen.add(id(te)) - return "".join(traceback_exc.format()) + stack_summary = traceback_exc.stack + for idx, frame_summary in enumerate(stack_summary): + try: + source_lines, line_offset = self.source_cache[frame_summary.filename] + except KeyError: + continue + lineno = frame_summary.lineno + if lineno is None: + continue + + try: + # line numbers are 1-based, the list indexes are 0-based + line = source_lines[lineno - 1] + except IndexError: + # the frame might be pointing at a different source code, ignore... + continue + lineno -= line_offset + # support for enhanced error locations in tracebacks + if py311_or_above: + end_lineno = frame_summary.end_lineno + if end_lineno is not None: + end_lineno -= line_offset + frame_summary = traceback.FrameSummary( + frame_summary.filename, + lineno, + frame_summary.name, + line=line, + end_lineno=end_lineno, + colno=frame_summary.colno, + end_colno=frame_summary.end_colno, + ) + else: + frame_summary = traceback.FrameSummary( + frame_summary.filename, lineno, frame_summary.name, line=line + ) + stack_summary[idx] = frame_summary + + return "".join(top_traceback_exc.format()) @cog_i18n(_) diff --git a/tests/core/test_dev_commands.py b/tests/core/test_dev_commands.py index 1cf82825c..b4a6b863e 100644 --- a/tests/core/test_dev_commands.py +++ b/tests/core/test_dev_commands.py @@ -330,6 +330,132 @@ STATEMENT_TESTS = { """, ), ), + # exception chaining + """\ + try: + 1 / 0 + except ZeroDivisionError as exc: + try: + raise RuntimeError("direct cause") from exc + except RuntimeError: + raise ValueError("indirect cause") + """: ( + ( + lambda v: v < (3, 11), + """\ + Traceback (most recent call last): + File "", line 2, in + 1 / 0 + ZeroDivisionError: division by zero + + The above exception was the direct cause of the following exception: + + Traceback (most recent call last): + File "", line 5, in + raise RuntimeError("direct cause") from exc + RuntimeError: direct cause + + During handling of the above exception, another exception occurred: + + Traceback (most recent call last): + File "", line 7, in + raise ValueError("indirect cause") + ValueError: indirect cause + """, + ), + ( + lambda v: v >= (3, 11), + """\ + Traceback (most recent call last): + File "", line 2, in + 1 / 0 + ~~^~~ + ZeroDivisionError: division by zero + + The above exception was the direct cause of the following exception: + + Traceback (most recent call last): + File "", line 5, in + raise RuntimeError("direct cause") from exc + RuntimeError: direct cause + + During handling of the above exception, another exception occurred: + + Traceback (most recent call last): + File "", line 7, in + raise ValueError("indirect cause") + ValueError: indirect cause + """, + ), + ), + # exception groups + """\ + def f(v): + try: + 1 / 0 + except ZeroDivisionError: + try: + raise ValueError(v) + except ValueError as e: + return e + try: + raise ExceptionGroup("one", [f(1)]) + except ExceptionGroup as e: + eg = e + try: + raise ExceptionGroup("two", [f(2), eg]) + except ExceptionGroup as e: + raise RuntimeError("wrapping") from e + """: ( + ( + lambda v: v >= (3, 11), + """\ + + Exception Group Traceback (most recent call last): + | File "", line 14, in + | raise ExceptionGroup("two", [f(2), eg]) + | ExceptionGroup: two (2 sub-exceptions) + +-+---------------- 1 ---------------- + | Traceback (most recent call last): + | File "", line 3, in f + | 1 / 0 + | ~~^~~ + | ZeroDivisionError: division by zero + | + | During handling of the above exception, another exception occurred: + | + | Traceback (most recent call last): + | File "", line 6, in f + | raise ValueError(v) + | ValueError: 2 + +---------------- 2 ---------------- + | Exception Group Traceback (most recent call last): + | File "", line 10, in + | raise ExceptionGroup("one", [f(1)]) + | ExceptionGroup: one (1 sub-exception) + +-+---------------- 1 ---------------- + | Traceback (most recent call last): + | File "", line 3, in f + | 1 / 0 + | ~~^~~ + | ZeroDivisionError: division by zero + | + | During handling of the above exception, another exception occurred: + | + | Traceback (most recent call last): + | File "", line 6, in f + | raise ValueError(v) + | ValueError: 1 + +------------------------------------ + + The above exception was the direct cause of the following exception: + + Traceback (most recent call last): + File "", line 16, in + raise RuntimeError("wrapping") from e + RuntimeError: wrapping + """, + ), + ), }