diff --git a/src/embed_tests/TestConversionErrors.cs b/src/embed_tests/TestConversionErrors.cs
new file mode 100644
index 000000000..a3ee43e7d
--- /dev/null
+++ b/src/embed_tests/TestConversionErrors.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+using NUnit.Framework;
+
+using Python.Runtime;
+
+namespace Python.EmbeddingTest
+{
+ ///
+ /// A failure while converting a CLR value for Python has to surface as a
+ /// Python exception that Python code can catch, rather than as a CLR exception
+ /// unwinding through the interpreter.
+ ///
+ public class TestConversionErrors
+ {
+ internal const string Failure = "conversion failed on purpose";
+
+ [SetUp]
+ public void SetUp() => PyObjectConversions.RegisterEncoder(new FailingConversionEncoder());
+
+ [TearDown]
+ public void TearDown() => PyObjectConversions.Reset();
+
+ [TestCase("host.Method()", TestName = "MethodReturn")]
+ [TestCase("host.OutParam(None)", TestName = "OutParameterAndReturn")]
+ [TestCase("host.OutOnly(None)", TestName = "OutParameterOnly")]
+ [TestCase("list(host.Iterate())", TestName = "IteratorItem")]
+ [TestCase("host.Array1[0]", TestName = "ArrayItem")]
+ [TestCase("host.Array2[0, 0]", TestName = "MultidimensionalArrayItem")]
+ [TestCase("host[0]", TestName = "IndexerResult")]
+ public void ConversionFailureIsCatchableInPython(string expression)
+ {
+ Assert.That(CaughtInPython(expression), Does.Contain(Failure));
+ }
+
+ [Test]
+ public void ThrowingCurrentIsCatchableInPython()
+ {
+ Assert.That(
+ CaughtInPython("list(host.ThrowingEnumerable())"),
+ Does.Contain(ThrowingCurrentEnumerator.Message));
+ }
+
+ ///
+ /// Evaluates inside a Python try/except and
+ /// returns what Python caught. If the failure escapes Python instead, the
+ /// call throws and the test fails.
+ ///
+ static string CaughtInPython(string expression)
+ {
+ using var scope = Py.CreateScope();
+ scope.Set("host", new ConversionErrorHost());
+ scope.Exec($@"
+def attempt():
+ try:
+ {expression}
+ except Exception as e:
+ return str(e)
+ return 'nothing raised'
+");
+ return scope.Eval("attempt()");
+ }
+ }
+
+ public class UnconvertibleValue { }
+
+ class FailingConversionEncoder : IPyObjectEncoder
+ {
+ public bool CanEncode(Type type) => type == typeof(UnconvertibleValue);
+
+ public PyObject TryEncode(object value) =>
+ throw new InvalidOperationException(TestConversionErrors.Failure);
+ }
+
+ /// Hands an unconvertible value to Python through each boundary.
+ public class ConversionErrorHost
+ {
+ public UnconvertibleValue Method() => new();
+ public int OutParam(out UnconvertibleValue value) { value = new(); return 1; }
+ public void OutOnly(out UnconvertibleValue value) => value = new();
+ public IEnumerable Iterate() { yield return new(); }
+ public UnconvertibleValue[] Array1 = { new() };
+ public UnconvertibleValue[,] Array2 = { { new() } };
+ public UnconvertibleValue this[int index] => new();
+ public IEnumerable ThrowingEnumerable() => new ThrowingCurrentEnumerable();
+ }
+
+ class ThrowingCurrentEnumerable : IEnumerable
+ {
+ public IEnumerator GetEnumerator() => new ThrowingCurrentEnumerator();
+ }
+
+ class ThrowingCurrentEnumerator : IEnumerator
+ {
+ public const string Message = "Current failed on purpose";
+ bool _moved;
+
+ public object Current => throw new InvalidOperationException(Message);
+ public bool MoveNext() => !_moved && (_moved = true);
+ public void Reset() => _moved = false;
+ }
+}
diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs
index 383d0bf49..9beb9a056 100644
--- a/src/runtime/MethodBinder.cs
+++ b/src/runtime/MethodBinder.cs
@@ -964,43 +964,54 @@ internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference a
var returnType = binding.info.IsConstructor ? typeof(void) : ((MethodInfo)binding.info).ReturnType;
- if (binding.outs > 0)
+ // Converting the results can fail too - a type refused by an
+ // IClrTypeFilter, for one - and that must become a Python exception
+ // rather than a CLR exception unwinding through the interpreter
+ try
{
- ParameterInfo[] pi = binding.info.GetParameters();
- int c = pi.Length;
- var n = 0;
-
- bool isVoid = returnType == typeof(void);
- int tupleSize = binding.outs + (isVoid ? 0 : 1);
- using var t = Runtime.PyTuple_New(tupleSize);
- if (!isVoid)
+ if (binding.outs > 0)
{
- using var v = Converter.ToPython(result, returnType);
- Runtime.PyTuple_SetItem(t.Borrow(), n, v.Steal());
- n++;
- }
-
- for (var i = 0; i < c; i++)
- {
- Type pt = pi[i].ParameterType;
- if (pt.IsByRef)
+ ParameterInfo[] pi = binding.info.GetParameters();
+ int c = pi.Length;
+ var n = 0;
+
+ bool isVoid = returnType == typeof(void);
+ int tupleSize = binding.outs + (isVoid ? 0 : 1);
+ using var t = Runtime.PyTuple_New(tupleSize);
+ if (!isVoid)
{
- using var v = Converter.ToPython(binding.args[i], pt.GetElementType());
+ using var v = Converter.ToPython(result, returnType);
Runtime.PyTuple_SetItem(t.Borrow(), n, v.Steal());
n++;
}
- }
- if (binding.outs == 1 && returnType == typeof(void))
- {
- BorrowedReference item = Runtime.PyTuple_GetItem(t.Borrow(), 0);
- return new NewReference(item);
+ for (var i = 0; i < c; i++)
+ {
+ Type pt = pi[i].ParameterType;
+ if (pt.IsByRef)
+ {
+ using var v = Converter.ToPython(binding.args[i], pt.GetElementType());
+ Runtime.PyTuple_SetItem(t.Borrow(), n, v.Steal());
+ n++;
+ }
+ }
+
+ if (binding.outs == 1 && returnType == typeof(void))
+ {
+ BorrowedReference item = Runtime.PyTuple_GetItem(t.Borrow(), 0);
+ return new NewReference(item);
+ }
+
+ return new NewReference(t.Borrow());
}
- return new NewReference(t.Borrow());
+ return Converter.ToPython(result, returnType);
+ }
+ catch (Exception e)
+ {
+ Exceptions.SetError(e);
+ return default;
}
-
- return Converter.ToPython(result, returnType);
}
}
diff --git a/src/runtime/Types/ArrayObject.cs b/src/runtime/Types/ArrayObject.cs
index f220d53fb..54b6208ea 100644
--- a/src/runtime/Types/ArrayObject.cs
+++ b/src/runtime/Types/ArrayObject.cs
@@ -191,7 +191,7 @@ public static NewReference mp_subscript(BorrowedReference ob, BorrowedReference
value = items.GetValue(index);
- return Converter.ToPython(value, itemType);
+ return ItemToPython(value, itemType);
}
// Multi-dimensional arrays can be indexed a la: list[1, 2, 3].
@@ -238,7 +238,22 @@ public static NewReference mp_subscript(BorrowedReference ob, BorrowedReference
value = items.GetValue(indices);
- return Converter.ToPython(value, itemType);
+ return ItemToPython(value, itemType);
+ }
+
+ // The conversion can throw - for a type refused by an IClrTypeFilter, say -
+ // and that must become a Python exception rather than unwinding through it
+ static NewReference ItemToPython(object? value, Type itemType)
+ {
+ try
+ {
+ return Converter.ToPython(value, itemType);
+ }
+ catch (Exception e)
+ {
+ Exceptions.SetError(e);
+ return default;
+ }
}
diff --git a/src/runtime/Types/Iterator.cs b/src/runtime/Types/Iterator.cs
index 49145d2c3..e009ca841 100644
--- a/src/runtime/Types/Iterator.cs
+++ b/src/runtime/Types/Iterator.cs
@@ -42,8 +42,18 @@ public static NewReference tp_iternext(BorrowedReference ob)
Exceptions.SetError(e);
return default;
}
- object item = self.iter.Current;
- return Converter.ToPython(item, self.elemType);
+ // Current and the conversion can both throw - the latter for a type
+ // refused by an IClrTypeFilter - and must not unwind through Python
+ try
+ {
+ object item = self.iter.Current;
+ return Converter.ToPython(item, self.elemType);
+ }
+ catch (Exception e)
+ {
+ Exceptions.SetError(e);
+ return default;
+ }
}
public static NewReference tp_iter(BorrowedReference ob) => new (ob);