Skip to content

Commit

Permalink
Add ClrBubbledException to handle exceptions bubbled from .Net to Pyt…
Browse files Browse the repository at this point in the history
…hon and back to .Net
  • Loading branch information
jhonabreul committed Oct 19, 2023
1 parent c13350f commit d861891
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 10 deletions.
77 changes: 77 additions & 0 deletions src/embed_tests/TestPythonException.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System;
using System.IO;
using System.Linq;

using NUnit.Framework;
using Python.Runtime;

Expand All @@ -10,6 +13,16 @@ public class TestPythonException
public void SetUp()
{
PythonEngine.Initialize();

// Add scripts folder to path in order to be able to import the test modules
string testPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "fixtures");
TestContext.Out.WriteLine(testPath);

using var str = Runtime.Runtime.PyString_FromString(testPath);
Assert.IsFalse(str.IsNull());
BorrowedReference path = Runtime.Runtime.PySys_GetObject("path");
Assert.IsFalse(path.IsNull);
Runtime.Runtime.PyList_Append(path, str.Borrow());
}

[OneTimeTearDown]
Expand Down Expand Up @@ -195,5 +208,69 @@ public void TestPythonException_Normalize_ThrowsWhenErrorSet()
Assert.Throws<InvalidOperationException>(() => pythonException.Normalize());
Exceptions.Clear();
}

[Test]
public void TestGetsPythonCodeInfoInStackTrace()
{
using (Py.GIL())
{
dynamic testClassModule = PyModule.FromString("TestGetsPythonCodeInfoInStackTrace_Module", @"
from clr import AddReference
AddReference(""Python.EmbeddingTest"")
from Python.EmbeddingTest import *
class TestPythonClass(TestPythonException.TestClass):
def CallThrow(self):
super().ThrowException()
");

try
{
var instance = testClassModule.TestPythonClass();
dynamic module = Py.Import("PyImportTest.SampleScript");
module.invokeMethod(instance, "CallThrow");
}
catch (ClrBubbledException ex)
{
Assert.AreEqual("Test Exception Message", ex.InnerException.Message);

var pythonTracebackLines = ex.PythonTraceback.TrimEnd('\n').Split('\n').Select(x => x.Trim()).ToList();
Assert.AreEqual(5, pythonTracebackLines.Count);

Assert.AreEqual("File \"none\", line 9, in CallThrow", pythonTracebackLines[0]);

Assert.IsTrue(new[]
{
"File ",
"fixtures\\PyImportTest\\SampleScript.py",
"line 5",
"in invokeMethodImpl"
}.All(x => pythonTracebackLines[1].Contains(x)));
Assert.AreEqual("getattr(instance, method_name)()", pythonTracebackLines[2]);

Assert.IsTrue(new[]
{
"File ",
"fixtures\\PyImportTest\\SampleScript.py",
"line 2",
"in invokeMethod"
}.All(x => pythonTracebackLines[3].Contains(x)));
Assert.AreEqual("invokeMethodImpl(instance, method_name)", pythonTracebackLines[4]);
}
catch (Exception ex)
{
Assert.Fail($"Unexpected exception: {ex}");
}
}
}

public class TestClass
{
public void ThrowException()
{
throw new ArgumentException("Test Exception Message");
}
}
}
}
5 changes: 5 additions & 0 deletions src/embed_tests/fixtures/PyImportTest/SampleScript.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
def invokeMethod(instance, method_name):
invokeMethodImpl(instance, method_name)

def invokeMethodImpl(instance, method_name):
getattr(instance, method_name)()
62 changes: 62 additions & 0 deletions src/runtime/ClrBubbledException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System;
using System.Text;

namespace Python.Runtime
{
/// <summary>
/// Provides an abstraction to represent a .Net exception that is bubbled to Python and back to .Net
/// and includes the Python traceback.
/// </summary>
public class ClrBubbledException : Exception
{
/// <summary>
/// The Python traceback
/// </summary>
public string PythonTraceback { get; }

/// <summary>
/// Creates a new instance of <see cref="ClrBubbledException"/>
/// </summary>
/// <param name="sourceException">The original exception that was thrown in .Net</param>
/// <param name="pythonTraceback">The Python traceback</param>
public ClrBubbledException(Exception sourceException, string pythonTraceback)
: base(sourceException.Message, sourceException)
{
PythonTraceback = pythonTraceback;
}

/// <summary>
/// StackTrace Property
/// </summary>
/// <remarks>
/// A string representing the exception stack trace.
/// </remarks>
public override string StackTrace
{
get
{
return PythonTraceback + "Underlying exception stack trace:" + Environment.NewLine + InnerException.StackTrace;
}
}

public override string ToString()
{
StringBuilder description = new StringBuilder();
description.AppendFormat("{0}: {1}{2}", InnerException.GetType().Name, Message, Environment.NewLine);
description.AppendFormat(" --> {0}", PythonTraceback);
description.AppendFormat(" --- End of Python traceback ---{0}", Environment.NewLine);

if (InnerException.InnerException != null)
{
description.AppendFormat(" ---> {0}", InnerException.InnerException);
description.AppendFormat("{0} --- End of inner exception stack trace ---{0}", Environment.NewLine);
}

description.Append(InnerException.StackTrace);
description.AppendFormat("{0} --- End of underlying exception ---", Environment.NewLine);

var str = description.ToString();
return str;
}
}
}
26 changes: 16 additions & 10 deletions src/runtime/PythonException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,26 +163,32 @@ private static Exception FromPyErr(BorrowedReference typeRef, BorrowedReference
var value = new PyObject(valRef);
var traceback = PyObject.FromNullableReference(tbRef);

Exception exception = null;

exceptionDispatchInfo = TryGetDispatchInfo(valRef);
if (exceptionDispatchInfo != null)
{
return exceptionDispatchInfo.SourceException;
exception = exceptionDispatchInfo.SourceException;
exceptionDispatchInfo = null;
}

if (ManagedType.GetManagedObject(valRef) is CLRObject { inst: Exception e })
else if (ManagedType.GetManagedObject(valRef) is CLRObject { inst: Exception e })
{
return e;
exception = e;
}

if (TryDecodePyErr(typeRef, valRef, tbRef) is { } pyErr)
else if (TryDecodePyErr(typeRef, valRef, tbRef) is { } pyErr)
{
return pyErr;
exception = pyErr;
}

if (PyObjectConversions.TryDecode(valRef, typeRef, typeof(Exception), out object? decoded)
else if (PyObjectConversions.TryDecode(valRef, typeRef, typeof(Exception), out object? decoded)
&& decoded is Exception decodedException)
{
return decodedException;
exception = decodedException;
}

if (!(exception is null))
{
using var _ = new Py.GILState();
return new ClrBubbledException(exception, TracebackToString(traceback));
}

using var cause = Runtime.PyException_GetCause(nValRef);
Expand Down

0 comments on commit d861891

Please sign in to comment.