From 8600d8d9f68afa14fc7bd720b96dcea01c166564 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 23 May 2024 17:37:44 -0400 Subject: [PATCH 1/4] Fix: allow calling C# class constructor with snake-cased arguments --- src/embed_tests/TestMethodBinder.cs | 24 ++++++++++++++++++++++++ src/runtime/ClassManager.cs | 5 +++++ 2 files changed, 29 insertions(+) diff --git a/src/embed_tests/TestMethodBinder.cs b/src/embed_tests/TestMethodBinder.cs index e377b5f83..0cb96cda2 100644 --- a/src/embed_tests/TestMethodBinder.cs +++ b/src/embed_tests/TestMethodBinder.cs @@ -898,6 +898,24 @@ def call_method(instance): Assert.IsFalse(Exceptions.ErrorOccurred()); } + [Test] + public void BindsConstructorToSnakeCasedArgumentsVersion() + { + using var _ = Py.GIL(); + + var module = PyModule.FromString("CallsCorrectOverloadWithoutErrors", @" +from clr import AddReference +AddReference(""System"") +from Python.EmbeddingTest import * + +def create_instance(): + return TestMethodBinder.CSharpModel(some_argument=1, another_argument=""another argument value"") +"); + var exception = Assert.Throws(() => module.GetAttr("create_instance").Invoke()); + var sourceException = exception.InnerException; + Assert.IsInstanceOf(sourceException); + Assert.AreEqual("Constructor with arguments", sourceException.Message); + } // Used to test that we match this function with Py DateTime & Date Objects public static int GetMonth(DateTime test) @@ -918,6 +936,12 @@ public CSharpModel() new TestImplicitConversion() }; } + + public CSharpModel(int someArgument, string anotherArgument = "another argument") + { + throw new NotImplementedException("Constructor with arguments"); + } + public void TestList(List conversions) { if (!conversions.Any()) diff --git a/src/runtime/ClassManager.cs b/src/runtime/ClassManager.cs index e5d31f4f1..9d92671d7 100644 --- a/src/runtime/ClassManager.cs +++ b/src/runtime/ClassManager.cs @@ -552,6 +552,11 @@ void AddMember(string name, string snakeCasedName, bool isStaticReadonlyCallable methodList = methods[name] = new (); } methodList.Add(ctor, true); + // Same constructor, but with snake-cased arguments + if (ctor.GetParameters().Any(pi => pi.Name.ToSnakeCase() != pi.Name)) + { + methodList.Add(ctor, false); + } continue; case MemberTypes.Property: From 8137198b1364b63bbbbb9250267011bcde47109b Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 23 May 2024 17:39:03 -0400 Subject: [PATCH 2/4] Bump version to 2.0.38 --- 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 c0990cf9b..2ef942f0d 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 0c15263c9..3b88a6eb5 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.37")] -[assembly: AssemblyFileVersion("2.0.37")] +[assembly: AssemblyVersion("2.0.38")] +[assembly: AssemblyFileVersion("2.0.38")] diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index 5b3276dbe..7c25c9219 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.37 + 2.0.38 false LICENSE https://github.com/pythonnet/pythonnet From c69fb42a33f7b0067eb63efe9bd798bee163b176 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 24 May 2024 09:25:01 -0400 Subject: [PATCH 3/4] Extend unit tests --- src/embed_tests/TestMethodBinder.cs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/embed_tests/TestMethodBinder.cs b/src/embed_tests/TestMethodBinder.cs index 0cb96cda2..e0da59a7a 100644 --- a/src/embed_tests/TestMethodBinder.cs +++ b/src/embed_tests/TestMethodBinder.cs @@ -899,22 +899,30 @@ def call_method(instance): } [Test] - public void BindsConstructorToSnakeCasedArgumentsVersion() + public void BindsConstructorToSnakeCasedArgumentsVersion([Values] bool useCamelCase, [Values] bool passOptionalArgument) { using var _ = Py.GIL(); - var module = PyModule.FromString("CallsCorrectOverloadWithoutErrors", @" + var argument1Name = useCamelCase ? "someArgument" : "some_argument"; + var argument2Name = useCamelCase ? "anotherArgument" : "another_argument"; + var argument2Code = passOptionalArgument ? $", {argument2Name}=\"another argument value\"" : ""; + + var module = PyModule.FromString("CallsCorrectOverloadWithoutErrors", @$" from clr import AddReference AddReference(""System"") from Python.EmbeddingTest import * def create_instance(): - return TestMethodBinder.CSharpModel(some_argument=1, another_argument=""another argument value"") + return TestMethodBinder.CSharpModel({argument1Name}=1{argument2Code}) "); var exception = Assert.Throws(() => module.GetAttr("create_instance").Invoke()); var sourceException = exception.InnerException; Assert.IsInstanceOf(sourceException); - Assert.AreEqual("Constructor with arguments", sourceException.Message); + + var expectedMessage = passOptionalArgument + ? "Constructor with arguments: someArgument=1. anotherArgument=\"another argument value\"" + : "Constructor with arguments: someArgument=1. anotherArgument=\"another argument default value\""; + Assert.AreEqual(expectedMessage, sourceException.Message); } // Used to test that we match this function with Py DateTime & Date Objects @@ -937,9 +945,9 @@ public CSharpModel() }; } - public CSharpModel(int someArgument, string anotherArgument = "another argument") + public CSharpModel(int someArgument, string anotherArgument = "another argument default value") { - throw new NotImplementedException("Constructor with arguments"); + throw new NotImplementedException($"Constructor with arguments: someArgument={someArgument}. anotherArgument=\"{anotherArgument}\""); } public void TestList(List conversions) From c799b7e698f56235b080fd8317b436ad2ddb70be Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 24 May 2024 12:10:18 -0400 Subject: [PATCH 4/4] Fix: PyObject array overloads precedence --- src/embed_tests/TestMethodBinder.cs | 73 ++++++++++++++++++++++++++++- src/runtime/ClassManager.cs | 2 +- src/runtime/MethodBinder.cs | 20 ++++---- 3 files changed, 83 insertions(+), 12 deletions(-) diff --git a/src/embed_tests/TestMethodBinder.cs b/src/embed_tests/TestMethodBinder.cs index e0da59a7a..355a96c3f 100644 --- a/src/embed_tests/TestMethodBinder.cs +++ b/src/embed_tests/TestMethodBinder.cs @@ -805,6 +805,34 @@ public string ImplicitConversionSameArgumentCount2(string symbol, decimal quanti { return "ImplicitConversionSameArgumentCount2 2"; } + + // ---- + + public string VariableArgumentsMethod(params CSharpModel[] paramsParams) + { + return "VariableArgumentsMethod(CSharpModel[])"; + } + + public string VariableArgumentsMethod(params PyObject[] paramsParams) + { + return "VariableArgumentsMethod(PyObject[])"; + } + + public string ConstructorMessage { get; set; } + + public OverloadsTestClass(params CSharpModel[] paramsParams) + { + ConstructorMessage = "OverloadsTestClass(CSharpModel[])"; + } + + public OverloadsTestClass(params PyObject[] paramsParams) + { + ConstructorMessage = "OverloadsTestClass(PyObject[])"; + } + + public OverloadsTestClass() + { + } } [TestCase("Method1('abc', namedArg1=10, namedArg2=321)", "Method1 Overload 1")] @@ -907,7 +935,7 @@ public void BindsConstructorToSnakeCasedArgumentsVersion([Values] bool useCamelC var argument2Name = useCamelCase ? "anotherArgument" : "another_argument"; var argument2Code = passOptionalArgument ? $", {argument2Name}=\"another argument value\"" : ""; - var module = PyModule.FromString("CallsCorrectOverloadWithoutErrors", @$" + var module = PyModule.FromString("BindsConstructorToSnakeCasedArgumentsVersion", @$" from clr import AddReference AddReference(""System"") from Python.EmbeddingTest import * @@ -925,6 +953,49 @@ def create_instance(): Assert.AreEqual(expectedMessage, sourceException.Message); } + [Test] + public void PyObjectArrayHasPrecedenceOverOtherTypeArrays() + { + using var _ = Py.GIL(); + + var module = PyModule.FromString("PyObjectArrayHasPrecedenceOverOtherTypeArrays", @$" +from clr import AddReference +AddReference(""System"") +from Python.EmbeddingTest import * + +class PythonModel(TestMethodBinder.CSharpModel): + pass + +def call_method(): + return TestMethodBinder.OverloadsTestClass().VariableArgumentsMethod(PythonModel(), PythonModel()) +"); + + var result = module.GetAttr("call_method").Invoke().As(); + Assert.AreEqual("VariableArgumentsMethod(PyObject[])", result); + } + + [Test] + public void PyObjectArrayHasPrecedenceOverOtherTypeArraysInConstructors() + { + using var _ = Py.GIL(); + + var module = PyModule.FromString("PyObjectArrayHasPrecedenceOverOtherTypeArrays", @$" +from clr import AddReference +AddReference(""System"") +from Python.EmbeddingTest import * + +class PythonModel(TestMethodBinder.CSharpModel): + pass + +def get_instance(): + return TestMethodBinder.OverloadsTestClass(PythonModel(), PythonModel()) +"); + + var instance = module.GetAttr("get_instance").Invoke(); + Assert.AreEqual("OverloadsTestClass(PyObject[])", instance.GetAttr("ConstructorMessage").As()); + } + + // Used to test that we match this function with Py DateTime & Date Objects public static int GetMonth(DateTime test) { diff --git a/src/runtime/ClassManager.cs b/src/runtime/ClassManager.cs index 9d92671d7..58f80ce30 100644 --- a/src/runtime/ClassManager.cs +++ b/src/runtime/ClassManager.cs @@ -553,7 +553,7 @@ void AddMember(string name, string snakeCasedName, bool isStaticReadonlyCallable } methodList.Add(ctor, true); // Same constructor, but with snake-cased arguments - if (ctor.GetParameters().Any(pi => pi.Name.ToSnakeCase() != pi.Name)) + if (ctor.GetParameters().Any(pi => pi.Name?.ToSnakeCase() != pi.Name)) { methodList.Add(ctor, false); } diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index bef394ba7..f598da499 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -365,6 +365,16 @@ internal static int ArgPrecedence(Type t, MethodInformation mi) return -1; } + if (t.IsArray) + { + Type e = t.GetElementType(); + if (e == objectType) + { + return 2500; + } + return 100 + ArgPrecedence(e, mi); + } + TypeCode tc = Type.GetTypeCode(t); // TODO: Clean up switch (tc) @@ -406,16 +416,6 @@ internal static int ArgPrecedence(Type t, MethodInformation mi) return 40; } - if (t.IsArray) - { - Type e = t.GetElementType(); - if (e == objectType) - { - return 2500; - } - return 100 + ArgPrecedence(e, mi); - } - return 2000; }