Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions src/embed_tests/TestConversionErrors.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System;
using System.Collections;
using System.Collections.Generic;

using NUnit.Framework;

using Python.Runtime;

namespace Python.EmbeddingTest
{
/// <summary>
/// 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.
/// </summary>
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));
}

/// <summary>
/// Evaluates <paramref name="expression"/> inside a Python try/except and
/// returns what Python caught. If the failure escapes Python instead, the
/// call throws and the test fails.
/// </summary>
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<string>("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);
}

/// <summary>Hands an unconvertible value to Python through each boundary.</summary>
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<UnconvertibleValue> 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;
}
}
65 changes: 38 additions & 27 deletions src/runtime/MethodBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
19 changes: 17 additions & 2 deletions src/runtime/Types/ArrayObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand Down Expand Up @@ -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;
}
}


Expand Down
14 changes: 12 additions & 2 deletions src/runtime/Types/Iterator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading