From 2fe63b5d188b57a94bab86ac3c57f4bb4561c801 Mon Sep 17 00:00:00 2001 From: Hamish McKenzie Date: Thu, 24 Sep 2026 11:02:52 +1000 Subject: [PATCH] Python exception raised on conversion failure 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. --- src/embed_tests/TestConversionErrors.cs | 104 ++++++++++++++++++++++++ src/runtime/MethodBinder.cs | 65 +++++++++------ src/runtime/Types/ArrayObject.cs | 19 ++++- src/runtime/Types/Iterator.cs | 14 +++- 4 files changed, 171 insertions(+), 31 deletions(-) create mode 100644 src/embed_tests/TestConversionErrors.cs 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);