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
4 changes: 3 additions & 1 deletion src/embed_tests/TestCallbacks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ public void TestNoOverloadException() {
var error = Assert.Throws<PythonException>(() => callWith42(pyFunc));
Assert.AreEqual("TypeError", error.Type.Name);
string expectedArgTypes = "(<class 'list'>)";
StringAssert.EndsWith(expectedArgTypes, error.Message);
// The message includes the offending argument types, followed by the
// candidate overload signatures, so assert containment rather than suffix.
StringAssert.Contains(expectedArgTypes, error.Message);
error.Traceback.Dispose();
}
}
Expand Down
179 changes: 179 additions & 0 deletions src/embed_tests/TestFloatToIntConversion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
using NUnit.Framework;
using Python.Runtime;

namespace Python.EmbeddingTest
{
/// <summary>
/// Passing a Python float where a .NET integer is expected.
///
/// A float that holds an integral value (e.g. 5.0) is accepted and converted;
/// a non-integral float (e.g. 5.5) is rejected rather than silently truncated.
/// This must hold regardless of whether the target method/constructor has a
/// single signature or several overloads (the latter reproduces Lean's
/// RangeConsolidator(period), which has two int-first constructor overloads).
/// </summary>
public class TestFloatToIntConversion
{
private PyModule _module;

private const string TestModule = @"
from clr import AddReference
AddReference(""Python.EmbeddingTest"")
from Python.EmbeddingTest import IntTaker, OverloadedIntTaker

def single_ctor(value):
return IntTaker(value).Value

def single_method(value):
return IntTaker(0).Echo(value)

def overloaded_ctor(value):
return OverloadedIntTaker(value).Value

def overloaded_method(value):
return OverloadedIntTaker(0).Echo(value)

def single_named(value):
return IntTaker(0).ComputeValue(value)

def overloaded_named(value):
return OverloadedIntTaker(0).ComputeRange(value)

def single_params(value):
return IntTaker(0).ComputeScaled(value)
";

[OneTimeSetUp]
public void Setup()
{
PythonEngine.Initialize();
_module = PyModule.FromString("float_to_int_module", TestModule);
}

[OneTimeTearDown]
public void TearDown()
{
_module.Dispose();
PythonEngine.Shutdown();
}

private int Call(string func, double value)
{
using (Py.GIL())
using (var arg = value.ToPython())
{
return _module.InvokeMethod(func, arg).As<int>();
}
}

// An integral-valued float is accepted and converted, single or overloaded.
[TestCase("single_ctor")]
[TestCase("single_method")]
[TestCase("overloaded_ctor")]
[TestCase("overloaded_method")]
public void IntegralFloat_IsAccepted(string func)
{
Assert.AreEqual(5, Call(func, 5.0));
}

// A non-integral float is rejected (no silent truncation) for every target.
[TestCase("single_ctor")]
[TestCase("single_method")]
[TestCase("overloaded_ctor")]
[TestCase("overloaded_method")]
public void NonIntegralFloat_IsRejected(string func)
{
var ex = Assert.Throws<PythonException>(() => Call(func, 5.5));
Assert.AreEqual("TypeError", ex.Type.Name);
}

// When no overload matches, the error should hint the expected signature(s).
[Test]
public void ErrorMessage_SingleOverload_ShowsExpectedSignature()
{
var ex = Assert.Throws<PythonException>(() => Call("single_ctor", 5.5));
StringAssert.Contains("The expected signature is:", ex.Message);
StringAssert.Contains("Int32 value", ex.Message);
}

[Test]
public void ErrorMessage_MultipleOverloads_ListsCandidates()
{
var ex = Assert.Throws<PythonException>(() => Call("overloaded_ctor", 5.5));
StringAssert.Contains("The following overloads are available:", ex.Message);
// The int overload is surfaced, hinting an integer was expected.
StringAssert.Contains("Int32 range", ex.Message);
}

// The hinted signatures use the snake_case name Python callers use, not the
// original C# name.
[Test]
public void ErrorMessage_SingleOverload_UsesSnakeCaseMethodName()
{
var ex = Assert.Throws<PythonException>(() => Call("single_named", 5.5));
StringAssert.Contains("compute_value(", ex.Message);
StringAssert.DoesNotContain("ComputeValue", ex.Message);
}

[Test]
public void ErrorMessage_MultipleOverloads_UseSnakeCaseMethodName()
{
var ex = Assert.Throws<PythonException>(() => Call("overloaded_named", 5.5));
StringAssert.Contains("compute_range(", ex.Message);
StringAssert.DoesNotContain("ComputeRange", ex.Message);
}

// The hinted signatures also snake_case the parameter names.
[Test]
public void ErrorMessage_SignatureParameters_AreSnakeCase()
{
var ex = Assert.Throws<PythonException>(() => Call("single_params", 5.5));
StringAssert.Contains("scale_factor", ex.Message);
StringAssert.DoesNotContain("scaleFactor", ex.Message);
}
}

public class IntTaker
{
public int Value { get; }

public IntTaker(int value)
{
Value = value;
}

public int Echo(int value) => value;

public int ComputeValue(int value) => value;

public int ComputeScaled(int scaleFactor) => scaleFactor;
}

/// <summary>
/// Mimics Lean's RangeConsolidator: two overloads that both take an int first
/// parameter, differing only in the (defaulted) later parameters. This forces the
/// binder through its overload-disambiguation path.
/// </summary>
public class OverloadedIntTaker
{
public int Value { get; }

public OverloadedIntTaker(int range, System.Func<int, int> selector = null)
{
Value = range;
}

public OverloadedIntTaker(int range, PyObject selector, PyObject volumeSelector = null)
{
Value = range;
}

public int Echo(int value, System.Func<int, int> selector = null) => value;

public int Echo(int value, PyObject selector, PyObject other = null) => value;

public int ComputeRange(int value, System.Func<int, int> selector = null) => value;

public int ComputeRange(int value, PyObject selector, PyObject other = null) => value;
}
}
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.56" GeneratePathProperty="true">
<PackageReference Include="quantconnect.pythonnet" Version="2.0.57" 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.56\lib\net10.0\Python.Runtime.dll" DestinationFolder="$(OutDir)baseline" />
<Copy SourceFiles="$(NuGetPackageRoot)quantconnect.pythonnet\2.0.57\lib\net10.0\Python.Runtime.dll" DestinationFolder="$(OutDir)baseline" />
</Target>

<Target Name="CopyNewBuild" AfterTargets="Build">
Expand Down
14 changes: 14 additions & 0 deletions src/runtime/Converter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,20 @@ internal static bool ToPrimitive(BorrowedReference value, Type obType, out objec

TypeCode tc = Type.GetTypeCode(obType);

// A Python float with a fractional part must not be silently truncated
// into an integer parameter. Integral-valued floats (e.g. 5.0) are still
// accepted. This keeps single- and multi-overload binding consistent:
// MethodBinder only treats integral floats as candidates for integer
// parameters, and this guard enforces the same rule at conversion time.
if (obType.IsInteger() && Runtime.PyFloat_Check(value))
{
double dbl = Runtime.PyFloat_AsDouble(value);
if (double.IsNaN(dbl) || double.IsInfinity(dbl) || Math.Truncate(dbl) != dbl)
{
goto type_error;
}
}

switch (tc)
{
case TypeCode.Object:
Expand Down
Loading
Loading