Visitar URL original
Python exception raised on conversion failure (#2741) · pythonnet/pythonnet@a7af7a8 · GitHub
Skip to content

Commit a7af7a8

Browse files
Python exception raised on conversion failure (#2741)
Converting a CLR value to Python can throw after the call has succeeded, eg if an encoder's TryEncode throws, or if calling enumerator.Current throws. Previously the exception was never converted to a Python exception making it uncatchable even instead, so Python code could not catch it, not even using except BaseException - MethodBinder.Invoke guarded only the invocation, and converted the result and out parameters after its try/catch - Iterator.tp_iternext guarded MoveNext but not Current or the conversion of the item - ArrayObject indexing converted items with no guard at all Each now reports the failure through Exceptions.SetError, as the existing catch already does for the call itself. Property and field getters already convert inside their try and are unchanged.
1 parent 8bb66fd commit a7af7a8

4 files changed

Lines changed: 171 additions & 31 deletions

File tree

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
5+
using NUnit.Framework;
6+
7+
using Python.Runtime;
8+
9+
namespace Python.EmbeddingTest
10+
{
11+
/// <summary>
12+
/// A failure while converting a CLR value for Python has to surface as a
13+
/// Python exception that Python code can catch, rather than as a CLR exception
14+
/// unwinding through the interpreter.
15+
/// </summary>
16+
public class TestConversionErrors
17+
{
18+
internal const string Failure = "conversion failed on purpose";
19+
20+
[SetUp]
21+
public void SetUp() => PyObjectConversions.RegisterEncoder(new FailingConversionEncoder());
22+
23+
[TearDown]
24+
public void TearDown() => PyObjectConversions.Reset();
25+
26+
[TestCase("host.Method()", TestName = "MethodReturn")]
27+
[TestCase("host.OutParam(None)", TestName = "OutParameterAndReturn")]
28+
[TestCase("host.OutOnly(None)", TestName = "OutParameterOnly")]
29+
[TestCase("list(host.Iterate())", TestName = "IteratorItem")]
30+
[TestCase("host.Array1[0]", TestName = "ArrayItem")]
31+
[TestCase("host.Array2[0, 0]", TestName = "MultidimensionalArrayItem")]
32+
[TestCase("host[0]", TestName = "IndexerResult")]
33+
public void ConversionFailureIsCatchableInPython(string expression)
34+
{
35+
Assert.That(CaughtInPython(expression), Does.Contain(Failure));
36+
}
37+
38+
[Test]
39+
public void ThrowingCurrentIsCatchableInPython()
40+
{
41+
Assert.That(
42+
CaughtInPython("list(host.ThrowingEnumerable())"),
43+
Does.Contain(ThrowingCurrentEnumerator.Message));
44+
}
45+
46+
/// <summary>
47+
/// Evaluates <paramref name="expression"/> inside a Python try/except and
48+
/// returns what Python caught. If the failure escapes Python instead, the
49+
/// call throws and the test fails.
50+
/// </summary>
51+
static string CaughtInPython(string expression)
52+
{
53+
using var scope = Py.CreateScope();
54+
scope.Set("host", new ConversionErrorHost());
55+
scope.Exec($@"
56+
def attempt():
57+
try:
58+
{expression}
59+
except Exception as e:
60+
return str(e)
61+
return 'nothing raised'
62+
");
63+
return scope.Eval<string>("attempt()");
64+
}
65+
}
66+
67+
public class UnconvertibleValue { }
68+
69+
class FailingConversionEncoder : IPyObjectEncoder
70+
{
71+
public bool CanEncode(Type type) => type == typeof(UnconvertibleValue);
72+
73+
public PyObject TryEncode(object value) =>
74+
throw new InvalidOperationException(TestConversionErrors.Failure);
75+
}
76+
77+
/// <summary>Hands an unconvertible value to Python through each boundary.</summary>
78+
public class ConversionErrorHost
79+
{
80+
public UnconvertibleValue Method() => new();
81+
public int OutParam(out UnconvertibleValue value) { value = new(); return 1; }
82+
public void OutOnly(out UnconvertibleValue value) => value = new();
83+
public IEnumerable<UnconvertibleValue> Iterate() { yield return new(); }
84+
public UnconvertibleValue[] Array1 = { new() };
85+
public UnconvertibleValue[,] Array2 = { { new() } };
86+
public UnconvertibleValue this[int index] => new();
87+
public IEnumerable ThrowingEnumerable() => new ThrowingCurrentEnumerable();
88+
}
89+
90+
class ThrowingCurrentEnumerable : IEnumerable
91+
{
92+
public IEnumerator GetEnumerator() => new ThrowingCurrentEnumerator();
93+
}
94+
95+
class ThrowingCurrentEnumerator : IEnumerator
96+
{
97+
public const string Message = "Current failed on purpose";
98+
bool _moved;
99+
100+
public object Current => throw new InvalidOperationException(Message);
101+
public bool MoveNext() => !_moved && (_moved = true);
102+
public void Reset() => _moved = false;
103+
}
104+
}

‎src/runtime/MethodBinder.cs‎

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -964,43 +964,54 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a
964964

965965
var returnType = binding.info.IsConstructor ? typeof(void) : ((MethodInfo)binding.info).ReturnType;
966966

967-
if (binding.outs > 0)
967+
// Converting the results can fail too - a type refused by an
968+
// IClrTypeFilter, for one - and that must become a Python exception
969+
// rather than a CLR exception unwinding through the interpreter
970+
try
968971
{
969-
ParameterInfo[] pi = binding.info.GetParameters();
970-
int c = pi.Length;
971-
var n = 0;
972-
973-
bool isVoid = returnType == typeof(void);
974-
int tupleSize = binding.outs + (isVoid ? 0 : 1);
975-
using var t = Runtime.PyTuple_New(tupleSize);
976-
if (!isVoid)
972+
if (binding.outs > 0)
977973
{
978-
using var v = Converter.ToPython(result, returnType);
979-
Runtime.PyTuple_SetItem(t.Borrow(), n, v.Steal());
980-
n++;
981-
}
982-
983-
for (var i = 0; i < c; i++)
984-
{
985-
Type pt = pi[i].ParameterType;
986-
if (pt.IsByRef)
974+
ParameterInfo[] pi = binding.info.GetParameters();
975+
int c = pi.Length;
976+
var n = 0;
977+
978+
bool isVoid = returnType == typeof(void);
979+
int tupleSize = binding.outs + (isVoid ? 0 : 1);
980+
using var t = Runtime.PyTuple_New(tupleSize);
981+
if (!isVoid)
987982
{
988-
using var v = Converter.ToPython(binding.args[i], pt.GetElementType());
983+
using var v = Converter.ToPython(result, returnType);
989984
Runtime.PyTuple_SetItem(t.Borrow(), n, v.Steal());
990985
n++;
991986
}
992-
}
993987

994-
if (binding.outs == 1 && returnType == typeof(void))
995-
{
996-
BorrowedReference item = Runtime.PyTuple_GetItem(t.Borrow(), 0);
997-
return new NewReference(item);
988+
for (var i = 0; i < c; i++)
989+
{
990+
Type pt = pi[i].ParameterType;
991+
if (pt.IsByRef)
992+
{
993+
using var v = Converter.ToPython(binding.args[i], pt.GetElementType());
994+
Runtime.PyTuple_SetItem(t.Borrow(), n, v.Steal());
995+
n++;
996+
}
997+
}
998+
999+
if (binding.outs == 1 && returnType == typeof(void))
1000+
{
1001+
BorrowedReference item = Runtime.PyTuple_GetItem(t.Borrow(), 0);
1002+
return new NewReference(item);
1003+
}
1004+
1005+
return new NewReference(t.Borrow());
9981006
}
9991007

1000-
return new NewReference(t.Borrow());
1008+
return Converter.ToPython(result, returnType);
1009+
}
1010+
catch (Exception e)
1011+
{
1012+
Exceptions.SetError(e);
1013+
return default;
10011014
}
1002-
1003-
return Converter.ToPython(result, returnType);
10041015
}
10051016
}
10061017

‎src/runtime/Types/ArrayObject.cs‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ public static NewReference mp_subscript(BorrowedReference ob, BorrowedReference
191191

192192
value = items.GetValue(index);
193193

194-
return Converter.ToPython(value, itemType);
194+
return ItemToPython(value, itemType);
195195
}
196196

197197
// Multi-dimensional arrays can be indexed a la: list[1, 2, 3].
@@ -238,7 +238,22 @@ public static NewReference mp_subscript(BorrowedReference ob, BorrowedReference
238238

239239
value = items.GetValue(indices);
240240

241-
return Converter.ToPython(value, itemType);
241+
return ItemToPython(value, itemType);
242+
}
243+
244+
// The conversion can throw - for a type refused by an IClrTypeFilter, say -
245+
// and that must become a Python exception rather than unwinding through it
246+
static NewReference ItemToPython(object? value, Type itemType)
247+
{
248+
try
249+
{
250+
return Converter.ToPython(value, itemType);
251+
}
252+
catch (Exception e)
253+
{
254+
Exceptions.SetError(e);
255+
return default;
256+
}
242257
}
243258

244259

‎src/runtime/Types/Iterator.cs‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,18 @@ public static NewReference tp_iternext(BorrowedReference ob)
4242
Exceptions.SetError(e);
4343
return default;
4444
}
45-
object item = self.iter.Current;
46-
return Converter.ToPython(item, self.elemType);
45+
// Current and the conversion can both throw - the latter for a type
46+
// refused by an IClrTypeFilter - and must not unwind through Python
47+
try
48+
{
49+
object item = self.iter.Current;
50+
return Converter.ToPython(item, self.elemType);
51+
}
52+
catch (Exception e)
53+
{
54+
Exceptions.SetError(e);
55+
return default;
56+
}
4757
}
4858

4959
public static NewReference tp_iter(BorrowedReference ob) => new (ob);

0 commit comments

Comments
 (0)