Skip to content

Commit 1c136b6

Browse files
jhonabreulclaude
andauthored
Fix IndexOutOfRangeException on empty **kwargs call to overloaded method (#133)
* Fix IndexOutOfRangeException on empty **kwargs call to overloaded method (#132) Calling an overloaded method with an empty kwargs mapping (e.g. obj.Method(arg, **{}), common when forwarding *args/**kwargs from a wrapper) crashed with an unhandled IndexOutOfRangeException in MethodBinder.CheckMethodArgumentsMatch. Since 10e721b (PR #83), the parameter names array is only populated when there are named arguments, but the kwargs code paths only checked the kwargs dictionary for null, so a non-null empty dict indexed into an empty names array. Treat an empty kwargs dict as no keyword arguments, matching Python semantics where f(x, **{}) is equivalent to f(x). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Update version to 2.0.60 Bump package <Version>, AssemblyVersion/AssemblyFileVersion and the perf-test baseline reference to 2.0.60. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 66dc277 commit 1c136b6

5 files changed

Lines changed: 58 additions & 9 deletions

File tree

src/embed_tests/TestMethodBinder.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,23 @@ public string ImplicitConversionSameArgumentCount2(string symbol, decimal quanti
814814

815815
// ----
816816

817+
public string GetValue(string name)
818+
{
819+
return "GetValue(name)";
820+
}
821+
822+
public string GetValue(string name, string defaultValue)
823+
{
824+
return "GetValue(name, defaultValue)";
825+
}
826+
827+
public int GetValue(string name, int defaultValue)
828+
{
829+
return defaultValue;
830+
}
831+
832+
// ----
833+
817834
public string VariableArgumentsMethod(params CSharpModel[] paramsParams)
818835
{
819836
return "VariableArgumentsMethod(CSharpModel[])";
@@ -895,6 +912,35 @@ def call_method(instance):
895912
Assert.AreEqual(expectedResult, result);
896913
}
897914

915+
[TestCase("GetValue('name', **{})", "GetValue(name)")]
916+
[TestCase("GetValue('name', 'default-value', **{})", "GetValue(name, defaultValue)")]
917+
[TestCase("GetValue('name', defaultValue='default-value', **{})", "GetValue(name, defaultValue)")]
918+
[TestCase("GetValue('name', **{'defaultValue': 'default-value'})", "GetValue(name, defaultValue)")]
919+
[TestCase("Method1('abc', **{})", "Method1 Overload 1")]
920+
public void BindsOverloadedMethodCalledWithEmptyOrUnpackedKwargs(string methodCallCode, string expectedResult)
921+
{
922+
using var _ = Py.GIL();
923+
924+
dynamic module = PyModule.FromString("BindsOverloadedMethodCalledWithEmptyOrUnpackedKwargs", @$"
925+
def call_method(instance):
926+
return instance.{methodCallCode}
927+
928+
def call_method_forwarding_args_and_kwargs(instance):
929+
# Common decorator/monkeypatch idiom: forward *args and **kwargs,
930+
# with kwargs being an empty dict when no keyword arguments are passed
931+
def wrapper(name, *args, **kwargs):
932+
return instance.GetValue(name, *args, **kwargs)
933+
return wrapper('name')
934+
");
935+
936+
var instance = new OverloadsTestClass();
937+
var result = module.call_method(instance).As<string>();
938+
Assert.AreEqual(expectedResult, result);
939+
940+
var forwardedResult = module.call_method_forwarding_args_and_kwargs(instance).As<string>();
941+
Assert.AreEqual("GetValue(name)", forwardedResult);
942+
}
943+
898944
public class CSharpClass
899945
{
900946
public string CalledMethodMessage { get; private set; }

src/perf_tests/Python.PerformanceTests.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1414
</PackageReference>
1515
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.*" />
16-
<PackageReference Include="quantconnect.pythonnet" Version="2.0.59" GeneratePathProperty="true">
16+
<PackageReference Include="quantconnect.pythonnet" Version="2.0.60" GeneratePathProperty="true">
1717
<IncludeAssets>compile</IncludeAssets>
1818
</PackageReference>
1919
</ItemGroup>
@@ -25,7 +25,7 @@
2525
</Target>
2626

2727
<Target Name="CopyBaseline" AfterTargets="Build">
28-
<Copy SourceFiles="$(NuGetPackageRoot)quantconnect.pythonnet\2.0.59\lib\net10.0\Python.Runtime.dll" DestinationFolder="$(OutDir)baseline" />
28+
<Copy SourceFiles="$(NuGetPackageRoot)quantconnect.pythonnet\2.0.60\lib\net10.0\Python.Runtime.dll" DestinationFolder="$(OutDir)baseline" />
2929
</Target>
3030

3131
<Target Name="CopyNewBuild" AfterTargets="Build">

src/runtime/MethodBinder.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -475,11 +475,14 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe
475475

476476
internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase info)
477477
{
478-
// If we have KWArgs create dictionary and collect them
478+
// If we have KWArgs create dictionary and collect them.
479+
// An empty kwargs dict (e.g. calling with **{}) is equivalent to no kwargs at all,
480+
// so we only create the dictionary if there are actual keyword arguments,
481+
// else the binding code below would try to index into empty parameter name arrays.
479482
Dictionary<string, PyObject> kwArgDict = null;
480-
if (kw != null)
483+
var pyKwArgsCount = kw == null ? 0 : (int)Runtime.PyDict_Size(kw);
484+
if (pyKwArgsCount > 0)
481485
{
482-
var pyKwArgsCount = (int)Runtime.PyDict_Size(kw);
483486
kwArgDict = new Dictionary<string, PyObject>(pyKwArgsCount);
484487
using var keylist = Runtime.PyDict_Keys(kw);
485488
using var valueList = Runtime.PyDict_Values(kw);
@@ -490,7 +493,7 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe
490493
kwArgDict[keyStr!] = new PyObject(value);
491494
}
492495
}
493-
var hasNamedArgs = kwArgDict != null && kwArgDict.Count > 0;
496+
var hasNamedArgs = kwArgDict != null;
494497

495498
// Fetch our methods we are going to attempt to match and bind too.
496499
var methods = info == null ? GetMethods()

src/runtime/Properties/AssemblyInfo.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
[assembly: InternalsVisibleTo("Python.EmbeddingTest, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")]
55
[assembly: InternalsVisibleTo("Python.Test, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")]
66

7-
[assembly: AssemblyVersion("2.0.59")]
8-
[assembly: AssemblyFileVersion("2.0.59")]
7+
[assembly: AssemblyVersion("2.0.60")]
8+
[assembly: AssemblyFileVersion("2.0.60")]

src/runtime/Python.Runtime.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<RootNamespace>Python.Runtime</RootNamespace>
66
<AssemblyName>Python.Runtime</AssemblyName>
77
<PackageId>QuantConnect.pythonnet</PackageId>
8-
<Version>2.0.59</Version>
8+
<Version>2.0.60</Version>
99
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
1010
<PackageLicenseFile>LICENSE</PackageLicenseFile>
1111
<RepositoryUrl>https://github.com/pythonnet/pythonnet</RepositoryUrl>

0 commit comments

Comments
 (0)