From 0d6f9afb5cabb1e01994f09f69f888b5dee269fb Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 9 Jul 2026 14:06:39 -0400 Subject: [PATCH 1/2] 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 --- src/embed_tests/TestMethodBinder.cs | 46 +++++++++++++++++++++++++++++ src/runtime/MethodBinder.cs | 11 ++++--- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/embed_tests/TestMethodBinder.cs b/src/embed_tests/TestMethodBinder.cs index 49b982d08..8e41a22de 100644 --- a/src/embed_tests/TestMethodBinder.cs +++ b/src/embed_tests/TestMethodBinder.cs @@ -814,6 +814,23 @@ public string ImplicitConversionSameArgumentCount2(string symbol, decimal quanti // ---- + public string GetValue(string name) + { + return "GetValue(name)"; + } + + public string GetValue(string name, string defaultValue) + { + return "GetValue(name, defaultValue)"; + } + + public int GetValue(string name, int defaultValue) + { + return defaultValue; + } + + // ---- + public string VariableArgumentsMethod(params CSharpModel[] paramsParams) { return "VariableArgumentsMethod(CSharpModel[])"; @@ -895,6 +912,35 @@ def call_method(instance): Assert.AreEqual(expectedResult, result); } + [TestCase("GetValue('name', **{})", "GetValue(name)")] + [TestCase("GetValue('name', 'default-value', **{})", "GetValue(name, defaultValue)")] + [TestCase("GetValue('name', defaultValue='default-value', **{})", "GetValue(name, defaultValue)")] + [TestCase("GetValue('name', **{'defaultValue': 'default-value'})", "GetValue(name, defaultValue)")] + [TestCase("Method1('abc', **{})", "Method1 Overload 1")] + public void BindsOverloadedMethodCalledWithEmptyOrUnpackedKwargs(string methodCallCode, string expectedResult) + { + using var _ = Py.GIL(); + + dynamic module = PyModule.FromString("BindsOverloadedMethodCalledWithEmptyOrUnpackedKwargs", @$" +def call_method(instance): + return instance.{methodCallCode} + +def call_method_forwarding_args_and_kwargs(instance): + # Common decorator/monkeypatch idiom: forward *args and **kwargs, + # with kwargs being an empty dict when no keyword arguments are passed + def wrapper(name, *args, **kwargs): + return instance.GetValue(name, *args, **kwargs) + return wrapper('name') +"); + + var instance = new OverloadsTestClass(); + var result = module.call_method(instance).As(); + Assert.AreEqual(expectedResult, result); + + var forwardedResult = module.call_method_forwarding_args_and_kwargs(instance).As(); + Assert.AreEqual("GetValue(name)", forwardedResult); + } + public class CSharpClass { public string CalledMethodMessage { get; private set; } diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index ec9172110..c27538985 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -475,11 +475,14 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase info) { - // If we have KWArgs create dictionary and collect them + // If we have KWArgs create dictionary and collect them. + // An empty kwargs dict (e.g. calling with **{}) is equivalent to no kwargs at all, + // so we only create the dictionary if there are actual keyword arguments, + // else the binding code below would try to index into empty parameter name arrays. Dictionary kwArgDict = null; - if (kw != null) + var pyKwArgsCount = kw == null ? 0 : (int)Runtime.PyDict_Size(kw); + if (pyKwArgsCount > 0) { - var pyKwArgsCount = (int)Runtime.PyDict_Size(kw); kwArgDict = new Dictionary(pyKwArgsCount); using var keylist = Runtime.PyDict_Keys(kw); using var valueList = Runtime.PyDict_Values(kw); @@ -490,7 +493,7 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe kwArgDict[keyStr!] = new PyObject(value); } } - var hasNamedArgs = kwArgDict != null && kwArgDict.Count > 0; + var hasNamedArgs = kwArgDict != null; // Fetch our methods we are going to attempt to match and bind too. var methods = info == null ? GetMethods() From 263aea904f26ebd267782dd73b38f2d085fe8973 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 9 Jul 2026 14:47:21 -0400 Subject: [PATCH 2/2] Update version to 2.0.60 Bump package , AssemblyVersion/AssemblyFileVersion and the perf-test baseline reference to 2.0.60. Co-Authored-By: Claude Fable 5 --- src/perf_tests/Python.PerformanceTests.csproj | 4 ++-- src/runtime/Properties/AssemblyInfo.cs | 4 ++-- src/runtime/Python.Runtime.csproj | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/perf_tests/Python.PerformanceTests.csproj b/src/perf_tests/Python.PerformanceTests.csproj index aa0cd6b1b..feb559f1e 100644 --- a/src/perf_tests/Python.PerformanceTests.csproj +++ b/src/perf_tests/Python.PerformanceTests.csproj @@ -13,7 +13,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + compile @@ -25,7 +25,7 @@ - + diff --git a/src/runtime/Properties/AssemblyInfo.cs b/src/runtime/Properties/AssemblyInfo.cs index 87df116e7..eb24839a9 100644 --- a/src/runtime/Properties/AssemblyInfo.cs +++ b/src/runtime/Properties/AssemblyInfo.cs @@ -4,5 +4,5 @@ [assembly: InternalsVisibleTo("Python.EmbeddingTest, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")] [assembly: InternalsVisibleTo("Python.Test, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")] -[assembly: AssemblyVersion("2.0.59")] -[assembly: AssemblyFileVersion("2.0.59")] +[assembly: AssemblyVersion("2.0.60")] +[assembly: AssemblyFileVersion("2.0.60")] diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index f05e71081..931d119d1 100644 --- a/src/runtime/Python.Runtime.csproj +++ b/src/runtime/Python.Runtime.csproj @@ -5,7 +5,7 @@ Python.Runtime Python.Runtime QuantConnect.pythonnet - 2.0.59 + 2.0.60 false LICENSE https://github.com/pythonnet/pythonnet