diff --git a/src/core/IronPython/Runtime/Exceptions/PythonExceptions.BaseException.cs b/src/core/IronPython/Runtime/Exceptions/PythonExceptions.BaseException.cs index 012720feb..57786de4f 100644 --- a/src/core/IronPython/Runtime/Exceptions/PythonExceptions.BaseException.cs +++ b/src/core/IronPython/Runtime/Exceptions/PythonExceptions.BaseException.cs @@ -304,8 +304,6 @@ public TraceBack? __traceback__ { internal bool HasCause => __cause__ != null; - internal bool IsImplicitException => __cause__ == null && __cause__ != __context__; - void IPythonObject.SetPythonType(PythonType/*!*/ newType) { if (_type.IsSystemType || newType.IsSystemType) { throw PythonOps.TypeError("__class__ assignment can only be performed on user defined types"); diff --git a/src/core/IronPython/Runtime/Exceptions/PythonExceptions.cs b/src/core/IronPython/Runtime/Exceptions/PythonExceptions.cs index 439538ad6..9ca53520c 100644 --- a/src/core/IronPython/Runtime/Exceptions/PythonExceptions.cs +++ b/src/core/IronPython/Runtime/Exceptions/PythonExceptions.cs @@ -833,6 +833,10 @@ public static BaseException ToPython(System.Exception/*!*/ clrException) { #endif res = ToPythonNewStyle(clrException); + if (clrException.InnerException?.GetPythonException() is BaseException inner) { + // e.g. a Python exception wrapped in a AggregateException + res.__context__ = inner; + } clrException.SetPythonException(res); } diff --git a/src/core/IronPython/Runtime/Operations/PythonOps.cs b/src/core/IronPython/Runtime/Operations/PythonOps.cs index 0a2ac20ae..fe62ab6d0 100644 --- a/src/core/IronPython/Runtime/Operations/PythonOps.cs +++ b/src/core/IronPython/Runtime/Operations/PythonOps.cs @@ -2591,9 +2591,9 @@ private static Exception MakeExceptionWorker(CodeContext/*!*/ context, object? t } var contextException = GetRawContextException(); - if (keepContext || contextException == baseException) { - // throw() does not change the context, and raising the exception being handled - // (including a bare raise) does not make it its own context + if (keepContext || contextException is null || contextException == baseException) { + // throw() does not change the context, raising outside an except block keeps the previous context, + // and raising the exception being handled (including a bare raise) does not make it its own context contextException = baseException.RawContext; } else { // avoid creating cycles through the context chain diff --git a/src/core/IronPython/Runtime/PythonContext.cs b/src/core/IronPython/Runtime/PythonContext.cs index f82e70d6b..5445e3cc4 100644 --- a/src/core/IronPython/Runtime/PythonContext.cs +++ b/src/core/IronPython/Runtime/PythonContext.cs @@ -1394,6 +1394,10 @@ static void Flush(CodeContext context, object obj) { public override string FormatException(Exception exception) { ContractUtils.RequiresNotNull(exception, nameof(exception)); + return FormatException(exception, new HashSet(ReferenceEqualityComparer.Instance)); + } + + private string FormatException(Exception exception, HashSet seen) { if (exception is SyntaxErrorException syntax_error) { return FormatPythonSyntaxError(syntax_error); } @@ -1401,21 +1405,18 @@ public override string FormatException(Exception exception) { StringBuilder result = new StringBuilder(); var pythonEx = PythonExceptions.ToPython(exception); - - if (exception.InnerException != null) { - if (exception.InnerException.GetPythonException() is PythonExceptions.BaseException pythonInnerException) { - // add the nested/chained exception - result.AppendLine(FormatException(exception.InnerException)); - result.AppendLine(); - - // check whether this is implicit or explicit - if (!((PythonExceptions.BaseException)pythonEx).IsImplicitException) { - result.AppendLine("The above exception was the direct cause of the following exception."); - } else { - result.AppendLine("During handing of the above exception, another exception occurred:"); - } - result.AppendLine(); + seen.Add(pythonEx); + + var chained = pythonEx.HasCause ? pythonEx.__cause__ : pythonEx.__suppress_context__ ? null : pythonEx.RawContext; + if (chained is not null && !seen.Contains(chained)) { + result.AppendLine(FormatException(chained.GetClrException(), seen)); + result.AppendLine(); + if (pythonEx.HasCause) { + result.AppendLine("The above exception was the direct cause of the following exception:"); + } else { + result.AppendLine("During handling of the above exception, another exception occurred:"); } + result.AppendLine(); } result.Append(FormatStackTraces(exception)); diff --git a/tests/suite/test_traceback.py b/tests/suite/test_traceback.py index c2110730b..55d0cd139 100644 --- a/tests/suite/test_traceback.py +++ b/tests/suite/test_traceback.py @@ -6,7 +6,12 @@ import sys import unittest -from iptest import IronPythonTestCase, is_cli, run_test +from iptest import IronPythonTestCase, is_cli, run_test, skipUnlessIronPython + +try: + from test.support.script_helper import assert_python_failure +except ImportError: # Python 3.4 + from test.script_helper import assert_python_failure FILE = __file__ @@ -538,4 +543,104 @@ def test_cp11923_second(self): os.remove(_t_test) os.remove(_t_test_a) + def test_chained_exception_output(self): + cause = "The above exception was the direct cause of the following exception:" + context = "During handling of the above exception, another exception occurred:" + + def run_code(code): + _, _, err = assert_python_failure("-S", "-c", code) + return err.decode("ascii") + + def run(raise_stmt): + return run_code("try:\n {}['inner']\nexcept KeyError:\n " + raise_stmt) + + err = run("raise ValueError('outer')") + self.assertIn("KeyError", err) + self.assertIn(context, err) + self.assertNotIn(cause, err) + self.assertIn("ValueError: outer", err) + + err = run("raise ValueError('outer') from TypeError('cause')") + self.assertNotIn("KeyError", err) + self.assertIn("TypeError: cause", err) + self.assertIn(cause, err) + self.assertNotIn(context, err) + self.assertIn("ValueError: outer", err) + + err = run("raise ValueError('outer') from None") + self.assertNotIn("KeyError", err) + self.assertNotIn(cause, err) + self.assertNotIn(context, err) + self.assertIn("ValueError: outer", err) + + # raising the same exception object again updates the chain + err = run_code(""" +e = ValueError('outer') +try: + {}['inner'] +except KeyError: + try: + raise e + except ValueError: + pass +raise e from TypeError('cause') +""") + self.assertNotIn("KeyError", err) + self.assertIn("TypeError: cause", err) + self.assertIn(cause, err) + + err = run_code(""" +e = ValueError('outer') +try: + raise e +except ValueError: + pass +try: + {}['inner'] +except KeyError: + raise e +""") + self.assertIn("KeyError", err) + self.assertIn(context, err) + + # raising outside an except block keeps the previous context + err = run_code(""" +e = ValueError('outer') +try: + {}['inner'] +except KeyError: + try: + raise e + except ValueError: + pass +raise e +""") + self.assertIn("KeyError", err) + self.assertIn(context, err) + + # cause cycle + err = run_code(""" +a = ValueError('a') +b = TypeError('b') +a.__cause__ = b +b.__cause__ = a +raise a +""") + self.assertEqual(err.count("ValueError: a"), 1) + self.assertEqual(err.count("TypeError: b"), 1) + self.assertEqual(err.count(cause), 1) + + @skipUnlessIronPython() + def test_chained_exception_output_clr(self): + # a Python exception wrapped by .NET (AggregateException) is shown as the context + _, _, err = assert_python_failure("-S", "-c", """ +import System +def f(): + raise ValueError('inner') +System.Threading.Tasks.Task.Run(System.Action(f)).Wait() +""") + err = err.decode("ascii") + self.assertIn("ValueError: inner", err) + self.assertIn("During handling of the above exception, another exception occurred:", err) + run_test(__name__)