using System; using System.IO; using System.Linq; using NUnit.Framework; using Python.Runtime; namespace Python.EmbeddingTest { public class TestPythonException { [OneTimeSetUp] 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] public void Dispose() { PythonEngine.Shutdown(); } [Test] public void TestMessage() { var list = new PyList(); PyObject foo = null; var ex = Assert.Throws(() => foo = list[0]); Assert.AreEqual("list index out of range", ex.Message); Assert.IsNull(foo); } [Test] public void TestType() { var list = new PyList(); PyObject foo = null; var ex = Assert.Throws(() => foo = list[0]); Assert.AreEqual("IndexError", ex.Type.Name); Assert.IsNull(foo); } [Test] public void TestMessageComplete() { using (Py.GIL()) { try { // importing a module with syntax error 'x = 01' will throw PyModule.FromString(Guid.NewGuid().ToString(), "x = 01"); } catch (PythonException exception) { Assert.True(exception.Message.Contains("x = 01")); return; } Assert.Fail("No Exception was thrown!"); } } [Test] public void TestNoError() { // There is no PyErr to fetch Assert.Throws(() => PythonException.FetchCurrentRaw()); var currentError = PythonException.FetchCurrentOrNullRaw(); Assert.IsNull(currentError); } [Test] public void TestNestedExceptions() { try { PythonEngine.Exec(@" try: raise Exception('inner') except Exception as ex: raise Exception('outer') from ex "); } catch (PythonException ex) { Assert.That(ex.InnerException, Is.InstanceOf()); Assert.That(ex.InnerException.Message, Is.EqualTo("inner")); } } [Test] public void InnerIsEmptyWithNoCause() { var list = new PyList(); PyObject foo = null; var ex = Assert.Throws(() => foo = list[0]); Assert.IsNull(ex.InnerException); } [Test] public void TestPythonExceptionFormat() { try { PythonEngine.Exec("raise ValueError('Error!')"); Assert.Fail("Exception should have been raised"); } catch (PythonException ex) { // Console.WriteLine($"Format: {ex.Format()}"); // Console.WriteLine($"Stacktrace: {ex.StackTrace}"); Assert.That( ex.Format(), Does.Contain("Traceback") .And.Contains("(most recent call last):") .And.Contains("ValueError: Error!") ); // Check that the stacktrace is properly formatted Assert.That( ex.StackTrace, Does.Not.StartWith("[") .And.Not.Contain("\\n") ); } } [Test] public void TestPythonExceptionFormatNoTraceback() { try { var module = PyModule.Import("really____unknown___module"); Assert.Fail("Unknown module should not be loaded"); } catch (PythonException ex) { // ImportError/ModuleNotFoundError do not have a traceback when not running in a script Assert.AreEqual(ex.StackTrace, ex.Format()); } } [Test] public void TestPythonExceptionFormatNormalized() { try { PythonEngine.Exec("a=b\n"); Assert.Fail("Exception should have been raised"); } catch (PythonException ex) { Assert.AreEqual("Traceback (most recent call last):\n File \"\", line 1, in \nNameError: name 'b' is not defined\n", ex.Format()); } } [Test] public void TestPythonException_PyErr_NormalizeException() { using (var scope = Py.CreateScope()) { scope.Exec(@" class TestException(NameError): def __init__(self, val): super().__init__(val) x = int(val)"); Assert.IsTrue(scope.TryGet("TestException", out PyObject type)); PyObject str = "dummy string".ToPython(); var typePtr = new NewReference(type.Reference); var strPtr = new NewReference(str.Reference); var tbPtr = new NewReference(Runtime.Runtime.None.Reference); Runtime.Runtime.PyErr_NormalizeException(ref typePtr, ref strPtr, ref tbPtr); using var typeObj = typePtr.MoveToPyObject(); using var strObj = strPtr.MoveToPyObject(); using var tbObj = tbPtr.MoveToPyObject(); // the type returned from PyErr_NormalizeException should not be the same type since a new // exception was raised by initializing the exception Assert.AreNotEqual(type.Handle, typeObj.Handle); // the message should now be the string from the throw exception during normalization Assert.AreEqual("invalid literal for int() with base 10: 'dummy string'", strObj.ToString()); } } [Test] public void TestPythonException_Normalize_ThrowsWhenErrorSet() { Exceptions.SetError(Exceptions.TypeError, "Error!"); var pythonException = PythonException.FetchCurrentRaw(); Exceptions.SetError(Exceptions.TypeError, "Another error"); Assert.Throws(() => 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}"); } } } [Test] public void TestGetsPythonCodeInfoInStackTraceForNestedInterop() { using (Py.GIL()) { dynamic testClassModule = PyModule.FromString("TestGetsPythonCodeInfoInStackTraceForNestedInterop_Module", @" from clr import AddReference AddReference(""Python.EmbeddingTest"") AddReference(""System"") from Python.EmbeddingTest import * from System import Action class TestPythonClass(TestPythonException.TestClass): def CallThrow(self): super().ThrowExceptionNested() def GetThrowAction(): return Action(CallThrow) def CallThrow(): TestPythonClass().CallThrow() "); try { var action = testClassModule.GetThrowAction(); action(); } 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(4, pythonTracebackLines.Count); Assert.IsTrue(new[] { "File ", "fixtures\\PyImportTest\\SampleScript.py", "line 5", "in invokeMethodImpl" }.All(x => pythonTracebackLines[0].Contains(x))); Assert.AreEqual("getattr(instance, method_name)()", pythonTracebackLines[1]); Assert.IsTrue(new[] { "File ", "fixtures\\PyImportTest\\SampleScript.py", "line 2", "in invokeMethod" }.All(x => pythonTracebackLines[2].Contains(x))); Assert.AreEqual("invokeMethodImpl(instance, method_name)", pythonTracebackLines[3]); } catch (Exception ex) { Assert.Fail($"Unexpected exception: {ex}"); } } } public class TestClass { public void ThrowException() { throw new ArgumentException("Test Exception Message"); } public void ThrowExceptionNested() { using var _ = Py.GIL(); dynamic module = Py.Import("PyImportTest.SampleScript"); module.invokeMethod(this, "ThrowException"); } } } }