8000 Merge pull request #1134 from losttech/PR/ExceptionsImprovement · pythonnet/pythonnet@7d8f754 · GitHub
[go: up one dir, main page]

Skip to content

Commit 7d8f754

Browse files
authored
Merge pull request #1134 from losttech/PR/ExceptionsImprovement
Improve Python <-> .NET exception integration
2 parents 7eac886 + c500a39 commit 7d8f754

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+793
-433
lines changed

.github/workflows/main.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ jobs:
5656
run: |
5757
python -m pythonnet.find_libpython --export | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
5858
59+
- name: Embedding tests
60+
run: dotnet test --runtime any-${{ matrix.platform }} src/embed_tests/
61+
5962
- name: Python Tests (Mono)
6063
if: ${{ matrix.os != 'windows' }}
6164
run: pytest --runtime mono
@@ -67,9 +70,6 @@ jobs:
6770
if: ${{ matrix.os == 'windows' }}
6871
run: pytest --runtime netfx
6972

70-
- name: Embedding tests
71-
run: dotnet test --runtime any-${{ matrix.platform }} src/embed_tests/
72-
7373
- name: Python tests run from .NET
7474
run: dotnet test --runtime any-${{ matrix.platform }} src/python_tests_runner/
7575

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ This document follows the conventions laid out in [Keep a CHANGELOG][].
1414
- Add GetPythonThreadID and Interrupt methods in PythonEngine
1515
- Ability to implement delegates with `ref` and `out` parameters in Python, by returning the modified parameter values in a tuple. ([#1355][i1355])
1616
- `PyType` - a wrapper for Python type objects, that also permits creating new heap types from `TypeSpec`
17+
- Improved exception handling:
18+
- exceptions can now be converted with codecs
19+
- `InnerException` and `__cause__` are propagated properly
1720

1821
### Changed
1922
- Drop support for Python 2, 3.4, and 3.5
@@ -44,7 +47,9 @@ One must now either use enum members (e.g. `MyEnum.Option`), or use enum constru
4447
- Sign Runtime DLL with a strong name
4548
- Implement loading through `clr_loader` instead of the included `ClrModule`, enables
4649
support for .NET Core
50+
- .NET and Python exceptions are preserved when crossing Python/.NET boundary
4751
- BREAKING: custom encoders are no longer called for instances of `System.Type`
52+
- `PythonException.Restore` no longer clears `PythonException` instance.
4853

4954
### Fixed
5055

@@ -70,6 +75,7 @@ One must now either use enum members (e.g. `MyEnum.Option`), or use enum constru
7075
### Removed
7176

7277
- implicit assembly loading (you have to explicitly `clr.AddReference` before doing import)
78+
- messages in `PythonException` no longer start with exception type
7379
- support for .NET Framework 4.0-4.6; Mono before 5.4. Python.NET now requires .NET Standard 2.0
7480
(see [the matrix](https://docs.microsoft.com/en-us/dotnet/standard/net-standard#net-implementation-support))
7581

src/embed_tests/Codecs.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,65 @@ def CanEncode(self, clr_type):
322322

323323
PythonEngine.Exec(PyCode);
324324
}
325+
326+
const string TestExceptionMessage = "Hello E377 World!";
327+
[Test]
328+
public void ExceptionEncoded()
329+
{
330+
PyObjectConversions.RegisterEncoder(new ValueErrorCodec());
331+
void CallMe() => throw new ValueErrorWrapper(TestExceptionMessage);
332+
var callMeAction = new Action(CallMe);
333+
using var _ = Py.GIL();
334+
using var scope = Py.CreateScope();
335+
scope.Exec(@"
336+
def call(func):
337+
try:
338+
func()
339+
except ValueError as e:
340+
return str(e)
341+
");
342+
var callFunc = scope.Get("call");
343+
string message = callFunc.Invoke(callMeAction.ToPython()).As<string>();
344+
Assert.AreEqual(TestExceptionMessage, message);
345+
}
346+
347+
[Test]
348+
public void ExceptionDecoded()
349+
{
350+
PyObjectConversions.RegisterDecoder(new ValueErrorCodec());
351+
using var _ = Py.GIL();
352+
using var scope = Py.CreateScope();
353+
var error = Assert.Throws<ValueErrorWrapper>(()
354+
=> PythonEngine.Exec($"raise ValueError('{TestExceptionMessage}')"));
355+
Assert.AreEqual(TestExceptionMessage, error.Message);
356+
}
357+
358+
class ValueErrorWrapper : Exception
359+
{
360+
public ValueErrorWrapper(string message) : base(message) { }
361+
}
362+
363+
class ValueErrorCodec : IPyObjectEncoder, IPyObjectDecoder
364+
{
365+
public bool CanDecode(PyObject objectType, Type targetType)
366+
=> this.CanEncode(targetType) && objectType.Equals(PythonEngine.Eval("ValueError"));
367+
368+
public bool CanEncode(Type type) => type == typeof(ValueErrorWrapper)
369+
|| typeof(ValueErrorWrapper).IsSubclassOf(type);
370+
371+
public bool TryDecode<T>(PyObject pyObj, out T value)
372+
{
373+
var message = pyObj.GetAttr("args")[0].As<string>();
374+
value = (T)(object)new ValueErrorWrapper(message);
375+
return true;
376+
}
377+
378+
public PyObject TryEncode(object value)
379+
{
380+
var error = (ValueErrorWrapper)value;
381+
return PythonEngine.Eval("ValueError").Invoke(error.Message.ToPython());
382+
}
383+
}
325384
}
326385

327386
/// <summary>

src/embed_tests/TestCallbacks.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public void TestNoOverloadException() {
2424
using (Py.GIL()) {
2525
dynamic callWith42 = PythonEngine.Eval("lambda f: f([42])");
2626
var error = Assert.Throws<PythonException>(() => callWith42(aFunctionThatCallsIntoPython.ToPython()));
27-
Assert.AreEqual("TypeError", error.PythonTypeName);
27+
Assert.AreEqual("TypeError", error.Type.Name);
2828
string expectedArgTypes = "(<class 'list'>)";
2929
StringAssert.EndsWith(expectedArgTypes, error.Message);
3030
}

src/embed_tests/TestPyFloat.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public void StringBadCtor()
9595

9696
var ex = Assert.Throws<PythonException>(() => a = new PyFloat(i));
9797

98-
StringAssert.StartsWith("ValueError : could not convert string to float", ex.Message);
98+
StringAssert.StartsWith("could not convert string to float", ex.Message);
9999
Assert.IsNull(a);
100100
}
101101

@@ -132,7 +132,7 @@ public void AsFloatBad()
132132
PyFloat a = null;
133133

134134
var ex = Assert.Throws<PythonException>(() => a = PyFloat.AsFloat(s));
135-
StringAssert.StartsWith("ValueError : could not convert string to float", ex.Message);
135+
StringAssert.StartsWith("could not convert string to float", ex.Message);
136136
Assert.IsNull(a);
137137
}
138138
}

src/embed_tests/TestPyInt.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ public void TestCtorBadString()
128128

129129
var ex = Assert.Throws<PythonException>(() => a = new PyInt(i));
130130

131-
StringAssert.StartsWith("ValueError : invalid literal for int", ex.Message);
131+
StringAssert.StartsWith("invalid literal for int", ex.Message);
132132
Assert.IsNull(a);
133133
}
134134

@@ -161,7 +161,7 @@ public void TestAsIntBad()
161161
PyInt a = null;
162162

163163
var ex = Assert.Throws<PythonException>(() => a = PyInt.AsInt(s));
164-
StringAssert.StartsWith("ValueError : invalid literal for int", ex.Message);
164+
StringAssert.StartsWith("invalid literal for int", ex.Message);
165165
Assert.IsNull(a);
166166
}
167167

src/embed_tests/TestPyList.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public void TestStringAsListType()
4141

4242
var ex = Assert.Throws<PythonException>(() => t = PyList.AsList(i));
4343

44-
Assert.AreEqual("TypeError : 'int' object is not iterable", ex.Message);
44+
Assert.AreEqual("'int' object is not iterable", ex.Message);
4545
Assert.IsNull(t);
4646
}
4747

src/embed_tests/TestPyLong.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ public void TestCtorBadString()
144144

145145
var ex = Assert.Throws<PythonException>(() => a = new PyLong(i));
146146

147-
StringAssert.StartsWith("ValueError : invalid literal", ex.Message);
147+
StringAssert.StartsWith("invalid literal", ex.Message);
148148
Assert.IsNull(a);
149149
}
150150

@@ -177,7 +177,7 @@ public void TestAsLongBad()
177177
PyLong a = null;
178178

179179
var ex = Assert.Throws<PythonException>(() => a = PyLong.AsLong(s));
180-
StringAssert.StartsWith("ValueError : invalid literal", ex.Message);
180+
StringAssert.StartsWith("invalid literal", ex.Message);
181181
Assert.IsNull(a);
182182
}
183183

src/embed_tests/TestPyTuple.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ public void TestPyTupleInvalidAppend()
104104

105105
var ex = Assert.Throws<PythonException>(() => t.Concat(s));
106106

107-
StringAssert.StartsWith("TypeError : can only concatenate tuple", ex.Message);
107+
StringAssert.StartsWith("can only concatenate tuple", ex.Message);
108108
Assert.AreEqual(0, t.Length());
109109
Assert.IsEmpty(t);
110110
}
@@ -164,7 +164,7 @@ public void TestInvalidAsTuple()
164164

165165
var ex = Assert.Throws<PythonException>(() => t = PyTuple.AsTuple(i));
166166

167-
Assert.AreEqual("TypeError : 'int' object is not iterable", ex.Message);
167+
Assert.AreEqual("'int' object is not iterable", ex.Message);
168168
Assert.IsNull(t);
169169
}
170170
}

src/embed_tests/TestPyType.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public void CanCreateHeapType()
4040

4141
using var type = new PyType(spec);
4242
Assert.AreEqual(name, type.GetAttr("__name__").As<string>());
43+
Assert.AreEqual(name, type.Name);
4344
Assert.AreEqual(docStr, type.GetAttr("__doc__").As<string>());
4445
}
4546
}

src/embed_tests/TestPyWith.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def fail(self):
5151
catch (PythonException e)
5252
{
5353
TestContext.Out.WriteLine(e.Message);
54-
Assert.IsTrue(e.Message.Contains("ZeroDivisionError"));
54+
Assert.IsTrue(e.Type.Name == "ZeroDivisionError");
5555
}
5656
}
5757

src/embed_tests/TestPythonException.cs

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -30,31 +30,61 @@ public void TestMessage()
3030

3131
var ex = Assert.Throws<PythonException>(() => foo = list[0]);
3232

33-
Assert.AreEqual("IndexError : list index out of range", ex.Message);
33+
Assert.AreEqual("list index out of range", ex.Message);
34+
Assert.IsNull(foo);
35+
}
36+
37+
[Test]
38+
public void TestType()
39+
{
40+
var list = new PyList();
41+
PyObject foo = null;
42+
43+
var ex = Assert.Throws<PythonException>(() => foo = list[0]);
44+
45+
Assert.AreEqual("IndexError", ex.Type.Name);
3446
Assert.IsNull(foo);
3547
}
3648

3749
[Test]
3850
public void TestNoError()
3951
{
40-
var e = new PythonException(); // There is no PyErr to fetch
41-
Assert.AreEqual("", e.Message);
52+
// There is no PyErr to fetch
53+
Assert.Throws<InvalidOperationException>(() => PythonException.FetchCurrentRaw());
54+
var currentError = PythonException.FetchCurrentOrNullRaw();
55+
Assert.IsNull(currentError);
4256
}
4357

4458
[Test]
45-
public void TestPythonErrorTypeName()
59+
public void TestNestedExceptions()
4660
{
4761
try
4862
{
49-
var module = PyModule.Import("really____unknown___module");
50-
Assert.Fail("Unknown module should not be loaded");
63+
PythonEngine.Exec(@"
64+
try:
65+
raise Exception('inner')
66+
except Exception as ex:
67+
raise Exception('outer') from ex
68+
");
5169
}
5270
catch (PythonException ex)
5371
{
54-
Assert.That(ex.PythonTypeName, Is.EqualTo("ModuleNotFoundError").Or.EqualTo("ImportError"));
72+
Assert.That(ex.InnerException, Is.InstanceOf<PythonException>());
73+
Assert.That(ex.InnerException.Message, Is.EqualTo("inner"));
5574
}
5675
}
5776

77+
[Test]
78+
public void InnerIsEmptyWithNoCause()
79+
{
80+
var list = new PyList();
81+
PyObject foo = null;
82+
83+
var ex = Assert.Throws<PythonException>(() => foo = list[0]);
84+
85+
Assert.IsNull(ex.InnerException);
86+
}
87+
5888
[Test]
5989
public void TestPythonExceptionFormat()
6090
{
@@ -83,13 +113,6 @@ public void TestPythonExceptionFormat()
83113
}
84114
}
85115

86-
[Test]
87-
public void TestPythonExceptionFormatNoError()
88-
{
89-
var ex = new PythonException();
90-
Assert.AreEqual(ex.StackTrace, ex.Format());
91-
}
92-
93116
[Test]
94117
public void TestPythonExceptionFormatNoTraceback()
95118
{
@@ -132,30 +155,27 @@ def __init__(self, val):
132155
Assert.IsTrue(scope.TryGet("TestException", out PyObject type));
133156

134157
PyObject str = "dummy string".ToPython();
135-
IntPtr typePtr = type.Handle;
136-
IntPtr strPtr = str.Handle;
137-
IntPtr tbPtr = Runtime.Runtime.None.Handle;
138-
Runtime.Runtime.XIncref(typePtr);
139-
Runtime.Runtime.XIncref(strPtr);
140-
Runtime.Runtime.XIncref(tbPtr);
158+
var typePtr = new NewReference(type.Reference);
159+
var strPtr = new NewReference(str.Reference);
160+
var tbPtr = new NewReference(Runtime.Runtime.None.Reference);
141161
Runtime.Runtime.PyErr_NormalizeException(ref typePtr, ref strPtr, ref tbPtr);
142162

143-
using (PyObject typeObj = new PyObject(typePtr), strObj = new PyObject(strPtr), tbObj = new PyObject(tbPtr))
144-
{
145-
// the type returned from PyErr_NormalizeException should not be the same type since a new
146-
// exception was raised by initializing the exception
147-
Assert.AreNotEqual(type.Handle, typePtr);
148-
// the message should now be the string from the throw exception during normalization
149-
Assert.AreEqual("invalid literal for int() with base 10: 'dummy string'", strObj.ToString());
150-
}
163+
using var typeObj = typePtr.MoveToPyObject();
164+
using var strObj = strPtr.MoveToPyObject();
165+
using var tbObj = tbPtr.MoveToPyObject();
166+
// the type returned from PyErr_NormalizeException should not be the same type since a new
167+
// exception was raised by initializing the exception
168+
Assert.AreNotEqual(type.Handle, typeObj.Handle);
169+
// the message should now be the string from the throw exception during normalization
170+
Assert.AreEqual("invalid literal for int() with base 10: 'dummy string'", strObj.ToString());
151171
}
152172
}
153173

154174
[Test]
155175
public void TestPythonException_Normalize_ThrowsWhenErrorSet()
156176
{
157177
Exceptions.SetError(Exceptions.TypeError, "Error!");
158-
var pythonException = new PythonException();
178+
var pythonException = PythonException.FetchCurrentRaw();
159179
Exceptions.SetError(Exceptions.TypeError, "Another error");
160180
Assert.Throws<InvalidOperationException>(() => pythonException.Normalize());
161181
}

src/embed_tests/TestRuntime.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using System.Collections.Generic;
33
using NUnit.Framework;
44
using Python.Runtime;
5-
using Python.Runtime.Platform;
65

76
namespace Python.EmbeddingTest
87
{
@@ -102,7 +101,7 @@ public static void PyCheck_Iter_PyObject_IsIterable_ThreadingLock_Test()
102101
Exceptions.ErrorCheck(threadingDict);
103102
var lockType = Runtime.Runtime.PyDict_GetItemString(threadingDict, "Lock");
104103
if (lockType.IsNull)
105-
throw new PythonException();
104+
throw PythonException.ThrowLastAsClrException();
106105

107106
using var args = NewReference.DangerousFromPointer(Runtime.Runtime.PyTuple_New(0));
108107
using var lockInstance = Runtime.Runtime.PyObject_CallObject(lockType, args);

src/embed_tests/pyimport.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,7 @@ import clr
102102
clr.AddReference('{path}')
103103
";
104104

105-
var error = Assert.Throws<PythonException>(() => PythonEngine.Exec(code));
106-
Assert.AreEqual(nameof(FileLoadException), error.PythonTypeName);
105+
Assert.Throws<FileLoadException>(() => PythonEngine.Exec(code));
107106
}
108107
}
109108
}

src/embed_tests/pyinitialize.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,10 @@ public static void TestRunExitFuncs()
158158
catch (PythonException e)
159159
{
160160
string msg = e.ToString();
161+
bool isImportError = e.Is(Exceptions.ImportError);
161162
Runtime.Runtime.Shutdown();
162163

163-
if (e.IsMatches(Exceptions.ImportError))
164+
if (isImportError)
164165
{
165166
Assert.Ignore("no atexit module");
166167
}

src/runtime/BorrowedReference.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ public IntPtr DangerousGetAddress()
1616
/// <summary>Gets a raw pointer to the Python object</summary>
1717
public IntPtr DangerousGetAddressOrNull() => this.pointer;
1818

19+
public static BorrowedReference Null => new BorrowedReference();
20+
1921
/// <summary>
2022
/// Creates new instance of <see cref="BorrowedReference"/> from raw pointer. Unsafe.
2123
/// </summary>

0 commit comments

Comments
 (0)
0