Skip to content
Open
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
33 changes: 33 additions & 0 deletions src/embed_tests/TestConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,39 @@ class TestPythonModel(TestCSharpModel):
Assert.AreEqual(shouldConvert, Converter.ToManaged(testPythonModelClass, type, out var result, setError: false));
Assert.IsFalse(Exceptions.ErrorOccurred());
}

[TestCase(true)]
[TestCase(false)]
public void RaisingIteratorFailsArrayConversion(bool setError)
{
using var _ = Py.GIL();

var module = PyModule.FromString("RaisingIteratorModule", @"
def gen():
yield 1
raise ValueError('mid-iteration failure')
");
using var generator = module.GetAttr("gen").Invoke();

// the conversion must fail rather than return a silently truncated array,
// and the raised error must only be left pending when setError is true
Assert.IsFalse(Converter.ToManaged(generator, typeof(int[]), out var result, setError));
Assert.IsNull(result);
Assert.AreEqual(setError, Exceptions.ErrorOccurred());
Exceptions.Clear();
}

[TestCase(true)]
[TestCase(false)]
public void OverflowConversionOnlyLeavesErrorWhenSettingErrors(bool setError)
{
using var _ = Py.GIL();

using var pyValue = new PyInt(300);
Assert.IsFalse(Converter.ToManaged(pyValue, typeof(byte), out var _, setError));
Assert.AreEqual(setError, Exceptions.ErrorOccurred());
Exceptions.Clear();
}
}

public interface IGetList
Expand Down
34 changes: 34 additions & 0 deletions src/embed_tests/TestPyObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,40 @@ public void InheritedMethodsAutoacquireGIL()
{
PythonEngine.Exec("from System import String\nString.Format('{0},{1}', 1, 2)");
}

[Test]
public void FailedTryAsDoesNotLeavePythonErrorSet()
{
using var _ = Py.GIL();

using var locals = new PyDict();
PythonEngine.Exec("def a_function(a, b): return a * b", null, locals);
using var pyObject = locals.GetItem("a_function");

Assert.IsFalse(pyObject.TryAs<int>(out var _));
Assert.IsFalse(Exceptions.ErrorOccurred());

Assert.IsFalse(pyObject.TryAsManagedObject(typeof(decimal), out var _));
Assert.IsFalse(Exceptions.ErrorOccurred());

// The thread state must be clean for subsequent unrelated Python calls
Assert.DoesNotThrow(() => PyModule.FromString("TryAsLeakCheck", "x = 1").Dispose());
}

[Test]
public void FailedAsManagedObjectRaisesWithConversionErrorAsCause()
{
using var _ = Py.GIL();

using var locals = new PyDict();
PythonEngine.Exec("def a_function(a, b): return a * b", null, locals);
using var pyObject = locals.GetItem("a_function");

var exception = Assert.Throws<InvalidCastException>(() => pyObject.AsManagedObject(typeof(int)));
Assert.IsNotNull(exception.InnerException);
StringAssert.Contains("int()", exception.InnerException.Message);
Assert.IsFalse(Exceptions.ErrorOccurred());
}
}

public class PyObjectTestMethods
Expand Down
4 changes: 2 additions & 2 deletions src/perf_tests/Python.PerformanceTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.*" />
<PackageReference Include="quantconnect.pythonnet" Version="2.0.62" GeneratePathProperty="true">
<PackageReference Include="quantconnect.pythonnet" Version="2.0.63" GeneratePathProperty="true">
<IncludeAssets>compile</IncludeAssets>
</PackageReference>
</ItemGroup>
Expand All @@ -25,7 +25,7 @@
</Target>

<Target Name="CopyBaseline" AfterTargets="Build">
<Copy SourceFiles="$(NuGetPackageRoot)quantconnect.pythonnet\2.0.62\lib\net10.0\Python.Runtime.dll" DestinationFolder="$(OutDir)baseline" />
<Copy SourceFiles="$(NuGetPackageRoot)quantconnect.pythonnet\2.0.63\lib\net10.0\Python.Runtime.dll" DestinationFolder="$(OutDir)baseline" />
</Target>

<Target Name="CopyNewBuild" AfterTargets="Build">
Expand Down
57 changes: 55 additions & 2 deletions src/runtime/Converter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,8 @@ internal static bool ToManagedValue(BorrowedReference value, Type obType,
catch
{
// Failed to convert using implicit conversion, must catch the error to stop program from exploding on Linux
// Raised even when setError is false: MethodBinder.Invoke surfaces a pending
// error as the binding failure reason instead of its generic no-match message
Exceptions.RaiseTypeError($"Failed to implicitly convert {type} to {obType}");
return false;
}
Expand Down Expand Up @@ -711,6 +713,8 @@ internal static bool ToManagedValue(BorrowedReference value, Type obType,
catch
{
// Failed to convert using implicit conversion, must catch the error to stop program from exploding on Linux
// Raised even when setError is false: MethodBinder.Invoke surfaces a pending
// error as the binding failure reason instead of its generic no-match message
Exceptions.RaiseTypeError($"Failed to implicitly convert {result.GetType()} to {obType}");
return false;
}
Expand Down Expand Up @@ -1371,6 +1375,11 @@ internal static bool ToPrimitive(BorrowedReference value, Type obType, out objec
string tpName = Runtime.PyObject_GetTypeName(value);
Exceptions.SetError(Exceptions.TypeError, $"'{tpName}' value cannot be converted to {obType}");
}
else
{
// a probing sub-path may have raised before jumping here
Exceptions.Clear();
}
return false;

overflow:
Expand All @@ -1379,6 +1388,10 @@ internal static bool ToPrimitive(BorrowedReference value, Type obType, out objec
{
Exceptions.SetError(Exceptions.OverflowError, "value too large to convert");
}
else
{
Exceptions.Clear();
}
return false;
}

Expand Down Expand Up @@ -1456,7 +1469,23 @@ private static bool ToArray(BorrowedReference value, Type obType, out object res
private static bool ToList(BorrowedReference value, Type obType, out object result, bool setError)
{
var elementType = obType.GetGenericArguments()[0];
var IterObject = Runtime.PyObject_GetIter(value);
result = null;

using var IterObject = Runtime.PyObject_GetIter(value);
if (IterObject.IsNull())
{
if (setError)
{
SetConversionError(value, obType);
}
else
{
// PyObject_GetIter will have set an error
Exceptions.Clear();
}
return false;
}

result = MakeList(value, IterObject, obType, elementType, setError);
return result != null;
}
Expand Down Expand Up @@ -1499,17 +1528,41 @@ private static IList MakeList(BorrowedReference value, NewReference IterObject,
Exceptions.SetError(e);
SetConversionError(value, obType);
}
else
{
// PySequence_Size may have raised (e.g. a broken __len__)
Exceptions.Clear();
}

return null;
}

while (true)
{
using var item = Runtime.PyIter_Next(IterObject.Borrow());
if (item.IsNull()) break;
if (item.IsNull())
{
if (Exceptions.ErrorOccurred())
{
// the iterator raised mid-iteration: the conversion failed,
// don't return a silently truncated list
if (!setError)
{
Exceptions.Clear();
}
return null;
}
break;
}

if (!Converter.ToManaged(item.Borrow(), elementType, out var obj, setError))
{
// some element conversion paths raise even when setError is false
// (e.g. failed implicit operators, deleted types)
if (!setError)
{
Exceptions.Clear();
}
return null;
}

Expand Down
4 changes: 2 additions & 2 deletions src/runtime/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
[assembly: InternalsVisibleTo("Python.EmbeddingTest, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")]
[assembly: InternalsVisibleTo("Python.Test, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")]

[assembly: AssemblyVersion("2.0.62")]
[assembly: AssemblyFileVersion("2.0.62")]
[assembly: AssemblyVersion("2.0.63")]
[assembly: AssemblyFileVersion("2.0.63")]
2 changes: 1 addition & 1 deletion src/runtime/Python.Runtime.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<RootNamespace>Python.Runtime</RootNamespace>
<AssemblyName>Python.Runtime</AssemblyName>
<PackageId>QuantConnect.pythonnet</PackageId>
<Version>2.0.62</Version>
<Version>2.0.63</Version>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<RepositoryUrl>https://github.com/pythonnet/pythonnet</RepositoryUrl>
Expand Down
12 changes: 10 additions & 2 deletions src/runtime/PythonTypes/PyObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ public static PyObject FromManagedObject(object ob)
/// </remarks>
public object? AsManagedObject(Type t)
{
if (!TryAsManagedObject(t, out var result))
if (!Converter.ToManaged(obj, t, out var result, setError: true))
{
throw new InvalidCastException("cannot convert object to target type",
PythonException.FetchCurrentOrNull(out _));
Expand All @@ -188,7 +188,15 @@ public static PyObject FromManagedObject(object ob)
/// </summary>
public bool TryAsManagedObject(Type t, out object? result)
{
return Converter.ToManaged(obj, t, out result, true);
if (Converter.ToManaged(obj, t, out result, setError: false))
{
return true;
}
// A failed Try-conversion must not leave a Python error pending on the
// thread: the caller only sees the boolean, so a stale error indicator
// would surface from an unrelated later call on this thread.
Exceptions.Clear();
return false;
}

/// <summary>
Expand Down
Loading