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 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
}

0 commit comments

Comments
 (0)