Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
4 changes: 4 additions & 0 deletions src/core/IronPython/Runtime/Exceptions/PythonExceptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
6 changes: 3 additions & 3 deletions src/core/IronPython/Runtime/Operations/PythonOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 15 additions & 14 deletions src/core/IronPython/Runtime/PythonContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1394,28 +1394,29 @@ static void Flush(CodeContext context, object obj) {
public override string FormatException(Exception exception) {
ContractUtils.RequiresNotNull(exception, nameof(exception));

return FormatException(exception, new HashSet<object>(ReferenceEqualityComparer<object>.Instance));
}

private string FormatException(Exception exception, HashSet<object> seen) {
if (exception is SyntaxErrorException syntax_error) {
return FormatPythonSyntaxError(syntax_error);
}

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));
Expand Down
107 changes: 106 additions & 1 deletion tests/suite/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__

Expand Down Expand Up @@ -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__)
Loading