Skip to content

Commit c0fe430

Browse files
committed
reworked PythonException:
Removed private fields, apart from ones returned by `PyErr_Fetch`. Corresponding property values are now generated on demand. Added FetchCurrent*Raw for internal consumption. `PythonException.Type` is now of type `PyType`. Use C API functions `PyException_GetCause` and `PyException_GetTraceback` instead of trying to read via attributes by name. `PythonException` instances are no longer disposable. You can still dispose `.Type`, `.Value` and `.Traceback`, but it is not recommended, as they may be shared with other instances.
1 parent 257a765 commit c0fe430

31 files changed

+415
-334
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ One must now either use enum members (e.g. `MyEnum.Option`), or use enum constru
4949
support for .NET Core
5050
- .NET and Python exceptions are preserved when crossing Python/.NET boundary
5151
- BREAKING: custom encoders are no longer called for instances of `System.Type`
52+
- `PythonException.Restore` no longer clears `PythonException` instance.
5253

5354
### Fixed
5455

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

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

src/embed_tests/Codecs.cs

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -325,56 +325,58 @@ def CanEncode(self, clr_type):
325325

326326
const string TestExceptionMessage = "Hello World!";
327327
[Test]
328-
public void ExceptionEncoded() {
328+
public void ExceptionEncoded()
329+
{
329330
PyObjectConversions.RegisterEncoder(new ValueErrorCodec());
330331
void CallMe() => throw new ValueErrorWrapper(TestExceptionMessage);
331332
var callMeAction = new Action(CallMe);
332-
using (var _ = Py.GIL())
333-
using (var scope = Py.CreateScope())
334-
{
335-
scope.Exec(@"
333+
using var _ = Py.GIL();
334+
using var scope = Py.CreateScope();
335+
scope.Exec(@"
336336
def call(func):
337337
try:
338338
func()
339339
except ValueError as e:
340340
return str(e)
341341
");
342-
var callFunc = scope.Get("call");
343-
string message = callFunc.Invoke(callMeAction.ToPython()).As<string>();
344-
Assert.AreEqual(TestExceptionMessage, message);
345-
}
342+
var callFunc = scope.Get("call");
343+
string message = callFunc.Invoke(callMeAction.ToPython()).As<string>();
344+
Assert.AreEqual(TestExceptionMessage, message);
346345
}
347346

348347
[Test]
349-
public void ExceptionDecoded() {
348+
public void ExceptionDecoded()
349+
{
350350
PyObjectConversions.RegisterDecoder(new ValueErrorCodec());
351-
using (var _ = Py.GIL())
352-
using (var scope = Py.CreateScope())
353-
{
354-
var error = Assert.Throws<ValueErrorWrapper>(()
355-
=> PythonEngine.Exec($"raise ValueError('{TestExceptionMessage}')"));
356-
Assert.AreEqual(TestExceptionMessage, error.Message);
357-
}
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);
358356
}
359357

360-
class ValueErrorWrapper : Exception {
358+
class ValueErrorWrapper : Exception
359+
{
361360
public ValueErrorWrapper(string message) : base(message) { }
362361
}
363362

364-
class ValueErrorCodec : IPyObjectEncoder, IPyObjectDecoder {
363+
class ValueErrorCodec : IPyObjectEncoder, IPyObjectDecoder
364+
{
365365
public bool CanDecode(PyObject objectType, Type targetType)
366366
=> this.CanEncode(targetType) && objectType.Equals(PythonEngine.Eval("ValueError"));
367367

368368
public bool CanEncode(Type type) => type == typeof(ValueErrorWrapper)
369369
|| typeof(ValueErrorWrapper).IsSubclassOf(type);
370370

371-
public bool TryDecode<T>(PyObject pyObj, out T value) {
371+
public bool TryDecode<T>(PyObject pyObj, out T value)
372+
{
372373
var message = pyObj.GetAttr("args")[0].As<string>();
373374
value = (T)(object)new ValueErrorWrapper(message);
374375
return true;
375376
}
376377

377-
public PyObject TryEncode(object value) {
378+
public PyObject TryEncode(object value)
379+
{
378380
var error = (ValueErrorWrapper)value;
379381
return PythonEngine.Eval("ValueError").Invoke(error.Message.ToPython());
380382
}

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
@@ -39,6 +39,7 @@ public void CanCreateHeapType()
3939

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

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: 27 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -30,29 +30,29 @@ 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);
3434
Assert.IsNull(foo);
3535
}
3636

3737
[Test]
38-
public void TestNoError()
38+
public void TestType()
3939
{
40-
var e = new PythonException(); // There is no PyErr to fetch
41-
Assert.AreEqual("", e.Message);
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);
46+
Assert.IsNull(foo);
4247
}
4348

4449
[Test]
45-
public void TestPythonErrorTypeName()
50+
public void TestNoError()
4651
{
47-
try
48-
{
49-
var module = PyModule.Import("really____unknown___module");
50-
Assert.Fail("Unknown module should not be loaded");
51-
}
52-
catch (PythonException ex)
53-
{
54-
Assert.That(ex.PythonTypeName, Is.EqualTo("ModuleNotFoundError").Or.EqualTo("ImportError"));
55-
}
52+
// There is no PyErr to fetch
53+
Assert.Throws<InvalidOperationException>(() => PythonException.FetchCurrentRaw());
54+
var currentError = PythonException.FetchCurrentOrNullRaw();
55+
Assert.IsNull(currentError);
5656
}
5757

5858
[Test]
@@ -70,7 +70,7 @@ raise Exception('outer') from ex
7070
catch (PythonException ex)
7171
{
7272
Assert.That(ex.InnerException, Is.InstanceOf<PythonException>());
73-
Assert.That(ex.InnerException.Message, Is.EqualTo("Exception : inner"));
73+
Assert.That(ex.InnerException.Message, Is.EqualTo("inner"));
7474
}
7575
}
7676

@@ -113,13 +113,6 @@ public void TestPythonExceptionFormat()
113113
}
114114
}
115115

116-
[Test]
117-
public void TestPythonExceptionFormatNoError()
118-
{
119-
var ex = new PythonException();
120-
Assert.AreEqual(ex.StackTrace, ex.Format());
121-
}
122-
123116
[Test]
124117
public void TestPythonExceptionFormatNoTraceback()
125118
{
@@ -162,30 +155,27 @@ def __init__(self, val):
162155
Assert.IsTrue(scope.TryGet("TestException", out PyObject type));
163156

164157
PyObject str = "dummy string".ToPython();
165-
IntPtr typePtr = type.Handle;
166-
IntPtr strPtr = str.Handle;
167-
IntPtr tbPtr = Runtime.Runtime.None.Handle;
168-
Runtime.Runtime.XIncref(typePtr);
169-
Runtime.Runtime.XIncref(strPtr);
170-
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);
171161
Runtime.Runtime.PyErr_NormalizeException(ref typePtr, ref strPtr, ref tbPtr);
172162

173-
using (PyObject typeObj = new PyObject(typePtr), strObj = new PyObject(strPtr), tbObj = new PyObject(tbPtr))
174-
{
175-
// the type returned from PyErr_NormalizeException should not be the same type since a new
176-
// exception was raised by initializing the exception
177-
Assert.AreNotEqual(type.Handle, typePtr);
178-
// the message should now be the string from the throw exception during normalization
179-
Assert.AreEqual("invalid literal for int() with base 10: 'dummy string'", strObj.ToString());
180-
}
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());
181171
}
182172
}
183173

184174
[Test]
185175
public void TestPythonException_Normalize_ThrowsWhenErrorSet()
186176
{
187177
Exceptions.SetError(Exceptions.TypeError, "Error!");
188-
var pythonException = new PythonException();
178+
var pythonException = PythonException.FetchCurrentRaw();
189179
Exceptions.SetError(Exceptions.TypeError, "Another error");
190180
Assert.Throws<InvalidOperationException>(() => pythonException.Normalize());
191181
}

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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ public static void TestRunExitFuncs()
160160
string msg = e.ToString();
161161
Runtime.Runtime.Shutdown();
162162

163-
if (e.IsMatches(Exceptions.ImportError))
163+
if (e.Is(Exceptions.ImportError))
164164
{
165165
Assert.Ignore("no atexit module");
166166
}

src/runtime/StolenReference.cs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,46 @@
11
namespace Python.Runtime
22
{
33
using System;
4+
using System.Diagnostics.Contracts;
45

56
/// <summary>
6-
/// Should only be used for the arguments of Python C API functions, that steal references
7+
/// Should only be used for the arguments of Python C API functions, that steal references,
8+
/// and internal <see cref="PyObject"/> constructors.
79
/// </summary>
810
[NonCopyable]
911
readonly ref struct StolenReference
1012
{
11-
readonly IntPtr pointer;
13+
internal readonly IntPtr Pointer;
1214

1315
internal StolenReference(IntPtr pointer)
1416
{
15-
this.pointer = pointer;
17+
Pointer = pointer;
1618
}
19+
20+
[Pure]
21+
public static bool operator ==(in StolenReference reference, NullOnly @null)
22+
=> reference.Pointer == IntPtr.Zero;
23+
[Pure]
24+
public static bool operator !=(in StolenReference reference, NullOnly @null)
25+
=> reference.Pointer != IntPtr.Zero;
26+
27+
[Pure]
28+
public override bool Equals(object obj)
29+
{
30+
if (obj is IntPtr ptr)
31+
return ptr == Pointer;
32+
33+
return false;
34+
}
35+
36+
[Pure]
37+
public override int GetHashCode() => Pointer.GetHashCode();
38+
}
39+
40+
static class StolenReferenceExtensions
41+
{
42+
[Pure]
43+
public static IntPtr DangerousGetAddressOrNull(this in StolenReference reference)
44+
=> reference.Pointer;
1745
}
1846
}

src/runtime/classmanager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ internal static void SaveRuntimeData(RuntimeDataStorage storage)
131131
}
132132
else if (Exceptions.ErrorOccurred())
133133
{
134-
throw new PythonException();
134+
throw PythonException.ThrowLastAsClrException();
135135
}
136136
}
137137
// We modified the Type object, notify it we did.

0 commit comments

Comments
 (0)