diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_except_star.py b/graalpython/com.oracle.graal.python.test/src/tests/test_except_star.py new file mode 100644 index 0000000000..66457d4795 --- /dev/null +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_except_star.py @@ -0,0 +1,606 @@ +# Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# The Universal Permissive License (UPL), Version 1.0 +# +# Subject to the condition set forth below, permission is hereby granted to any +# person obtaining a copy of this software, associated documentation and/or +# data (collectively the "Software"), free of charge and under any and all +# copyright rights in the Software, and any and all patent rights owned or +# freely licensable by each licensor hereunder covering either (i) the +# unmodified Software as contributed to or provided by such licensor, or (ii) +# the Larger Works (as defined below), to deal in both +# +# (a) the Software, and +# +# (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if +# one is included with the Software each a "Larger Work" to which the Software +# is contributed by such licensors), +# +# without restriction, including without limitation the rights to copy, create +# derivative works of, display, perform, and distribute the Software and make, +# use, sell, offer for sale, import, export, have made, and have sold the +# Software and the Larger Work(s), and to sublicense the foregoing rights on +# either these or other terms. +# +# This license is subject to the following condition: +# +# The above copyright notice and either this complete permission notice or at a +# minimum a reference to the UPL must be included in all copies or substantial +# portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import unittest +import subprocess +import sys +import textwrap + +from tests.util import skipIfBytecodeDSL + + +class ExceptStarPrintTest(unittest.TestCase): + def test_01_eg_simple(self): + script = textwrap.dedent(""" + raise ExceptionGroup("eg", [ + ValueError(1), + TypeError(2) + ]) + """) + p = subprocess.run([sys.executable, "-c", script], capture_output=True) + expected = [b' + Exception Group Traceback (most recent call last):', + b' | File "", line 2, in ', + b' | ExceptionGroup: eg (2 sub-exceptions)', + b' +-+---------------- 1 ----------------', + b' | ValueError: 1', + b' +---------------- 2 ----------------', + b' | TypeError: 2', + b' +------------------------------------'] + self.assertEqual(p.stderr.splitlines(), expected) + + def test_02_eg_nested(self): + script = textwrap.dedent(""" + raise ExceptionGroup("EG", [ + TypeError("1"), + ExceptionGroup("2", [ + IndexError("2.1"), + ValueError("2.2"), + ExceptionGroup("2.3", [ + IndexError("2.3.1"), + ExceptionGroup("2.3.2", [ + TypeError("2.3.2.1"), + IndexError("2.3.2.2"), + ExceptionGroup("2.3.2.3", [ + ImportError("2.3.2.3.1"), + ValueError("2.3.2.3.2") + ]) + ]), + IndexError("2.3.3"), + IndexError("2.3.4"), + IndexError("2.3.5"), + IndexError("2.3.6"), + ]), + ExceptionGroup("2.4", [ + IndexError("2.4.1"), + ExceptionGroup("2.4.2", [ + TypeError("2.4.2.1"), + IndexError("2.4.2.2"), + ExceptionGroup("2.4.3", [ + ImportError("2.4.3.1"), + ValueError("2.4.3.2") + ]) + ]) + ]) + ]), + ValueError("3"), + ValueError("4"), + ValueError("5"), + ]) + """) + p = subprocess.run([sys.executable, "-c", script], capture_output=True) + expected = [b' + Exception Group Traceback (most recent call last):', + b' | File "", line 2, in ', + b' | ExceptionGroup: EG (5 sub-exceptions)', + b' +-+---------------- 1 ----------------', + b' | TypeError: 1', + b' +---------------- 2 ----------------', + b' | ExceptionGroup: 2 (4 sub-exceptions)', + b' +-+---------------- 1 ----------------', + b' | IndexError: 2.1', + b' +---------------- 2 ----------------', + b' | ValueError: 2.2', + b' +---------------- 3 ----------------', + b' | ExceptionGroup: 2.3 (6 sub-exceptions)', + b' +-+---------------- 1 ----------------', + b' | IndexError: 2.3.1', + b' +---------------- 2 ----------------', + b' | ExceptionGroup: 2.3.2 (3 sub-exceptions)', + b' +-+---------------- 1 ----------------', + b' | TypeError: 2.3.2.1', + b' +---------------- 2 ----------------', + b' | IndexError: 2.3.2.2', + b' +---------------- 3 ----------------', + b' | ExceptionGroup: 2.3.2.3 (2 sub-exceptions)', + b' +-+---------------- 1 ----------------', + b' | ImportError: 2.3.2.3.1', + b' +---------------- 2 ----------------', + b' | ValueError: 2.3.2.3.2', + b' +------------------------------------', + b' +---------------- 3 ----------------', + b' | IndexError: 2.3.3', + b' +---------------- 4 ----------------', + b' | IndexError: 2.3.4', + b' +---------------- 5 ----------------', + b' | IndexError: 2.3.5', + b' +---------------- 6 ----------------', + b' | IndexError: 2.3.6', + b' +------------------------------------', + b' +---------------- 4 ----------------', + b' | ExceptionGroup: 2.4 (2 sub-exceptions)', + b' +-+---------------- 1 ----------------', + b' | IndexError: 2.4.1', + b' +---------------- 2 ----------------', + b' | ExceptionGroup: 2.4.2 (3 sub-exceptions)', + b' +-+---------------- 1 ----------------', + b' | TypeError: 2.4.2.1', + b' +---------------- 2 ----------------', + b' | IndexError: 2.4.2.2', + b' +---------------- 3 ----------------', + b' | ExceptionGroup: 2.4.3 (2 sub-exceptions)', + b' +-+---------------- 1 ----------------', + b' | ImportError: 2.4.3.1', + b' +---------------- 2 ----------------', + b' | ValueError: 2.4.3.2', + b' +------------------------------------', + b' +---------------- 3 ----------------', + b' | ValueError: 3', + b' +---------------- 4 ----------------', + b' | ValueError: 4', + b' +---------------- 5 ----------------', + b' | ValueError: 5', + b' +------------------------------------'] + self.maxDiff = None + self.assertEqual(p.stderr.splitlines(), expected) + + def test_03_eg_nested_truncated(self): + script = textwrap.dedent(""" + raise ExceptionGroup("EG", [ + TypeError("1"), + ExceptionGroup("2", [ + IndexError("2.1"), + ValueError("2.2"), + ExceptionGroup("2.3", [ + IndexError("2.3.1"), + ExceptionGroup("2.3.2", [ + TypeError("2.3.2.1"), + IndexError("2.3.2.2"), + ExceptionGroup("2.3.2.3", [ + ExceptionGroup("2.3.2.3.1", [ + ExceptionGroup("2.3.2.3.1.1", [ + ExceptionGroup("2.3.2.3.1.1.1", [ + ExceptionGroup("2.3.2.3.1.1.1.1", [ + ExceptionGroup("2.3.2.3.1.1.1.1.1", [ + ExceptionGroup("2.3.2.3.1.1.1.1.1.1", [ + ExceptionGroup("2.3.2.3.1.1.1.1.1.1.1", [ + ExceptionGroup("2.3.2.3.1.1.1.1.1.1.1.1", [ + ExceptionGroup("2.3.2.3.1.1.1.1.1.1.1.1.1", [ + IndexError(1) + ]), + ]), + ]), + ]), + ImportError("2.3.2.3.1.1.1.1.1.2"), + IndexError("2.3.2.3.1.1.1.1.1.3"), + ]), + ]), + ]), + ]), + ]), + ImportError("2.3.2.3.1"), + ValueError("2.3.2.3.2") + ]) + ]), + IndexError("2.3.3"), + IndexError("2.3.4"), + IndexError("2.3.5"), + IndexError("2.3.6"), + IndexError("2.3.7"), + IndexError("2.3.8"), + IndexError("2.3.9"), + IndexError("2.3.10"), + IndexError("2.3.11"), + IndexError("2.3.12"), + IndexError("2.3.13"), + IndexError("2.3.14"), + IndexError("2.3.15"), + IndexError("2.3.16"), + IndexError("2.3.17"), + IndexError("2.3.18"), + ]), + ExceptionGroup("2.4", [ + IndexError("2.4.1"), + ExceptionGroup("2.4.2", [ + TypeError("2.4.2.1"), + IndexError("2.4.2.2"), + ExceptionGroup("2.4.3", [ + ImportError("2.4.3.1"), + ValueError("2.4.3.2") + ]) + ]) + ]) + ]), + ValueError("3"), + ValueError("4"), + ValueError("5"), + ValueError("6"), + ValueError("7"), + ValueError("8"), + ValueError("9"), + ValueError("10"), + ValueError("11"), + ValueError("12"), + ValueError("13"), + ValueError("14"), + ValueError("15"), + ValueError("16"), + ValueError("17"), + ValueError("18"), + ValueError("19"), + ValueError("20"), + ValueError("21"), + ValueError("22"), + ValueError("23"), + ]) + """) + p = subprocess.run([sys.executable, "-c", script], capture_output=True) + expected = [b' + Exception Group Traceback (most recent call last):', + b' | File "", line 2, in ', + b' | ExceptionGroup: EG (23 sub-exceptions)', + b' +-+---------------- 1 ----------------', + b' | TypeError: 1', + b' +---------------- 2 ----------------', + b' | ExceptionGroup: 2 (4 sub-exceptions)', + b' +-+---------------- 1 ----------------', + b' | IndexError: 2.1', + b' +---------------- 2 ----------------', + b' | ValueError: 2.2', + b' +---------------- 3 ----------------', + b' | ExceptionGroup: 2.3 (18 sub-exceptions)', + b' +-+---------------- 1 ----------------', + b' | IndexError: 2.3.1', + b' +---------------- 2 ----------------', + b' | ExceptionGroup: 2.3.2 (3 sub-exceptions)', + b' +-+---------------- 1 ----------------', + b' | TypeError: 2.3.2.1', + b' +---------------- 2 ----------------', + b' | IndexError: 2.3.2.2', + b' +---------------- 3 ----------------', + b' | ExceptionGroup: 2.3.2.3 (3 sub-exceptions)', + b' +-+---------------- 1 ----------------', + b' | ExceptionGroup: 2.3.2.3.1 (1 sub-exception)', + b' +-+---------------- 1 ----------------', + b' | ExceptionGroup: 2.3.2.3.1.1 (1 sub-exception)', + b' +-+---------------- 1 ----------------', + b' | ExceptionGroup: 2.3.2.3.1.1.1 (1 sub-exception)', + b' +-+---------------- 1 ----------------', + b' | ExceptionGroup: 2.3.2.3.1.1.1.1 (1 sub-exception)', + b' +-+---------------- 1 ----------------', + b' | ExceptionGroup: 2.3.2.3.1.1.1.1.1 (3 sub-exceptions)', + b' +-+---------------- 1 ----------------', + b' | ... (max_group_depth is 10)', + b' +---------------- 2 ----------------', + b' | ImportError: 2.3.2.3.1.1.1.1.1.2', + b' +---------------- 3 ----------------', + b' | IndexError: 2.3.2.3.1.1.1.1.1.3', + b' +------------------------------------', + b' +---------------- 2 ----------------', + b' | ImportError: 2.3.2.3.1', + b' +---------------- 3 ----------------', + b' | ValueError: 2.3.2.3.2', + b' +------------------------------------', + b' +---------------- 3 ----------------', + b' | IndexError: 2.3.3', + b' +---------------- 4 ----------------', + b' | IndexError: 2.3.4', + b' +---------------- 5 ----------------', + b' | IndexError: 2.3.5', + b' +---------------- 6 ----------------', + b' | IndexError: 2.3.6', + b' +---------------- 7 ----------------', + b' | IndexError: 2.3.7', + b' +---------------- 8 ----------------', + b' | IndexError: 2.3.8', + b' +---------------- 9 ----------------', + b' | IndexError: 2.3.9', + b' +---------------- 10 ----------------', + b' | IndexError: 2.3.10', + b' +---------------- 11 ----------------', + b' | IndexError: 2.3.11', + b' +---------------- 12 ----------------', + b' | IndexError: 2.3.12', + b' +---------------- 13 ----------------', + b' | IndexError: 2.3.13', + b' +---------------- 14 ----------------', + b' | IndexError: 2.3.14', + b' +---------------- 15 ----------------', + b' | IndexError: 2.3.15', + b' +---------------- ... ----------------', + b' | and 3 more exceptions', + b' +------------------------------------', + b' +---------------- 4 ----------------', + b' | ExceptionGroup: 2.4 (2 sub-exceptions)', + b' +-+---------------- 1 ----------------', + b' | IndexError: 2.4.1', + b' +---------------- 2 ----------------', + b' | ExceptionGroup: 2.4.2 (3 sub-exceptions)', + b' +-+---------------- 1 ----------------', + b' | TypeError: 2.4.2.1', + b' +---------------- 2 ----------------', + b' | IndexError: 2.4.2.2', + b' +---------------- 3 ----------------', + b' | ExceptionGroup: 2.4.3 (2 sub-exceptions)', + b' +-+---------------- 1 ----------------', + b' | ImportError: 2.4.3.1', + b' +---------------- 2 ----------------', + b' | ValueError: 2.4.3.2', + b' +------------------------------------', + b' +---------------- 3 ----------------', + b' | ValueError: 3', + b' +---------------- 4 ----------------', + b' | ValueError: 4', + b' +---------------- 5 ----------------', + b' | ValueError: 5', + b' +---------------- 6 ----------------', + b' | ValueError: 6', + b' +---------------- 7 ----------------', + b' | ValueError: 7', + b' +---------------- 8 ----------------', + b' | ValueError: 8', + b' +---------------- 9 ----------------', + b' | ValueError: 9', + b' +---------------- 10 ----------------', + b' | ValueError: 10', + b' +---------------- 11 ----------------', + b' | ValueError: 11', + b' +---------------- 12 ----------------', + b' | ValueError: 12', + b' +---------------- 13 ----------------', + b' | ValueError: 13', + b' +---------------- 14 ----------------', + b' | ValueError: 14', + b' +---------------- 15 ----------------', + b' | ValueError: 15', + b' +---------------- ... ----------------', + b' | and 8 more exceptions', + b' +------------------------------------'] + self.maxDiff = None + self.assertEqual(p.stderr.splitlines(), expected) + + def test_04_eg_cause(self): + script = textwrap.dedent(""" + EG = ExceptionGroup + try: + raise EG("eg1", [ValueError(1), TypeError(2)]) + except Exception as e: + raise EG("eg2", [ValueError(3), TypeError(4)]) from e + """) + + p = subprocess.run([sys.executable, "-c", script], capture_output=True) + + expected = [b' + Exception Group Traceback (most recent call last):', + b' | File "", line 4, in ', + b' | ExceptionGroup: eg1 (2 sub-exceptions)', + b' +-+---------------- 1 ----------------', + b' | ValueError: 1', + b' +---------------- 2 ----------------', + b' | TypeError: 2', + b' +------------------------------------', + b'', + b'The above exception was the direct cause of the following exception:', + b'', + b' + Exception Group Traceback (most recent call last):', + b' | File "", line 6, in ', + b' | ExceptionGroup: eg2 (2 sub-exceptions)', + b' +-+---------------- 1 ----------------', + b' | ValueError: 3', + b' +---------------- 2 ----------------', + b' | TypeError: 4', + b' +------------------------------------'] + + self.maxDiff = None + self.assertEqual(p.stderr.splitlines(), expected) + + def test_05_eg_context_with_context(self): + script = textwrap.dedent(""" + EG = ExceptionGroup + try: + try: + raise EG("eg1", [ValueError(1), TypeError(2)]) + except EG: + raise EG("eg2", [ValueError(3), TypeError(4)]) + except EG: + raise ImportError(5) + """) + + p = subprocess.run([sys.executable, "-c", script], capture_output=True) + + expected = [b' + Exception Group Traceback (most recent call last):', + b' | File "", line 5, in ', + b' | ExceptionGroup: eg1 (2 sub-exceptions)', + b' +-+---------------- 1 ----------------', + b' | ValueError: 1', + b' +---------------- 2 ----------------', + b' | TypeError: 2', + b' +------------------------------------', + b'', + b'During handling of the above exception, another exception occurred:', + b'', + b' + Exception Group Traceback (most recent call last):', + b' | File "", line 7, in ', + b' | ExceptionGroup: eg2 (2 sub-exceptions)', + b' +-+---------------- 1 ----------------', + b' | ValueError: 3', + b' +---------------- 2 ----------------', + b' | TypeError: 4', + b' +------------------------------------', + b'', + b'During handling of the above exception, another exception occurred:', + b'', + b'Traceback (most recent call last):', + b' File "", line 9, in ', + b'ImportError: 5',] + + self.maxDiff = None + self.assertEqual(p.stderr.splitlines(), expected) + + def test_06_eg_nested_with_context(self): + script = textwrap.dedent(""" + EG = ExceptionGroup + VE = ValueError + TE = TypeError + try: + try: + raise EG("nested", [TE(2), TE(3)]) + except Exception as e: + exc = e + raise EG("eg", [VE(1), exc, VE(4)]) + except EG: + raise EG("top", [VE(5)]) + """) + + p = subprocess.run([sys.executable, "-c", script], capture_output=True) + + expected = [b' + Exception Group Traceback (most recent call last):', + b' | File "", line 10, in ', + b' | ExceptionGroup: eg (3 sub-exceptions)', + b' +-+---------------- 1 ----------------', + b' | ValueError: 1', + b' +---------------- 2 ----------------', + b' | Exception Group Traceback (most recent call last):', + b' | File "", line 7, in ', + b' | ExceptionGroup: nested (2 sub-exceptions)', + b' +-+---------------- 1 ----------------', + b' | TypeError: 2', + b' +---------------- 2 ----------------', + b' | TypeError: 3', + b' +------------------------------------', + b' +---------------- 3 ----------------', + b' | ValueError: 4', + b' +------------------------------------', + b'', + b'During handling of the above exception, another exception occurred:', + b'', + b' + Exception Group Traceback (most recent call last):', + b' | File "", line 12, in ', + b' | ExceptionGroup: top (1 sub-exception)', + b' +-+---------------- 1 ----------------', + b' | ValueError: 5', + b' +------------------------------------',] + + self.maxDiff = None + self.assertEqual(p.stderr.splitlines(), expected) + + def test_07_eg_with_notes(self): + script = textwrap.dedent(""" + try: + excs = [] + for msg in ['bad value', 'terrible value']: + try: + raise ValueError(msg) + except ValueError as e: + e.add_note(f'the {msg}') + excs.append(e) + raise ExceptionGroup("nested", excs) + except ExceptionGroup as e: + e.add_note(('>> Multi line note\\n' + '>> Because I am such\\n' + '>> an important exception.\\n' + '>> empty lines work too\\n' + '\\n' + '(that was an empty line)')) + raise + """) + + p = subprocess.run([sys.executable, "-c", script], capture_output=True) + + expected = [b' + Exception Group Traceback (most recent call last):', + b' | File "", line 10, in ', + b' | ExceptionGroup: nested (2 sub-exceptions)', + b' | >> Multi line note', + b' | >> Because I am such', + b' | >> an important exception.', + b' | >> empty lines work too', + b' | ', + b' | (that was an empty line)', + b' +-+---------------- 1 ----------------', + b' | Traceback (most recent call last):', + b' | File "", line 6, in ', + b' | ValueError: bad value', + b' | the bad value', + b' +---------------- 2 ----------------', + b' | Traceback (most recent call last):', + b' | File "", line 6, in ', + b' | ValueError: terrible value', + b' | the terrible value', + b' +------------------------------------',] + + self.maxDiff = None + self.assertEqual(p.stderr.splitlines(), expected) + + def test_08_eg_with_multiple_notes(self): + script = textwrap.dedent(""" + try: + excs = [] + for msg in ['bad value', 'terrible value']: + try: + raise ValueError(msg) + except ValueError as e: + e.add_note(f'the {msg}') + e.add_note(f'Goodbye {msg}') + excs.append(e) + raise ExceptionGroup("nested", excs) + except ExceptionGroup as e: + e.add_note(('>> Multi line note\\n' + '>> Because I am such\\n' + '>> an important exception.\\n' + '>> empty lines work too\\n' + '\\n' + '(that was an empty line)')) + e.add_note('Goodbye!') + raise + """) + + p = subprocess.run([sys.executable, "-c", script], capture_output=True) + + expected = [b' + Exception Group Traceback (most recent call last):', + b' | File "", line 11, in ', + b' | ExceptionGroup: nested (2 sub-exceptions)', + b' | >> Multi line note', + b' | >> Because I am such', + b' | >> an important exception.', + b' | >> empty lines work too', + b' | ', + b' | (that was an empty line)', + b' | Goodbye!', + b' +-+---------------- 1 ----------------', + b' | Traceback (most recent call last):', + b' | File "", line 6, in ', + b' | ValueError: bad value', + b' | the bad value', + b' | Goodbye bad value', + b' +---------------- 2 ----------------', + b' | Traceback (most recent call last):', + b' | File "", line 6, in ', + b' | ValueError: terrible value', + b' | the terrible value', + b' | Goodbye terrible value', + b' +------------------------------------',] + + self.maxDiff = None + self.assertEqual(p.stderr.splitlines(), expected) \ No newline at end of file diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SysModuleBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SysModuleBuiltins.java index 3185a01e7d..5dff21f74c 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SysModuleBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SysModuleBuiltins.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -101,6 +101,7 @@ import static com.oracle.graal.python.nodes.BuiltinNames.T___DISPLAYHOOK__; import static com.oracle.graal.python.nodes.BuiltinNames.T___EXCEPTHOOK__; import static com.oracle.graal.python.nodes.BuiltinNames.T___GRAALPYTHON__; +import static com.oracle.graal.python.nodes.BuiltinNames.T___NOTES__; import static com.oracle.graal.python.nodes.BuiltinNames.T___STDERR__; import static com.oracle.graal.python.nodes.BuiltinNames.T___STDIN__; import static com.oracle.graal.python.nodes.BuiltinNames.T___STDOUT__; @@ -115,6 +116,7 @@ import static com.oracle.graal.python.nodes.SpecialAttributeNames.T___; import static com.oracle.graal.python.nodes.SpecialAttributeNames.T___MODULE__; import static com.oracle.graal.python.nodes.SpecialMethodNames.T___SIZEOF__; +import static com.oracle.graal.python.nodes.StringLiterals.J_NEWLINE; import static com.oracle.graal.python.nodes.StringLiterals.T_BACKSLASHREPLACE; import static com.oracle.graal.python.nodes.StringLiterals.T_BASE_PREFIX; import static com.oracle.graal.python.nodes.StringLiterals.T_BIG; @@ -126,6 +128,7 @@ import static com.oracle.graal.python.nodes.StringLiterals.T_LITTLE; import static com.oracle.graal.python.nodes.StringLiterals.T_NEWLINE; import static com.oracle.graal.python.nodes.StringLiterals.T_PREFIX; +import static com.oracle.graal.python.nodes.StringLiterals.T_SPACE; import static com.oracle.graal.python.nodes.StringLiterals.T_STRICT; import static com.oracle.graal.python.nodes.StringLiterals.T_STRING_SOURCE; import static com.oracle.graal.python.nodes.StringLiterals.T_SURROGATEESCAPE; @@ -174,10 +177,12 @@ import com.oracle.graal.python.builtins.objects.dict.PDict; import com.oracle.graal.python.builtins.objects.exception.ExceptionNodes; import com.oracle.graal.python.builtins.objects.exception.GetEscapedExceptionNode; +import com.oracle.graal.python.builtins.objects.exception.PBaseExceptionGroup; import com.oracle.graal.python.builtins.objects.frame.PFrame; import com.oracle.graal.python.builtins.objects.function.PArguments; import com.oracle.graal.python.builtins.objects.function.PKeyword; import com.oracle.graal.python.builtins.objects.ints.PInt; +import com.oracle.graal.python.builtins.objects.iterator.IteratorNodes; import com.oracle.graal.python.builtins.objects.list.PList; import com.oracle.graal.python.builtins.objects.module.PythonModule; import com.oracle.graal.python.builtins.objects.namespace.PSimpleNamespace; @@ -192,6 +197,7 @@ import com.oracle.graal.python.builtins.objects.tuple.StructSequence; import com.oracle.graal.python.builtins.objects.tuple.TupleBuiltins; import com.oracle.graal.python.lib.OsEnvironGetNode; +import com.oracle.graal.python.lib.PyExceptionGroupInstanceCheckNode; import com.oracle.graal.python.lib.PyExceptionInstanceCheckNode; import com.oracle.graal.python.lib.PyFloatAsDoubleNode; import com.oracle.graal.python.lib.PyFloatCheckExactNode; @@ -1250,7 +1256,7 @@ private void writeUnraisableExc(Node inliningTarget, TracebackBuiltins.GetTraceb } if (excTb != PNone.NONE) { - PyTraceBackPrint.print(inliningTarget, getTbFrameNode, materializeStNode, sys, out, excTb); + PyTraceBackPrint.print(inliningTarget, getTbFrameNode, materializeStNode, sys, out, excTb, false, false, 0, null); } if (excType == PNone.NONE) { @@ -1343,6 +1349,11 @@ abstract static class ExceptHookNode extends PythonBuiltinNode { static final TruffleString T_ATTR_LINENO = tsInternedLiteral("lineno"); static final TruffleString T_ATTR_OFFSET = tsInternedLiteral("offset"); static final TruffleString T_ATTR_TEXT = tsInternedLiteral("text"); + static final TruffleString T_EG_MARGIN = tsInternedLiteral("| "); + + protected static final int INT_MAX_GROUP_WIDTH = 15; + protected static final int INT_MAX_GROUP_DEPTH = 10; + protected static final int INT_INDENT_SIZE = 2; @ValueType static final class SyntaxErrData { @@ -1363,6 +1374,42 @@ static final class SyntaxErrData { } } + static class ExceptionPrintContext { + public int depthMax; + public int depthCurrent; + public int widthMax; + public boolean needsToEnd; + + ExceptionPrintContext() { + this.depthMax = INT_MAX_GROUP_DEPTH; + this.depthCurrent = 0; + this.widthMax = INT_MAX_GROUP_WIDTH; + this.needsToEnd = false; + } + + public TruffleString getMargin() { + if (this.depthCurrent > 0) { + return T_EG_MARGIN; + } else { + return tsLiteral(""); + } + } + + public int getIndent() { + return this.depthCurrent * INT_INDENT_SIZE; + } + + public void increaseDepth() { + this.depthCurrent++; + } + + public void decreaseDepth() { + if (depthCurrent > 0) { + this.depthCurrent--; + } + } + } + private static SyntaxErrData parseSyntaxError(Object err) { Object v, msg; TruffleString fileName = null, text = null; @@ -1474,7 +1521,13 @@ private static void printErrorText(Object out, SyntaxErrData syntaxErrData) { @TruffleBoundary void printExceptionRecursive(Node inliningTarget, TracebackBuiltins.GetTracebackFrameNode getTbFrameNode, TracebackBuiltins.MaterializeTruffleStacktraceNode materializeStNode, - PythonModule sys, Object out, Object value, Set seen) { + PythonModule sys, Object out, Object value, Set seen, IteratorNodes.ToArrayNode toArrayNode) { + printExceptionRecursive(inliningTarget, getTbFrameNode, materializeStNode, sys, out, value, seen, new ExceptionPrintContext(), toArrayNode); + } + + @TruffleBoundary + void printExceptionRecursive(Node inliningTarget, TracebackBuiltins.GetTracebackFrameNode getTbFrameNode, TracebackBuiltins.MaterializeTruffleStacktraceNode materializeStNode, + PythonModule sys, Object out, Object value, Set seen, ExceptionPrintContext ctx, IteratorNodes.ToArrayNode toArrayNode) { if (seen != null) { // Exception chaining add(seen, value); @@ -1482,24 +1535,43 @@ void printExceptionRecursive(Node inliningTarget, TracebackBuiltins.GetTraceback Object cause = ExceptionNodes.GetCauseNode.executeUncached(value); Object context = ExceptionNodes.GetContextNode.executeUncached(value); + boolean needsToEnd = ctx.needsToEnd; if (cause != PNone.NONE) { if (notSeen(seen, cause)) { - printExceptionRecursive(inliningTarget, getTbFrameNode, materializeStNode, sys, out, cause, seen); + printExceptionRecursive(inliningTarget, getTbFrameNode, materializeStNode, sys, out, cause, seen, ctx, toArrayNode); fileWriteString(out, T_CAUSE_MESSAGE); } } else if (context != PNone.NONE && !ExceptionNodes.GetSuppressContextNode.executeUncached(value)) { if (notSeen(seen, context)) { - printExceptionRecursive(inliningTarget, getTbFrameNode, materializeStNode, sys, out, context, seen); + printExceptionRecursive(inliningTarget, getTbFrameNode, materializeStNode, sys, out, context, seen, ctx, toArrayNode); fileWriteString(out, T_CONTEXT_MESSAGE); } } + ctx.needsToEnd = needsToEnd; } } - printException(inliningTarget, getTbFrameNode, materializeStNode, sys, out, value); + if (value instanceof PBaseExceptionGroup) { + printExceptionGroup(inliningTarget, getTbFrameNode, materializeStNode, sys, out, value, seen, ctx, toArrayNode); + } else { + printException(inliningTarget, getTbFrameNode, materializeStNode, sys, out, value, ctx, toArrayNode); + } + } + + protected static TruffleString getIndent(int indent) { + return T_SPACE.repeatUncached(indent, TS_ENCODING); + } + + protected static void fileWriteIndentedString(Object file, String string, int indent) { + fileWriteIndentedString(file, tsLiteral(string), indent); + } + + protected static void fileWriteIndentedString(Object file, TruffleString string, int indent) { + fileWriteString(file, getIndent(indent)); + fileWriteString(file, string); } protected void printException(Node inliningTarget, TracebackBuiltins.GetTracebackFrameNode getTbFrameNode, TracebackBuiltins.MaterializeTruffleStacktraceNode materializeStNode, - PythonModule sys, Object out, Object excValue) { + PythonModule sys, Object out, Object excValue, ExceptionPrintContext ctx, IteratorNodes.ToArrayNode toArrayNode) { Object value = excValue; final Object type = getObjectClass(value); if (!PyExceptionInstanceCheckNode.executeUncached(value)) { @@ -1511,7 +1583,11 @@ protected void printException(Node inliningTarget, TracebackBuiltins.GetTracebac final Object tb = getExceptionTraceback(value); if (tb instanceof PTraceback) { - PyTraceBackPrint.print(inliningTarget, getTbFrameNode, materializeStNode, sys, out, tb); + if (value instanceof PBaseExceptionGroup pbeg) { + PyTraceBackPrint.print(inliningTarget, getTbFrameNode, materializeStNode, sys, out, tb, true, ctx.depthCurrent > 1, ctx.getIndent(), ctx.getMargin()); + } else { + PyTraceBackPrint.print(inliningTarget, getTbFrameNode, materializeStNode, sys, out, tb, false, ctx.depthCurrent == 0, ctx.getIndent(), ctx.getMargin()); + } } if (objectHasAttr(value, T_ATTR_PRINT_FILE_AND_LINE)) { @@ -1530,6 +1606,8 @@ protected void printException(Node inliningTarget, TracebackBuiltins.GetTracebac } } + fileWriteIndentedString(out, ctx.getMargin(), ctx.getIndent()); + TruffleString className; try { className = getTypeName(type); @@ -1569,6 +1647,104 @@ protected void printException(Node inliningTarget, TracebackBuiltins.GetTracebac } fileWriteString(out, T_NEWLINE); + + if (objectHasAttr(value, T___NOTES__)) { + // print notes + Object notes = objectLookupAttr(value, T___NOTES__); + if (notes instanceof PList noteList) { + Object[] arr = toArrayNode.execute(null, noteList); + for (Object oStr : arr) { + if (oStr instanceof TruffleString note) { + String n = note.toString(); + if (n.contains(J_NEWLINE)) { + String[] lines = n.split(J_NEWLINE); + for (String line : lines) { + fileWriteIndentedString(out, ctx.getMargin(), ctx.getIndent()); + fileWriteString(out, line); + fileWriteString(out, T_NEWLINE); + } + } else { + fileWriteIndentedString(out, ctx.getMargin(), ctx.getIndent()); + fileWriteString(out, note); + fileWriteString(out, T_NEWLINE); + } + } + } + } + } + } + + protected void printExceptionGroup(Node inliningTarget, TracebackBuiltins.GetTracebackFrameNode getTbFrameNode, TracebackBuiltins.MaterializeTruffleStacktraceNode materializeStNode, + PythonModule sys, Object out, Object excValue, Set seen, ExceptionPrintContext ctx, IteratorNodes.ToArrayNode toArrayNode) { + Object value = excValue; + final Object type = getObjectClass(value); + if (!PyExceptionGroupInstanceCheckNode.executeUncached(value)) { + PyTraceBackPrint.fileWriteString(out, "TypeError: print_exception_group(): Exception group expected for value, "); + fileWriteString(out, getTypeName(type)); + PyTraceBackPrint.fileWriteString(out, " found\n"); + } + + if (ctx.depthCurrent > ctx.depthMax) { + fileWriteIndentedString(out, ctx.getMargin(), ctx.getIndent()); + fileWriteString(out, String.format("... (max_group_depth is %d)", ctx.depthMax)); + fileWriteString(out, T_NEWLINE); + return; + } + + ctx.needsToEnd = false; + + if (ctx.depthCurrent == 0) { + ctx.increaseDepth(); + } + + printException(inliningTarget, getTbFrameNode, materializeStNode, sys, out, excValue, ctx, toArrayNode); + + PBaseExceptionGroup exceptionGroup = (PBaseExceptionGroup) excValue; + int counter = 1; + boolean lastException = false; + for (Object exception : exceptionGroup.getExceptions()) { + if (counter == exceptionGroup.getExceptions().length) { + lastException = true; + ctx.needsToEnd = true; + } + if (counter == 1) { + fileWriteIndentedString(out, "+".concat("-".repeat(INT_INDENT_SIZE - 1)), ctx.getIndent()); + } else { + fileWriteString(out, getIndent(ctx.getIndent() + INT_INDENT_SIZE)); + } + if (counter <= ctx.widthMax) { + fileWriteString(out, String.format("+---------------- %d ----------------", counter)); + fileWriteString(out, T_NEWLINE); + ctx.increaseDepth(); + printExceptionRecursive(inliningTarget, getTbFrameNode, materializeStNode, sys, out, exception, seen, ctx, toArrayNode); + ctx.decreaseDepth(); + } else { + fileWriteString(out, "+---------------- ... ----------------"); + fileWriteString(out, T_NEWLINE); + fileWriteIndentedString(out, ctx.getMargin(), ctx.getIndent() + INT_INDENT_SIZE); + int exceptionsRemaining = exceptionGroup.getExceptions().length - ctx.widthMax; + fileWriteString(out, String.format("and %d more exception%s", exceptionsRemaining, exceptionsRemaining > 1 ? "s" : "")); + fileWriteString(out, T_NEWLINE); + + // this makes this exception in this exception group essentially last + lastException = true; + ctx.needsToEnd = true; + break; + } + counter++; + } + + if (lastException && ctx.needsToEnd) { + fileWriteString(out, getIndent(ctx.getIndent() + INT_INDENT_SIZE)); + fileWriteString(out, "+------------------------------------"); + fileWriteString(out, T_NEWLINE); + // let only the innermost exception print the end of an exception group cascade + ctx.needsToEnd = false; + } + + if (ctx.depthCurrent == 1) { + ctx.decreaseDepth(); + } } @TruffleBoundary(allowInlining = true) @@ -1609,10 +1785,11 @@ Object doHookWithTb(VirtualFrame frame, PythonModule sys, @SuppressWarnings("unu @Bind Node inliningTarget, @Shared @Cached TracebackBuiltins.GetTracebackFrameNode getTbFrameNode, @Shared @Cached TracebackBuiltins.MaterializeTruffleStacktraceNode materializeStNode, - @Shared @Cached("createFor($node)") BoundaryCallData boundaryCallData) { + @Shared @Cached("createFor($node)") BoundaryCallData boundaryCallData, + @Shared @Cached IteratorNodes.ToArrayNode toArrayNode) { Object saved = BoundaryCallContext.enter(frame, boundaryCallData); try { - doHookWithTbImpl(inliningTarget, getTbFrameNode, materializeStNode, sys, value, traceBack); + doHookWithTbImpl(inliningTarget, getTbFrameNode, materializeStNode, sys, value, traceBack, toArrayNode); } finally { BoundaryCallContext.exit(frame, boundaryCallData, saved); } @@ -1621,10 +1798,10 @@ Object doHookWithTb(VirtualFrame frame, PythonModule sys, @SuppressWarnings("unu @TruffleBoundary private void doHookWithTbImpl(Node inliningTarget, TracebackBuiltins.GetTracebackFrameNode getTbFrameNode, TracebackBuiltins.MaterializeTruffleStacktraceNode materializeStNode, - PythonModule sys, Object value, PTraceback traceBack) { + PythonModule sys, Object value, PTraceback traceBack, IteratorNodes.ToArrayNode toArrayNode) { setExceptionTraceback(value, traceBack); Object stdErr = objectLookupAttr(sys, T_STDERR); - printExceptionRecursive(inliningTarget, getTbFrameNode, materializeStNode, sys, stdErr, value, createSet()); + printExceptionRecursive(inliningTarget, getTbFrameNode, materializeStNode, sys, stdErr, value, createSet(), toArrayNode); fileFlush(stdErr); } @@ -1633,10 +1810,11 @@ Object doHookWithoutTb(VirtualFrame frame, PythonModule sys, @SuppressWarnings(" @Bind Node inliningTarget, @Shared @Cached TracebackBuiltins.GetTracebackFrameNode getTbFrameNode, @Shared @Cached TracebackBuiltins.MaterializeTruffleStacktraceNode materializeStNode, - @Shared @Cached("createFor($node)") BoundaryCallData boundaryCallData) { + @Shared @Cached("createFor($node)") BoundaryCallData boundaryCallData, + @Shared @Cached IteratorNodes.ToArrayNode toArrayNode) { Object saved = BoundaryCallContext.enter(frame, boundaryCallData); try { - doHookWithoutTbImpl(inliningTarget, getTbFrameNode, materializeStNode, sys, value); + doHookWithoutTbImpl(inliningTarget, getTbFrameNode, materializeStNode, sys, value, toArrayNode); } finally { BoundaryCallContext.exit(frame, boundaryCallData, saved); } @@ -1645,9 +1823,9 @@ Object doHookWithoutTb(VirtualFrame frame, PythonModule sys, @SuppressWarnings(" @TruffleBoundary private void doHookWithoutTbImpl(Node inliningTarget, TracebackBuiltins.GetTracebackFrameNode getTbFrameNode, TracebackBuiltins.MaterializeTruffleStacktraceNode materializeStNode, - PythonModule sys, Object value) { + PythonModule sys, Object value, IteratorNodes.ToArrayNode toArrayNode) { Object stdErr = objectLookupAttr(sys, T_STDERR); - printExceptionRecursive(inliningTarget, getTbFrameNode, materializeStNode, sys, stdErr, value, createSet()); + printExceptionRecursive(inliningTarget, getTbFrameNode, materializeStNode, sys, stdErr, value, createSet(), toArrayNode); fileFlush(stdErr); } } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/lib/PyExceptionGroupInstanceCheckNode.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/lib/PyExceptionGroupInstanceCheckNode.java new file mode 100644 index 0000000000..b499f18115 --- /dev/null +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/lib/PyExceptionGroupInstanceCheckNode.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2023, 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or + * data (collectively the "Software"), free of charge and under any and all + * copyright rights in the Software, and any and all patent rights owned or + * freely licensable by each licensor hereunder covering either (i) the + * unmodified Software as contributed to or provided by such licensor, or (ii) + * the Larger Works (as defined below), to deal in both + * + * (a) the Software, and + * + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * + * The above copyright notice and either this complete permission notice or at a + * minimum a reference to the UPL must be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.oracle.graal.python.lib; + +import com.oracle.graal.python.builtins.PythonBuiltinClassType; +import com.oracle.graal.python.builtins.objects.cext.PythonAbstractNativeObject; +import com.oracle.graal.python.builtins.objects.exception.PBaseExceptionGroup; +import com.oracle.graal.python.nodes.classes.IsSubtypeNode; +import com.oracle.graal.python.nodes.object.GetClassNode; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Fallback; +import com.oracle.truffle.api.dsl.GenerateCached; +import com.oracle.truffle.api.dsl.GenerateInline; +import com.oracle.truffle.api.dsl.GenerateUncached; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.exception.AbstractTruffleException; +import com.oracle.truffle.api.nodes.Node; + +@GenerateInline +@GenerateCached(false) +@GenerateUncached +public abstract class PyExceptionGroupInstanceCheckNode extends Node { + public abstract boolean execute(Node inliningTarget, Object object); + + public static boolean executeUncached(Object object) { + return PyExceptionInstanceCheckNodeGen.getUncached().execute(null, object); + } + + @Specialization + static boolean doManaged(@SuppressWarnings("unused") PBaseExceptionGroup exception) { + return true; + } + + @Specialization + static boolean doNative(Node inliningTarget, PythonAbstractNativeObject object, + @Cached GetClassNode getClassNode, + @Cached(inline = false) IsSubtypeNode isSubtypeNode) { + // May be native or interop + return isSubtypeNode.execute(getClassNode.execute(inliningTarget, object), PythonBuiltinClassType.PBaseExceptionGroup); + } + + @Specialization + static boolean doInterop(@SuppressWarnings("unused") AbstractTruffleException exception) { + return false; + } + + @Fallback + static boolean doOther(@SuppressWarnings("unused") Object exception) { + return false; + } +} diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/lib/PyTraceBackPrint.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/lib/PyTraceBackPrint.java index 368b2778ee..d813f4c9f8 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/lib/PyTraceBackPrint.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/lib/PyTraceBackPrint.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -48,6 +48,7 @@ import static com.oracle.graal.python.nodes.StringLiterals.T_SPACE; import static com.oracle.graal.python.util.PythonUtils.TS_ENCODING; import static com.oracle.graal.python.util.PythonUtils.toTruffleStringUncached; +import static com.oracle.graal.python.util.PythonUtils.tsLiteral; import java.io.BufferedReader; import java.io.IOException; @@ -238,16 +239,20 @@ private static void printLineRepeated(Object out, int count) { fileWriteString(out, sbToString(sb)); } - private static void displayLine(Object out, TruffleString fileName, int lineNo, TruffleString name) { + private static void displayLine(Object out, TruffleString fileName, int lineNo, TruffleString name, int indent, TruffleString margin) { if (fileName == null || name == null) { return; } - final StringBuilder sb = newStringBuilder(" File \""); + boolean withIndentOrMargin = indent > 0 || !margin.isEmpty(); + final StringBuilder sb = withIndentOrMargin ? new StringBuilder() : newStringBuilder(" File \""); + if (withIndentOrMargin) { + append(sb, getIndent(indent), margin, " File \""); + } append(sb, fileName, "\", line ", lineNo, ", in ", name, J_NEWLINE); fileWriteString(out, sbToString(sb)); // ignore errors since we can't report them, can we? - displaySourceLine(out, fileName, lineNo, 4); + displaySourceLine(out, fileName, lineNo, 4, indent, margin); } protected static TruffleString getIndent(int indent) { @@ -287,9 +292,11 @@ protected static CharSequence getSourceLine(TruffleString fileName, int lineNo) return line; } - private static void displaySourceLine(Object out, TruffleString fileName, int lineNo, int indent) { + private static void displaySourceLine(Object out, TruffleString fileName, int lineNo, int indent, int marginIndent, TruffleString margin) { final CharSequence line = getSourceLine(fileName, lineNo); if (line != null) { + fileWriteString(out, getIndent(marginIndent)); + fileWriteString(out, margin); fileWriteString(out, getIndent(indent)); fileWriteString(out, trimLeft(line)); fileWriteString(out, J_NEWLINE); @@ -324,8 +331,14 @@ private static String trimLeft(CharSequence sequence) { return (st > 0 ? sequence.subSequence(st, len) : sequence).toString(); } + private static void printIndentedHeader(Object out, String header, int indent, String margin) { + String sb = " ".repeat(indent) + margin + header + "\n"; + fileWriteString(out, sb); + } + private static void printInternal(Node inliningTarget, TracebackBuiltins.GetTracebackFrameNode getTbFrameNode, - TracebackBuiltins.MaterializeTruffleStacktraceNode materializeStNode, Object out, PTraceback traceback, long limit) { + TracebackBuiltins.MaterializeTruffleStacktraceNode materializeStNode, Object out, PTraceback traceback, long limit, + int indent, TruffleString margin) { int depth = 0; TruffleString lastFile = null; int lastLine = -1; @@ -359,7 +372,7 @@ private static void printInternal(Node inliningTarget, TracebackBuiltins.GetTrac } cnt++; if (cnt <= TB_RECURSIVE_CUTOFF) { - displayLine(out, code.getFilename(), tb.getLineno(), code.getName()); + displayLine(out, code.getFilename(), tb.getLineno(), code.getName(), indent, margin); } tb = getNextTb(inliningTarget, materializeStNode, tb); } @@ -369,7 +382,7 @@ private static void printInternal(Node inliningTarget, TracebackBuiltins.GetTrac } public static void print(Node inliningTarget, TracebackBuiltins.GetTracebackFrameNode getTbFrameNode, TracebackBuiltins.MaterializeTruffleStacktraceNode materializeStNode, PythonModule sys, - Object out, Object tbObj) { + Object out, Object tbObj, boolean isExceptionGroup, boolean printMarginControl, int indent, TruffleString margin) { // Although we should be behind TB, we need cached nodes, because they may do stack walking // and for that they must be connected to the currently executing root. In practice, it's // not strictly necessary, because they will never request the current frame, but in order @@ -378,6 +391,10 @@ public static void print(Node inliningTarget, TracebackBuiltins.GetTracebackFram assert inliningTarget != null && inliningTarget.isAdoptable(); assert getTbFrameNode.isAdoptable(); + if (margin == null) { + margin = tsLiteral(""); + } + if (tbObj instanceof PTraceback tb) { long limit = TRACEBACK_LIMIT; final Object limitv = ReadAttributeFromObjectNode.getUncached().execute(sys, BuiltinNames.T_TRACEBACKLIMIT); @@ -387,8 +404,20 @@ public static void print(Node inliningTarget, TracebackBuiltins.GetTracebackFram return; } } - fileWriteString(out, "Traceback (most recent call last):\n"); - printInternal(inliningTarget, getTbFrameNode, materializeStNode, out, tb, limit); + if (isExceptionGroup) { + if (printMarginControl) { + printIndentedHeader(out, "Exception Group Traceback (most recent call last):", indent, margin.toString()); + } else { + printIndentedHeader(out, "Exception Group Traceback (most recent call last):", indent, "+ "); + } + } else { + if (printMarginControl) { + printIndentedHeader(out, "Traceback (most recent call last):", indent, ""); + } else { + printIndentedHeader(out, "Traceback (most recent call last):", indent, margin.toString()); + } + } + printInternal(inliningTarget, getTbFrameNode, materializeStNode, out, tb, limit, indent, margin); } else { throw PRaiseNode.raiseStatic(inliningTarget, PythonBuiltinClassType.SystemError, BAD_ARG_TO_INTERNAL_FUNC); }