From 4fc58714415ab245e955c3d0cafd7df98921a7d8 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 30 Oct 2024 18:50:32 -0400 Subject: [PATCH 1/8] Match PyObject arguments overloads first --- src/embed_tests/TestMethodBinder.cs | 2609 ++++++++++++++------------- src/runtime/MethodBinder.cs | 2292 +++++++++++------------ 2 files changed, 2500 insertions(+), 2401 deletions(-) diff --git a/src/embed_tests/TestMethodBinder.cs b/src/embed_tests/TestMethodBinder.cs index 355a96c3f..78aa6d1f2 100644 --- a/src/embed_tests/TestMethodBinder.cs +++ b/src/embed_tests/TestMethodBinder.cs @@ -1,1283 +1,1330 @@ -using System; -using System.Linq; -using Python.Runtime; -using NUnit.Framework; -using System.Collections.Generic; -using System.Diagnostics; -using static Python.Runtime.Py; - -namespace Python.EmbeddingTest -{ - public class TestMethodBinder - { - private static dynamic module; - private static string testModule = @" -from datetime import * -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * -class PythonModel(TestMethodBinder.CSharpModel): - def TestA(self): - return self.OnlyString(TestMethodBinder.TestImplicitConversion()) - def TestB(self): - return self.OnlyClass('input string') - def TestC(self): - return self.InvokeModel('input string') - def TestD(self): - return self.InvokeModel(TestMethodBinder.TestImplicitConversion()) - def TestE(self, array): - return array.Length == 2 - def TestF(self): - model = TestMethodBinder.CSharpModel() - model.TestEnumerable(model.SomeList) - def TestG(self): - model = TestMethodBinder.CSharpModel() - model.TestList(model.SomeList) - def TestH(self): - return self.OnlyString(TestMethodBinder.ErroredImplicitConversion()) - def MethodTimeSpanTest(self): - TestMethodBinder.CSharpModel.MethodDateTimeAndTimeSpan(self, timedelta(days = 1), TestMethodBinder.SomeEnu.A, pinocho = 0) - TestMethodBinder.CSharpModel.MethodDateTimeAndTimeSpan(self, date(1, 1, 1), TestMethodBinder.SomeEnu.A, pinocho = 0) - TestMethodBinder.CSharpModel.MethodDateTimeAndTimeSpan(self, datetime(1, 1, 1, 1, 1, 1), TestMethodBinder.SomeEnu.A, pinocho = 0) - def NumericalArgumentMethodInteger(self): - self.NumericalArgumentMethod(1) - def NumericalArgumentMethodDouble(self): - self.NumericalArgumentMethod(0.1) - def NumericalArgumentMethodNumpy64Float(self): - self.NumericalArgumentMethod(TestMethodBinder.Numpy.float64(0.1)) - def ListKeyValuePairTest(self): - self.ListKeyValuePair([{'key': 1}]) - self.ListKeyValuePair([]) - def EnumerableKeyValuePairTest(self): - self.EnumerableKeyValuePair([{'key': 1}]) - self.EnumerableKeyValuePair([]) - def MethodWithParamsTest(self): - self.MethodWithParams(1, 'pepe') - - def TestList(self): - model = TestMethodBinder.CSharpModel() - model.List([TestMethodBinder.CSharpModel]) - def TestListReadOnlyCollection(self): - model = TestMethodBinder.CSharpModel() - model.ListReadOnlyCollection([TestMethodBinder.CSharpModel]) - def TestEnumerable(self): - model = TestMethodBinder.CSharpModel() - model.ListEnumerable([TestMethodBinder.CSharpModel])"; - - public static dynamic Numpy; - - [OneTimeSetUp] - public void SetUp() - { +using System; +using System.Linq; +using Python.Runtime; +using NUnit.Framework; +using System.Collections.Generic; +using System.Diagnostics; +using static Python.Runtime.Py; + +namespace Python.EmbeddingTest +{ + public class TestMethodBinder + { + private static dynamic module; + private static string testModule = @" +from datetime import * +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * +class PythonModel(TestMethodBinder.CSharpModel): + def TestA(self): + return self.OnlyString(TestMethodBinder.TestImplicitConversion()) + def TestB(self): + return self.OnlyClass('input string') + def TestC(self): + return self.InvokeModel('input string') + def TestD(self): + return self.InvokeModel(TestMethodBinder.TestImplicitConversion()) + def TestE(self, array): + return array.Length == 2 + def TestF(self): + model = TestMethodBinder.CSharpModel() + model.TestEnumerable(model.SomeList) + def TestG(self): + model = TestMethodBinder.CSharpModel() + model.TestList(model.SomeList) + def TestH(self): + return self.OnlyString(TestMethodBinder.ErroredImplicitConversion()) + def MethodTimeSpanTest(self): + TestMethodBinder.CSharpModel.MethodDateTimeAndTimeSpan(self, timedelta(days = 1), TestMethodBinder.SomeEnu.A, pinocho = 0) + TestMethodBinder.CSharpModel.MethodDateTimeAndTimeSpan(self, date(1, 1, 1), TestMethodBinder.SomeEnu.A, pinocho = 0) + TestMethodBinder.CSharpModel.MethodDateTimeAndTimeSpan(self, datetime(1, 1, 1, 1, 1, 1), TestMethodBinder.SomeEnu.A, pinocho = 0) + def NumericalArgumentMethodInteger(self): + self.NumericalArgumentMethod(1) + def NumericalArgumentMethodDouble(self): + self.NumericalArgumentMethod(0.1) + def NumericalArgumentMethodNumpy64Float(self): + self.NumericalArgumentMethod(TestMethodBinder.Numpy.float64(0.1)) + def ListKeyValuePairTest(self): + self.ListKeyValuePair([{'key': 1}]) + self.ListKeyValuePair([]) + def EnumerableKeyValuePairTest(self): + self.EnumerableKeyValuePair([{'key': 1}]) + self.EnumerableKeyValuePair([]) + def MethodWithParamsTest(self): + self.MethodWithParams(1, 'pepe') + + def TestList(self): + model = TestMethodBinder.CSharpModel() + model.List([TestMethodBinder.CSharpModel]) + def TestListReadOnlyCollection(self): + model = TestMethodBinder.CSharpModel() + model.ListReadOnlyCollection([TestMethodBinder.CSharpModel]) + def TestEnumerable(self): + model = TestMethodBinder.CSharpModel() + model.ListEnumerable([TestMethodBinder.CSharpModel])"; + + public static dynamic Numpy; + + [OneTimeSetUp] + public void SetUp() + { PythonEngine.Initialize(); - - try - { - Numpy = Py.Import("numpy"); - } - catch (PythonException) - { - } - - using (Py.GIL()) - { - module = PyModule.FromString("module", testModule).GetAttr("PythonModel").Invoke(); - } - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - - [Test] - public void MethodCalledList() - { - using (Py.GIL()) - module.TestList(); - Assert.AreEqual("List(List collection)", CSharpModel.MethodCalled); - } - - [Test] - public void MethodCalledReadOnlyCollection() - { - using (Py.GIL()) - module.TestListReadOnlyCollection(); - Assert.AreEqual("List(IReadOnlyCollection collection)", CSharpModel.MethodCalled); - } - - [Test] - public void MethodCalledEnumerable() - { - using (Py.GIL()) - module.TestEnumerable(); - Assert.AreEqual("List(IEnumerable collection)", CSharpModel.MethodCalled); - } - - [Test] - public void ListToEnumerableExpectingMethod() - { - using (Py.GIL()) - Assert.DoesNotThrow(() => module.TestF()); - } - - [Test] - public void ListToListExpectingMethod() - { - using (Py.GIL()) - Assert.DoesNotThrow(() => module.TestG()); - } - - [Test] - public void ImplicitConversionToString() - { - using (Py.GIL()) - { - var data = (string)module.TestA(); - // we assert implicit conversion took place - Assert.AreEqual("OnlyString impl: implicit to string", data); - } - } - - [Test] - public void ImplicitConversionToClass() - { - using (Py.GIL()) - { - var data = (string)module.TestB(); - // we assert implicit conversion took place - Assert.AreEqual("OnlyClass impl", data); - } - } - - // Reproduces a bug in which program explodes when implicit conversion fails - // in Linux - [Test] - public void ImplicitConversionErrorHandling() - { - using (Py.GIL()) - { - var errorCaught = false; - try - { - var data = (string)module.TestH(); - } - catch (Exception e) - { - errorCaught = true; - Assert.AreEqual("Failed to implicitly convert Python.EmbeddingTest.TestMethodBinder+ErroredImplicitConversion to System.String", e.Message); - } - - Assert.IsTrue(errorCaught); - } - } - - [Test] - public void WillAvoidUsingImplicitConversionIfPossible_String() - { - using (Py.GIL()) - { - var data = (string)module.TestC(); - // we assert no implicit conversion took place - Assert.AreEqual("string impl: input string", data); - } - } - - [Test] - public void WillAvoidUsingImplicitConversionIfPossible_Class() - { - using (Py.GIL()) - { - var data = (string)module.TestD(); - - // we assert no implicit conversion took place - Assert.AreEqual("TestImplicitConversion impl", data); - } - } - - [Test] - public void ArrayLength() - { - using (Py.GIL()) - { - var array = new[] { "pepe", "pinocho" }; - var data = (bool)module.TestE(array); - - // Assert it is true - Assert.AreEqual(true, data); - } - } - - [Test] - public void MethodDateTimeAndTimeSpan() - { - using (Py.GIL()) - Assert.DoesNotThrow(() => module.MethodTimeSpanTest()); - } - - [Test] - public void NumericalArgumentMethod() - { - using (Py.GIL()) - { - CSharpModel.ProvidedArgument = 0; - - module.NumericalArgumentMethodInteger(); - Assert.AreEqual(typeof(int), CSharpModel.ProvidedArgument.GetType()); - Assert.AreEqual(1, CSharpModel.ProvidedArgument); - - // python float type has double precision - module.NumericalArgumentMethodDouble(); - Assert.AreEqual(typeof(double), CSharpModel.ProvidedArgument.GetType()); - Assert.AreEqual(0.1d, CSharpModel.ProvidedArgument); - - module.NumericalArgumentMethodNumpy64Float(); - Assert.AreEqual(typeof(decimal), CSharpModel.ProvidedArgument.GetType()); - Assert.AreEqual(0.1, CSharpModel.ProvidedArgument); - } - } - - [Test] - // TODO: see GH issue https://github.com/pythonnet/pythonnet/issues/1532 re importing numpy after an engine restart fails - // so moving example test here so we import numpy once - public void TestReadme() - { - using (Py.GIL()) - { - Assert.AreEqual("1.0", Numpy.cos(Numpy.pi * 2).ToString()); - - dynamic sin = Numpy.sin; - StringAssert.StartsWith("-0.95892", sin(5).ToString()); - - double c = Numpy.cos(5) + sin(5); - Assert.AreEqual(-0.675262, c, 0.01); - - dynamic a = Numpy.array(new List { 1, 2, 3 }); - Assert.AreEqual("float64", a.dtype.ToString()); - - dynamic b = Numpy.array(new List { 6, 5, 4 }, Py.kw("dtype", Numpy.int32)); - Assert.AreEqual("int32", b.dtype.ToString()); - - Assert.AreEqual("[ 6. 10. 12.]", (a * b).ToString().Replace(" ", " ")); - } - } - - [Test] - public void NumpyDateTime64() - { - using (Py.GIL()) - { - var number = 10; - var numpyDateTime = Numpy.datetime64("2011-02"); - - object result; - var converted = Converter.ToManaged(numpyDateTime, typeof(DateTime), out result, false); - - Assert.IsTrue(converted); - Assert.AreEqual(new DateTime(2011, 02, 1), result); - } - } - - [Test] - public void ListKeyValuePair() - { - using (Py.GIL()) - Assert.DoesNotThrow(() => module.ListKeyValuePairTest()); - } - - [Test] - public void EnumerableKeyValuePair() - { - using (Py.GIL()) - Assert.DoesNotThrow(() => module.EnumerableKeyValuePairTest()); - } - - [Test] - public void MethodWithParamsPerformance() - { - using (Py.GIL()) - { - var stopwatch = new Stopwatch(); - stopwatch.Start(); - for (var i = 0; i < 100000; i++) - { - module.MethodWithParamsTest(); - } - stopwatch.Stop(); - - Console.WriteLine($"Took: {stopwatch.ElapsedMilliseconds}"); - } - } - - [Test] - public void NumericalArgumentMethodNumpy64FloatPerformance() - { - using (Py.GIL()) - { - var stopwatch = new Stopwatch(); - stopwatch.Start(); - for (var i = 0; i < 100000; i++) - { - module.NumericalArgumentMethodNumpy64Float(); - } - stopwatch.Stop(); - - Console.WriteLine($"Took: {stopwatch.ElapsedMilliseconds}"); - } - } - - [Test] - public void MethodWithParamsTest() - { - using (Py.GIL()) - Assert.DoesNotThrow(() => module.MethodWithParamsTest()); - } - - [Test] - public void TestNonStaticGenericMethodBinding() - { - using (Py.GIL()) - { - // Test matching generic on instance functions - // i.e. function signature is (Generic var1) - - // Run in C# - var class1 = new TestGenericClass1(); - var class2 = new TestGenericClass2(); - - class1.TestNonStaticGenericMethod(class1); - class2.TestNonStaticGenericMethod(class2); - - Assert.AreEqual(1, class1.Value); - Assert.AreEqual(1, class2.Value); - - // Run in Python - Assert.DoesNotThrow(() => PyModule.FromString("test", @" -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * -class1 = TestMethodBinder.TestGenericClass1() -class2 = TestMethodBinder.TestGenericClass2() - -class1.TestNonStaticGenericMethod(class1) -class2.TestNonStaticGenericMethod(class2) - -if class1.Value != 1 or class2.Value != 1: - raise AssertionError('Values were not updated') - ")); - } - } - - [Test] - public void TestGenericMethodBinding() - { - using (Py.GIL()) - { - // Test matching generic - // i.e. function signature is (Generic var1) - - // Run in C# - var class1 = new TestGenericClass1(); - var class2 = new TestGenericClass2(); - - TestGenericMethod(class1); - TestGenericMethod(class2); - - Assert.AreEqual(1, class1.Value); - Assert.AreEqual(1, class2.Value); - - // Run in Python - Assert.DoesNotThrow(() => PyModule.FromString("test", @" -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * -class1 = TestMethodBinder.TestGenericClass1() -class2 = TestMethodBinder.TestGenericClass2() - -TestMethodBinder.TestGenericMethod(class1) -TestMethodBinder.TestGenericMethod(class2) - -if class1.Value != 1 or class2.Value != 1: - raise AssertionError('Values were not updated') -")); - } - } - - [Test] - public void TestMultipleGenericMethodBinding() - { - using (Py.GIL()) - { - // Test matching multiple generics - // i.e. function signature is (Generic var1) - - // Run in C# - var class1 = new TestMultipleGenericClass1(); - var class2 = new TestMultipleGenericClass2(); - - TestMultipleGenericMethod(class1); - TestMultipleGenericMethod(class2); - - Assert.AreEqual(1, class1.Value); - Assert.AreEqual(1, class2.Value); - - // Run in Python - Assert.DoesNotThrow(() => PyModule.FromString("test", @" -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * -class1 = TestMethodBinder.TestMultipleGenericClass1() -class2 = TestMethodBinder.TestMultipleGenericClass2() - -TestMethodBinder.TestMultipleGenericMethod(class1) -TestMethodBinder.TestMultipleGenericMethod(class2) - -if class1.Value != 1 or class2.Value != 1: - raise AssertionError('Values were not updated') -")); - } - } - - [Test] - public void TestMultipleGenericParamMethodBinding() - { - using (Py.GIL()) - { - // Test multiple param generics matching - // i.e. function signature is (Generic1 var1, Generic var2) - - // Run in C# - var class1a = new TestGenericClass1(); - var class1b = new TestMultipleGenericClass1(); - - TestMultipleGenericParamsMethod(class1a, class1b); - - Assert.AreEqual(1, class1a.Value); - Assert.AreEqual(1, class1a.Value); - - - var class2a = new TestGenericClass2(); - var class2b = new TestMultipleGenericClass2(); - - TestMultipleGenericParamsMethod(class2a, class2b); - - Assert.AreEqual(1, class2a.Value); - Assert.AreEqual(1, class2b.Value); - - // Run in Python - Assert.DoesNotThrow(() => PyModule.FromString("test", @" -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * -class1a = TestMethodBinder.TestGenericClass1() -class1b = TestMethodBinder.TestMultipleGenericClass1() - -TestMethodBinder.TestMultipleGenericParamsMethod(class1a, class1b) - -if class1a.Value != 1 or class1b.Value != 1: - raise AssertionError('Values were not updated') - -class2a = TestMethodBinder.TestGenericClass2() -class2b = TestMethodBinder.TestMultipleGenericClass2() - -TestMethodBinder.TestMultipleGenericParamsMethod(class2a, class2b) - -if class2a.Value != 1 or class2b.Value != 1: - raise AssertionError('Values were not updated') -")); - } - } - - [Test] - public void TestMultipleGenericParamMethodBinding_MixedOrder() - { - using (Py.GIL()) - { - // Test matching multiple param generics with mixed order - // i.e. function signature is (Generic1 var1, Generic var2) - - // Run in C# - var class1a = new TestGenericClass2(); - var class1b = new TestMultipleGenericClass1(); - - TestMultipleGenericParamsMethod2(class1a, class1b); - - Assert.AreEqual(1, class1a.Value); - Assert.AreEqual(1, class1a.Value); - - var class2a = new TestGenericClass1(); - var class2b = new TestMultipleGenericClass2(); - - TestMultipleGenericParamsMethod2(class2a, class2b); - - Assert.AreEqual(1, class2a.Value); - Assert.AreEqual(1, class2b.Value); - - // Run in Python - Assert.DoesNotThrow(() => PyModule.FromString("test", @" -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * -class1a = TestMethodBinder.TestGenericClass2() -class1b = TestMethodBinder.TestMultipleGenericClass1() - -TestMethodBinder.TestMultipleGenericParamsMethod2(class1a, class1b) - -if class1a.Value != 1 or class1b.Value != 1: - raise AssertionError('Values were not updated') - -class2a = TestMethodBinder.TestGenericClass1() -class2b = TestMethodBinder.TestMultipleGenericClass2() - -TestMethodBinder.TestMultipleGenericParamsMethod2(class2a, class2b) - -if class2a.Value != 1 or class2b.Value != 1: - raise AssertionError('Values were not updated') -")); - } - } - - [Test] - public void TestPyClassGenericBinding() - { - using (Py.GIL()) - // Overriding our generics in Python we should still match with the generic method - Assert.DoesNotThrow(() => PyModule.FromString("test", @" -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * - -class PyGenericClass(TestMethodBinder.TestGenericClass1): - pass - -class PyMultipleGenericClass(TestMethodBinder.TestMultipleGenericClass1): - pass - -singleGenericClass = PyGenericClass() -multiGenericClass = PyMultipleGenericClass() - -TestMethodBinder.TestGenericMethod(singleGenericClass) -TestMethodBinder.TestMultipleGenericMethod(multiGenericClass) -TestMethodBinder.TestMultipleGenericParamsMethod(singleGenericClass, multiGenericClass) - -if singleGenericClass.Value != 1 or multiGenericClass.Value != 1: - raise AssertionError('Values were not updated') -")); - } - - [Test] - public void TestNonGenericIsUsedWhenAvailable() - { - using (Py.GIL()) - {// Run in C# - var class1 = new TestGenericClass3(); - TestGenericMethod(class1); - Assert.AreEqual(10, class1.Value); - - - // When available, should select non-generic method over generic method - Assert.DoesNotThrow(() => PyModule.FromString("test", @" -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * - -class1 = TestMethodBinder.TestGenericClass3() - -TestMethodBinder.TestGenericMethod(class1) - -if class1.Value != 10: - raise AssertionError('Value was not updated') -")); - } - } - - [Test] - public void TestMatchTypedGenericOverload() - { - using (Py.GIL()) - {// Test to ensure we can match a typed generic overload - // even when there are other matches that would apply. - var class1 = new TestGenericClass4(); - TestGenericMethod(class1); - Assert.AreEqual(15, class1.Value); - - Assert.DoesNotThrow(() => PyModule.FromString("test", @" -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * - -class1 = TestMethodBinder.TestGenericClass4() - -TestMethodBinder.TestGenericMethod(class1) - -if class1.Value != 15: - raise AssertionError('Value was not updated') -")); - } - } - - [Test] - public void TestGenericBindingSpeed() - { - using (Py.GIL()) - { - var stopwatch = new Stopwatch(); - stopwatch.Start(); - for (int i = 0; i < 10000; i++) - { - TestMultipleGenericParamMethodBinding(); - } - stopwatch.Stop(); - - Console.WriteLine($"Took: {stopwatch.ElapsedMilliseconds} ms"); - } - } - - [Test] - public void TestGenericTypeMatchingWithConvertedPyType() - { - // This test ensures that we can still match and bind a generic method when we - // have a converted pytype in the args (py timedelta -> C# TimeSpan) - - using (Py.GIL()) - Assert.DoesNotThrow(() => PyModule.FromString("test", @" -from datetime import timedelta -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * -class1 = TestMethodBinder.TestGenericClass1() - -span = timedelta(hours=5) - -TestMethodBinder.TestGenericMethod(class1, span) - -if class1.Value != 5: - raise AssertionError('Values were not updated properly') -")); - } - - [Test] - public void TestGenericTypeMatchingWithDefaultArgs() - { - // This test ensures that we can still match and bind a generic method when we have default args - - using (Py.GIL()) - Assert.DoesNotThrow(() => PyModule.FromString("test", @" -from datetime import timedelta -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * -class1 = TestMethodBinder.TestGenericClass1() - -TestMethodBinder.TestGenericMethodWithDefault(class1) - -if class1.Value != 25: - raise AssertionError(f'Value was not 25, was {class1.Value}') - -TestMethodBinder.TestGenericMethodWithDefault(class1, 50) - -if class1.Value != 50: - raise AssertionError('Value was not 50, was {class1.Value}') -")); - } - - [Test] - public void TestGenericTypeMatchingWithNullDefaultArgs() - { - // This test ensures that we can still match and bind a generic method when we have \ - // null default args, important because caching by arg types occurs - - using (Py.GIL()) - Assert.DoesNotThrow(() => PyModule.FromString("test", @" -from datetime import timedelta -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * -class1 = TestMethodBinder.TestGenericClass1() - -TestMethodBinder.TestGenericMethodWithNullDefault(class1) - -if class1.Value != 10: - raise AssertionError(f'Value was not 25, was {class1.Value}') - -TestMethodBinder.TestGenericMethodWithNullDefault(class1, class1) - -if class1.Value != 20: - raise AssertionError('Value was not 50, was {class1.Value}') -")); - } - - [Test] - public void TestMatchPyDateToDateTime() - { - using (Py.GIL()) - // This test ensures that we match py datetime.date object to C# DateTime object - Assert.DoesNotThrow(() => PyModule.FromString("test", @" -from datetime import * -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * - -test = date(year=2011, month=5, day=1) -result = TestMethodBinder.GetMonth(test) - -if result != 5: - raise AssertionError('Failed to return expected value 1') -")); - } - - public class OverloadsTestClass - { - - public string Method1(string positionalArg, decimal namedArg1 = 1.2m, int namedArg2 = 123) - { - Console.WriteLine("1"); - return "Method1 Overload 1"; - } - - public string Method1(decimal namedArg1 = 1.2m, int namedArg2 = 123) - { - Console.WriteLine("2"); - return "Method1 Overload 2"; - } - - // ---- - - public string Method2(string arg1, int arg2, decimal arg3, decimal kwarg1 = 1.1m, bool kwarg2 = false, string kwarg3 = "") - { - return "Method2 Overload 1"; - } - - public string Method2(string arg1, int arg2, decimal kwarg1 = 1.1m, bool kwarg2 = false, string kwarg3 = "") - { - return "Method2 Overload 2"; - } - - // ---- - - public string Method3(string arg1, int arg2, float arg3, float kwarg1 = 1.1f, bool kwarg2 = false, string kwarg3 = "") - { - return "Method3 Overload 1"; - } - - public string Method3(string arg1, int arg2, float kwarg1 = 1.1f, bool kwarg2 = false, string kwarg3 = "") - { - return "Method3 Overload 2"; - } - - // ---- - - public string ImplicitConversionSameArgumentCount(string symbol, int quantity, float trailingAmount, bool trailingAsPercentage, string tag = "") - { - return "ImplicitConversionSameArgumentCount 1"; - } - - public string ImplicitConversionSameArgumentCount(string symbol, decimal quantity, decimal trailingAmount, bool trailingAsPercentage, string tag = "") - { - return "ImplicitConversionSameArgumentCount 2"; - } - - public string ImplicitConversionSameArgumentCount2(string symbol, int quantity, float trailingAmount, bool trailingAsPercentage, string tag = "") - { - return "ImplicitConversionSameArgumentCount2 1"; - } - - public string ImplicitConversionSameArgumentCount2(string symbol, float quantity, float trailingAmount, bool trailingAsPercentage, string tag = "") - { - return "ImplicitConversionSameArgumentCount2 2"; - } - - public string ImplicitConversionSameArgumentCount2(string symbol, decimal quantity, float trailingAmount, bool trailingAsPercentage, string tag = "") - { - 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")] - [TestCase("Method1('abc', namedArg1=12.34, namedArg2=321)", "Method1 Overload 1")] - [TestCase("Method2(\"SPY\", 10, 123, kwarg1=1, kwarg2=True)", "Method2 Overload 1")] - [TestCase("Method2(\"SPY\", 10, 123.34, kwarg1=1.23, kwarg2=True)", "Method2 Overload 1")] - [TestCase("Method3(\"SPY\", 10, 123.34, kwarg1=1.23, kwarg2=True)", "Method3 Overload 1")] - public void SelectsRightOverloadWithNamedParameters(string methodCallCode, string expectedResult) - { - using var _ = Py.GIL(); - - dynamic module = PyModule.FromString("SelectsRightOverloadWithNamedParameters", @$" - -def call_method(instance): - return instance.{methodCallCode} -"); - - var instance = new OverloadsTestClass(); - var result = module.call_method(instance).As(); - - Assert.AreEqual(expectedResult, result); - } - - [TestCase("ImplicitConversionSameArgumentCount", "10", "ImplicitConversionSameArgumentCount 1")] - [TestCase("ImplicitConversionSameArgumentCount", "10.1", "ImplicitConversionSameArgumentCount 2")] - [TestCase("ImplicitConversionSameArgumentCount2", "10", "ImplicitConversionSameArgumentCount2 1")] - [TestCase("ImplicitConversionSameArgumentCount2", "10.1", "ImplicitConversionSameArgumentCount2 2")] - public void DisambiguatesOverloadWithSameArgumentCountAndImplicitConversion(string methodName, string quantity, string expectedResult) - { - using var _ = Py.GIL(); - - dynamic module = PyModule.FromString("DisambiguatesOverloadWithSameArgumentCountAndImplicitConversion", @$" -def call_method(instance): - return instance.{methodName}(""SPY"", {quantity}, 123.4, trailingAsPercentage=True) -"); - - var instance = new OverloadsTestClass(); - var result = module.call_method(instance).As(); - - Assert.AreEqual(expectedResult, result); - } - - public class CSharpClass - { - public string CalledMethodMessage { get; private set; } - - public void Method() - { - CalledMethodMessage = "Overload 1"; - } - - public void Method(string stringArgument, decimal decimalArgument = 1.2m) - { - CalledMethodMessage = "Overload 2"; - } - - public void Method(PyObject typeArgument, decimal decimalArgument = 1.2m) - { - CalledMethodMessage = "Overload 3"; - } - } - - [Test] - public void CallsCorrectOverloadWithoutErrors() - { - using var _ = Py.GIL(); - - var module = PyModule.FromString("CallsCorrectOverloadWithoutErrors", @" -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * - -class PythonModel(TestMethodBinder.CSharpModel): - pass - -def call_method(instance): - instance.Method(PythonModel, decimalArgument=1.234) -"); - - var instance = new CSharpClass(); + using var _ = Py.GIL(); + + try + { + Numpy = Py.Import("numpy"); + } + catch (PythonException) + { + } + + module = PyModule.FromString("module", testModule).GetAttr("PythonModel").Invoke(); + } + + [OneTimeTearDown] + public void Dispose() + { + PythonEngine.Shutdown(); + } + + [Test] + public void MethodCalledList() + { + using (Py.GIL()) + module.TestList(); + Assert.AreEqual("List(List collection)", CSharpModel.MethodCalled); + } + + [Test] + public void MethodCalledReadOnlyCollection() + { + using (Py.GIL()) + module.TestListReadOnlyCollection(); + Assert.AreEqual("List(IReadOnlyCollection collection)", CSharpModel.MethodCalled); + } + + [Test] + public void MethodCalledEnumerable() + { + using (Py.GIL()) + module.TestEnumerable(); + Assert.AreEqual("List(IEnumerable collection)", CSharpModel.MethodCalled); + } + + [Test] + public void ListToEnumerableExpectingMethod() + { + using (Py.GIL()) + Assert.DoesNotThrow(() => module.TestF()); + } + + [Test] + public void ListToListExpectingMethod() + { + using (Py.GIL()) + Assert.DoesNotThrow(() => module.TestG()); + } + + [Test] + public void ImplicitConversionToString() + { + using (Py.GIL()) + { + var data = (string)module.TestA(); + // we assert implicit conversion took place + Assert.AreEqual("OnlyString impl: implicit to string", data); + } + } + + [Test] + public void ImplicitConversionToClass() + { + using (Py.GIL()) + { + var data = (string)module.TestB(); + // we assert implicit conversion took place + Assert.AreEqual("OnlyClass impl", data); + } + } + + // Reproduces a bug in which program explodes when implicit conversion fails + // in Linux + [Test] + public void ImplicitConversionErrorHandling() + { + using (Py.GIL()) + { + var errorCaught = false; + try + { + var data = (string)module.TestH(); + } + catch (Exception e) + { + errorCaught = true; + Assert.AreEqual("Failed to implicitly convert Python.EmbeddingTest.TestMethodBinder+ErroredImplicitConversion to System.String", e.Message); + } + + Assert.IsTrue(errorCaught); + } + } + + [Test] + public void WillAvoidUsingImplicitConversionIfPossible_String() + { + using (Py.GIL()) + { + var data = (string)module.TestC(); + // we assert no implicit conversion took place + Assert.AreEqual("string impl: input string", data); + } + } + + [Test] + public void WillAvoidUsingImplicitConversionIfPossible_Class() + { + using (Py.GIL()) + { + var data = (string)module.TestD(); + + // we assert no implicit conversion took place + Assert.AreEqual("TestImplicitConversion impl", data); + } + } + + [Test] + public void ArrayLength() + { + using (Py.GIL()) + { + var array = new[] { "pepe", "pinocho" }; + var data = (bool)module.TestE(array); + + // Assert it is true + Assert.AreEqual(true, data); + } + } + + [Test] + public void MethodDateTimeAndTimeSpan() + { + using (Py.GIL()) + Assert.DoesNotThrow(() => module.MethodTimeSpanTest()); + } + + [Test] + public void NumericalArgumentMethod() + { + using (Py.GIL()) + { + CSharpModel.ProvidedArgument = 0; + + module.NumericalArgumentMethodInteger(); + Assert.AreEqual(typeof(int), CSharpModel.ProvidedArgument.GetType()); + Assert.AreEqual(1, CSharpModel.ProvidedArgument); + + // python float type has double precision + module.NumericalArgumentMethodDouble(); + Assert.AreEqual(typeof(double), CSharpModel.ProvidedArgument.GetType()); + Assert.AreEqual(0.1d, CSharpModel.ProvidedArgument); + + module.NumericalArgumentMethodNumpy64Float(); + Assert.AreEqual(typeof(decimal), CSharpModel.ProvidedArgument.GetType()); + Assert.AreEqual(0.1, CSharpModel.ProvidedArgument); + } + } + + [Test] + // TODO: see GH issue https://github.com/pythonnet/pythonnet/issues/1532 re importing numpy after an engine restart fails + // so moving example test here so we import numpy once + public void TestReadme() + { + using (Py.GIL()) + { + Assert.AreEqual("1.0", Numpy.cos(Numpy.pi * 2).ToString()); + + dynamic sin = Numpy.sin; + StringAssert.StartsWith("-0.95892", sin(5).ToString()); + + double c = Numpy.cos(5) + sin(5); + Assert.AreEqual(-0.675262, c, 0.01); + + dynamic a = Numpy.array(new List { 1, 2, 3 }); + Assert.AreEqual("float64", a.dtype.ToString()); + + dynamic b = Numpy.array(new List { 6, 5, 4 }, Py.kw("dtype", Numpy.int32)); + Assert.AreEqual("int32", b.dtype.ToString()); + + Assert.AreEqual("[ 6. 10. 12.]", (a * b).ToString().Replace(" ", " ")); + } + } + + [Test] + public void NumpyDateTime64() + { + using (Py.GIL()) + { + var number = 10; + var numpyDateTime = Numpy.datetime64("2011-02"); + + object result; + var converted = Converter.ToManaged(numpyDateTime, typeof(DateTime), out result, false); + + Assert.IsTrue(converted); + Assert.AreEqual(new DateTime(2011, 02, 1), result); + } + } + + [Test] + public void ListKeyValuePair() + { + using (Py.GIL()) + Assert.DoesNotThrow(() => module.ListKeyValuePairTest()); + } + + [Test] + public void EnumerableKeyValuePair() + { + using (Py.GIL()) + Assert.DoesNotThrow(() => module.EnumerableKeyValuePairTest()); + } + + [Test] + public void MethodWithParamsPerformance() + { + using (Py.GIL()) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); + for (var i = 0; i < 100000; i++) + { + module.MethodWithParamsTest(); + } + stopwatch.Stop(); + + Console.WriteLine($"Took: {stopwatch.ElapsedMilliseconds}"); + } + } + + [Test] + public void NumericalArgumentMethodNumpy64FloatPerformance() + { + using (Py.GIL()) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); + for (var i = 0; i < 100000; i++) + { + module.NumericalArgumentMethodNumpy64Float(); + } + stopwatch.Stop(); + + Console.WriteLine($"Took: {stopwatch.ElapsedMilliseconds}"); + } + } + + [Test] + public void MethodWithParamsTest() + { + using (Py.GIL()) + Assert.DoesNotThrow(() => module.MethodWithParamsTest()); + } + + [Test] + public void TestNonStaticGenericMethodBinding() + { + using (Py.GIL()) + { + // Test matching generic on instance functions + // i.e. function signature is (Generic var1) + + // Run in C# + var class1 = new TestGenericClass1(); + var class2 = new TestGenericClass2(); + + class1.TestNonStaticGenericMethod(class1); + class2.TestNonStaticGenericMethod(class2); + + Assert.AreEqual(1, class1.Value); + Assert.AreEqual(1, class2.Value); + + // Run in Python + Assert.DoesNotThrow(() => PyModule.FromString("test", @" +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * +class1 = TestMethodBinder.TestGenericClass1() +class2 = TestMethodBinder.TestGenericClass2() + +class1.TestNonStaticGenericMethod(class1) +class2.TestNonStaticGenericMethod(class2) + +if class1.Value != 1 or class2.Value != 1: + raise AssertionError('Values were not updated') + ")); + } + } + + [Test] + public void TestGenericMethodBinding() + { + using (Py.GIL()) + { + // Test matching generic + // i.e. function signature is (Generic var1) + + // Run in C# + var class1 = new TestGenericClass1(); + var class2 = new TestGenericClass2(); + + TestGenericMethod(class1); + TestGenericMethod(class2); + + Assert.AreEqual(1, class1.Value); + Assert.AreEqual(1, class2.Value); + + // Run in Python + Assert.DoesNotThrow(() => PyModule.FromString("test", @" +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * +class1 = TestMethodBinder.TestGenericClass1() +class2 = TestMethodBinder.TestGenericClass2() + +TestMethodBinder.TestGenericMethod(class1) +TestMethodBinder.TestGenericMethod(class2) + +if class1.Value != 1 or class2.Value != 1: + raise AssertionError('Values were not updated') +")); + } + } + + [Test] + public void TestMultipleGenericMethodBinding() + { + using (Py.GIL()) + { + // Test matching multiple generics + // i.e. function signature is (Generic var1) + + // Run in C# + var class1 = new TestMultipleGenericClass1(); + var class2 = new TestMultipleGenericClass2(); + + TestMultipleGenericMethod(class1); + TestMultipleGenericMethod(class2); + + Assert.AreEqual(1, class1.Value); + Assert.AreEqual(1, class2.Value); + + // Run in Python + Assert.DoesNotThrow(() => PyModule.FromString("test", @" +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * +class1 = TestMethodBinder.TestMultipleGenericClass1() +class2 = TestMethodBinder.TestMultipleGenericClass2() + +TestMethodBinder.TestMultipleGenericMethod(class1) +TestMethodBinder.TestMultipleGenericMethod(class2) + +if class1.Value != 1 or class2.Value != 1: + raise AssertionError('Values were not updated') +")); + } + } + + [Test] + public void TestMultipleGenericParamMethodBinding() + { + using (Py.GIL()) + { + // Test multiple param generics matching + // i.e. function signature is (Generic1 var1, Generic var2) + + // Run in C# + var class1a = new TestGenericClass1(); + var class1b = new TestMultipleGenericClass1(); + + TestMultipleGenericParamsMethod(class1a, class1b); + + Assert.AreEqual(1, class1a.Value); + Assert.AreEqual(1, class1a.Value); + + + var class2a = new TestGenericClass2(); + var class2b = new TestMultipleGenericClass2(); + + TestMultipleGenericParamsMethod(class2a, class2b); + + Assert.AreEqual(1, class2a.Value); + Assert.AreEqual(1, class2b.Value); + + // Run in Python + Assert.DoesNotThrow(() => PyModule.FromString("test", @" +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * +class1a = TestMethodBinder.TestGenericClass1() +class1b = TestMethodBinder.TestMultipleGenericClass1() + +TestMethodBinder.TestMultipleGenericParamsMethod(class1a, class1b) + +if class1a.Value != 1 or class1b.Value != 1: + raise AssertionError('Values were not updated') + +class2a = TestMethodBinder.TestGenericClass2() +class2b = TestMethodBinder.TestMultipleGenericClass2() + +TestMethodBinder.TestMultipleGenericParamsMethod(class2a, class2b) + +if class2a.Value != 1 or class2b.Value != 1: + raise AssertionError('Values were not updated') +")); + } + } + + [Test] + public void TestMultipleGenericParamMethodBinding_MixedOrder() + { + using (Py.GIL()) + { + // Test matching multiple param generics with mixed order + // i.e. function signature is (Generic1 var1, Generic var2) + + // Run in C# + var class1a = new TestGenericClass2(); + var class1b = new TestMultipleGenericClass1(); + + TestMultipleGenericParamsMethod2(class1a, class1b); + + Assert.AreEqual(1, class1a.Value); + Assert.AreEqual(1, class1a.Value); + + var class2a = new TestGenericClass1(); + var class2b = new TestMultipleGenericClass2(); + + TestMultipleGenericParamsMethod2(class2a, class2b); + + Assert.AreEqual(1, class2a.Value); + Assert.AreEqual(1, class2b.Value); + + // Run in Python + Assert.DoesNotThrow(() => PyModule.FromString("test", @" +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * +class1a = TestMethodBinder.TestGenericClass2() +class1b = TestMethodBinder.TestMultipleGenericClass1() + +TestMethodBinder.TestMultipleGenericParamsMethod2(class1a, class1b) + +if class1a.Value != 1 or class1b.Value != 1: + raise AssertionError('Values were not updated') + +class2a = TestMethodBinder.TestGenericClass1() +class2b = TestMethodBinder.TestMultipleGenericClass2() + +TestMethodBinder.TestMultipleGenericParamsMethod2(class2a, class2b) + +if class2a.Value != 1 or class2b.Value != 1: + raise AssertionError('Values were not updated') +")); + } + } + + [Test] + public void TestPyClassGenericBinding() + { + using (Py.GIL()) + // Overriding our generics in Python we should still match with the generic method + Assert.DoesNotThrow(() => PyModule.FromString("test", @" +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * + +class PyGenericClass(TestMethodBinder.TestGenericClass1): + pass + +class PyMultipleGenericClass(TestMethodBinder.TestMultipleGenericClass1): + pass + +singleGenericClass = PyGenericClass() +multiGenericClass = PyMultipleGenericClass() + +TestMethodBinder.TestGenericMethod(singleGenericClass) +TestMethodBinder.TestMultipleGenericMethod(multiGenericClass) +TestMethodBinder.TestMultipleGenericParamsMethod(singleGenericClass, multiGenericClass) + +if singleGenericClass.Value != 1 or multiGenericClass.Value != 1: + raise AssertionError('Values were not updated') +")); + } + + [Test] + public void TestNonGenericIsUsedWhenAvailable() + { + using (Py.GIL()) + {// Run in C# + var class1 = new TestGenericClass3(); + TestGenericMethod(class1); + Assert.AreEqual(10, class1.Value); + + + // When available, should select non-generic method over generic method + Assert.DoesNotThrow(() => PyModule.FromString("test", @" +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * + +class1 = TestMethodBinder.TestGenericClass3() + +TestMethodBinder.TestGenericMethod(class1) + +if class1.Value != 10: + raise AssertionError('Value was not updated') +")); + } + } + + [Test] + public void TestMatchTypedGenericOverload() + { + using (Py.GIL()) + {// Test to ensure we can match a typed generic overload + // even when there are other matches that would apply. + var class1 = new TestGenericClass4(); + TestGenericMethod(class1); + Assert.AreEqual(15, class1.Value); + + Assert.DoesNotThrow(() => PyModule.FromString("test", @" +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * + +class1 = TestMethodBinder.TestGenericClass4() + +TestMethodBinder.TestGenericMethod(class1) + +if class1.Value != 15: + raise AssertionError('Value was not updated') +")); + } + } + + [Test] + public void TestGenericBindingSpeed() + { + using (Py.GIL()) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); + for (int i = 0; i < 10000; i++) + { + TestMultipleGenericParamMethodBinding(); + } + stopwatch.Stop(); + + Console.WriteLine($"Took: {stopwatch.ElapsedMilliseconds} ms"); + } + } + + [Test] + public void TestGenericTypeMatchingWithConvertedPyType() + { + // This test ensures that we can still match and bind a generic method when we + // have a converted pytype in the args (py timedelta -> C# TimeSpan) + + using (Py.GIL()) + Assert.DoesNotThrow(() => PyModule.FromString("test", @" +from datetime import timedelta +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * +class1 = TestMethodBinder.TestGenericClass1() + +span = timedelta(hours=5) + +TestMethodBinder.TestGenericMethod(class1, span) + +if class1.Value != 5: + raise AssertionError('Values were not updated properly') +")); + } + + [Test] + public void TestGenericTypeMatchingWithDefaultArgs() + { + // This test ensures that we can still match and bind a generic method when we have default args + + using (Py.GIL()) + Assert.DoesNotThrow(() => PyModule.FromString("test", @" +from datetime import timedelta +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * +class1 = TestMethodBinder.TestGenericClass1() + +TestMethodBinder.TestGenericMethodWithDefault(class1) + +if class1.Value != 25: + raise AssertionError(f'Value was not 25, was {class1.Value}') + +TestMethodBinder.TestGenericMethodWithDefault(class1, 50) + +if class1.Value != 50: + raise AssertionError('Value was not 50, was {class1.Value}') +")); + } + + [Test] + public void TestGenericTypeMatchingWithNullDefaultArgs() + { + // This test ensures that we can still match and bind a generic method when we have \ + // null default args, important because caching by arg types occurs + + using (Py.GIL()) + Assert.DoesNotThrow(() => PyModule.FromString("test", @" +from datetime import timedelta +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * +class1 = TestMethodBinder.TestGenericClass1() + +TestMethodBinder.TestGenericMethodWithNullDefault(class1) + +if class1.Value != 10: + raise AssertionError(f'Value was not 25, was {class1.Value}') + +TestMethodBinder.TestGenericMethodWithNullDefault(class1, class1) + +if class1.Value != 20: + raise AssertionError('Value was not 50, was {class1.Value}') +")); + } + + [Test] + public void TestMatchPyDateToDateTime() + { + using (Py.GIL()) + // This test ensures that we match py datetime.date object to C# DateTime object + Assert.DoesNotThrow(() => PyModule.FromString("test", @" +from datetime import * +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * + +test = date(year=2011, month=5, day=1) +result = TestMethodBinder.GetMonth(test) + +if result != 5: + raise AssertionError('Failed to return expected value 1') +")); + } + + public class OverloadsTestClass + { + + public string Method1(string positionalArg, decimal namedArg1 = 1.2m, int namedArg2 = 123) + { + Console.WriteLine("1"); + return "Method1 Overload 1"; + } + + public string Method1(decimal namedArg1 = 1.2m, int namedArg2 = 123) + { + Console.WriteLine("2"); + return "Method1 Overload 2"; + } + + // ---- + + public string Method2(string arg1, int arg2, decimal arg3, decimal kwarg1 = 1.1m, bool kwarg2 = false, string kwarg3 = "") + { + return "Method2 Overload 1"; + } + + public string Method2(string arg1, int arg2, decimal kwarg1 = 1.1m, bool kwarg2 = false, string kwarg3 = "") + { + return "Method2 Overload 2"; + } + + // ---- + + public string Method3(string arg1, int arg2, float arg3, float kwarg1 = 1.1f, bool kwarg2 = false, string kwarg3 = "") + { + return "Method3 Overload 1"; + } + + public string Method3(string arg1, int arg2, float kwarg1 = 1.1f, bool kwarg2 = false, string kwarg3 = "") + { + return "Method3 Overload 2"; + } + + // ---- + + public string ImplicitConversionSameArgumentCount(string symbol, int quantity, float trailingAmount, bool trailingAsPercentage, string tag = "") + { + return "ImplicitConversionSameArgumentCount 1"; + } + + public string ImplicitConversionSameArgumentCount(string symbol, decimal quantity, decimal trailingAmount, bool trailingAsPercentage, string tag = "") + { + return "ImplicitConversionSameArgumentCount 2"; + } + + public string ImplicitConversionSameArgumentCount2(string symbol, int quantity, float trailingAmount, bool trailingAsPercentage, string tag = "") + { + return "ImplicitConversionSameArgumentCount2 1"; + } + + public string ImplicitConversionSameArgumentCount2(string symbol, float quantity, float trailingAmount, bool trailingAsPercentage, string tag = "") + { + return "ImplicitConversionSameArgumentCount2 2"; + } + + public string ImplicitConversionSameArgumentCount2(string symbol, decimal quantity, float trailingAmount, bool trailingAsPercentage, string tag = "") + { + 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")] + [TestCase("Method1('abc', namedArg1=12.34, namedArg2=321)", "Method1 Overload 1")] + [TestCase("Method2(\"SPY\", 10, 123, kwarg1=1, kwarg2=True)", "Method2 Overload 1")] + [TestCase("Method2(\"SPY\", 10, 123.34, kwarg1=1.23, kwarg2=True)", "Method2 Overload 1")] + [TestCase("Method3(\"SPY\", 10, 123.34, kwarg1=1.23, kwarg2=True)", "Method3 Overload 1")] + public void SelectsRightOverloadWithNamedParameters(string methodCallCode, string expectedResult) + { + using var _ = Py.GIL(); + + dynamic module = PyModule.FromString("SelectsRightOverloadWithNamedParameters", @$" + +def call_method(instance): + return instance.{methodCallCode} +"); + + var instance = new OverloadsTestClass(); + var result = module.call_method(instance).As(); + + Assert.AreEqual(expectedResult, result); + } + + [TestCase("ImplicitConversionSameArgumentCount", "10", "ImplicitConversionSameArgumentCount 1")] + [TestCase("ImplicitConversionSameArgumentCount", "10.1", "ImplicitConversionSameArgumentCount 2")] + [TestCase("ImplicitConversionSameArgumentCount2", "10", "ImplicitConversionSameArgumentCount2 1")] + [TestCase("ImplicitConversionSameArgumentCount2", "10.1", "ImplicitConversionSameArgumentCount2 2")] + public void DisambiguatesOverloadWithSameArgumentCountAndImplicitConversion(string methodName, string quantity, string expectedResult) + { + using var _ = Py.GIL(); + + dynamic module = PyModule.FromString("DisambiguatesOverloadWithSameArgumentCountAndImplicitConversion", @$" +def call_method(instance): + return instance.{methodName}(""SPY"", {quantity}, 123.4, trailingAsPercentage=True) +"); + + var instance = new OverloadsTestClass(); + var result = module.call_method(instance).As(); + + Assert.AreEqual(expectedResult, result); + } + + public class CSharpClass + { + public string CalledMethodMessage { get; private set; } + + public void Method() + { + CalledMethodMessage = "Overload 1"; + } + + public void Method(string stringArgument, decimal decimalArgument = 1.2m) + { + CalledMethodMessage = "Overload 2"; + } + + public void Method(PyObject typeArgument, decimal decimalArgument = 1.2m) + { + CalledMethodMessage = "Overload 3"; + } + } + + [Test] + public void CallsCorrectOverloadWithoutErrors() + { + using var _ = Py.GIL(); + + var module = PyModule.FromString("CallsCorrectOverloadWithoutErrors", @" +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * + +class PythonModel(TestMethodBinder.CSharpModel): + pass + +def call_method(instance): + instance.Method(PythonModel, decimalArgument=1.234) +"); + + var instance = new CSharpClass(); + using var pyInstance = instance.ToPython(); + + Assert.DoesNotThrow(() => + { + module.GetAttr("call_method").Invoke(pyInstance); + }); + + Assert.AreEqual("Overload 3", instance.CalledMethodMessage); + + Assert.IsFalse(Exceptions.ErrorOccurred()); + } + + public class CSharpClass2 + { + public string CalledMethodMessage { get; private set; } + + public void Method() + { + CalledMethodMessage = "Overload 1"; + } + + public void Method(CSharpClass csharpClassArgument, decimal decimalArgument = 1.2m, PyObject pyObjectKArgument = null) + { + CalledMethodMessage = "Overload 2"; + } + + public void Method(PyObject pyObjectArgument, decimal decimalArgument = 1.2m, object objectArgument = null) + { + CalledMethodMessage = "Overload 3"; + } + + // This must be matched when passing just a single argument and it's a PyObject, + // event though the PyObject kwarg in the second overload has more precedence. + // But since it will not be passed, this overload must be called. + public void Method(PyObject pyObjectArgument, decimal decimalArgument = 1.2m, int intArgument = 0) + { + CalledMethodMessage = "Overload 4"; + } + } + + [Test] + public void PyObjectArgsHavePrecedenceOverOtherTypes() + { + using var _ = Py.GIL(); + + var instance = new CSharpClass2(); using var pyInstance = instance.ToPython(); - - Assert.DoesNotThrow(() => - { - module.GetAttr("call_method").Invoke(pyInstance); - }); - - Assert.AreEqual("Overload 3", instance.CalledMethodMessage); - - Assert.IsFalse(Exceptions.ErrorOccurred()); - } - - [Test] - public void BindsConstructorToSnakeCasedArgumentsVersion([Values] bool useCamelCase, [Values] bool passOptionalArgument) - { - using var _ = Py.GIL(); - - var argument1Name = useCamelCase ? "someArgument" : "some_argument"; - var argument2Name = useCamelCase ? "anotherArgument" : "another_argument"; - var argument2Code = passOptionalArgument ? $", {argument2Name}=\"another argument value\"" : ""; - - var module = PyModule.FromString("BindsConstructorToSnakeCasedArgumentsVersion", @$" -from clr import AddReference -AddReference(""System"") -from Python.EmbeddingTest import * - -def create_instance(): - return TestMethodBinder.CSharpModel({argument1Name}=1{argument2Code}) -"); - var exception = Assert.Throws(() => module.GetAttr("create_instance").Invoke()); - var sourceException = exception.InnerException; - Assert.IsInstanceOf(sourceException); - - 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); - } - - [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) - { - return test.Month; - } - - public class CSharpModel - { - public static string MethodCalled { get; set; } - public static dynamic ProvidedArgument; - public List SomeList { get; set; } - - public CSharpModel() - { - SomeList = new List - { - new TestImplicitConversion() - }; - } - - public CSharpModel(int someArgument, string anotherArgument = "another argument default value") - { - throw new NotImplementedException($"Constructor with arguments: someArgument={someArgument}. anotherArgument=\"{anotherArgument}\""); - } - - public void TestList(List conversions) - { - if (!conversions.Any()) - { - throw new ArgumentException("We expect at least an instance"); - } - } - - public void TestEnumerable(IEnumerable conversions) - { - if (!conversions.Any()) - { - throw new ArgumentException("We expect at least an instance"); - } - } - - public bool SomeMethod() - { - return true; - } - - public virtual string OnlyClass(TestImplicitConversion data) - { - return "OnlyClass impl"; - } - - public virtual string OnlyString(string data) - { - return "OnlyString impl: " + data; - } - - public virtual string InvokeModel(string data) - { - return "string impl: " + data; - } - - public virtual string InvokeModel(TestImplicitConversion data) - { - return "TestImplicitConversion impl"; - } - - public void NumericalArgumentMethod(int value) - { - ProvidedArgument = value; - } - public void NumericalArgumentMethod(float value) - { - ProvidedArgument = value; - } - public void NumericalArgumentMethod(double value) - { - ProvidedArgument = value; - } - public void NumericalArgumentMethod(decimal value) - { - ProvidedArgument = value; - } - public void EnumerableKeyValuePair(IEnumerable> value) - { - ProvidedArgument = value; - } - public void ListKeyValuePair(List> value) - { - ProvidedArgument = value; - } - - public void MethodWithParams(decimal value, params string[] argument) - { - - } - - public void ListReadOnlyCollection(IReadOnlyCollection collection) - { - MethodCalled = "List(IReadOnlyCollection collection)"; - } - public void List(List collection) - { - MethodCalled = "List(List collection)"; - } - public void ListEnumerable(IEnumerable collection) - { - MethodCalled = "List(IEnumerable collection)"; - } - - private static void AssertErrorNotOccurred() - { - using (Py.GIL()) - { - if (Exceptions.ErrorOccurred()) - { - throw new Exception("Error occurred"); - } - } - } - - public static void MethodDateTimeAndTimeSpan(CSharpModel pepe, SomeEnu @someEnu, int integer, double? jose = null, double? pinocho = null) - { - AssertErrorNotOccurred(); - } - public static void MethodDateTimeAndTimeSpan(CSharpModel pepe, DateTime dateTime, SomeEnu someEnu, double? jose = null, double? pinocho = null) - { - AssertErrorNotOccurred(); - } - public static void MethodDateTimeAndTimeSpan(CSharpModel pepe, TimeSpan timeSpan, SomeEnu someEnu, double? jose = null, double? pinocho = null) - { - AssertErrorNotOccurred(); - } - public static void MethodDateTimeAndTimeSpan(CSharpModel pepe, Func func, SomeEnu someEnu, double? jose = null, double? pinocho = null) - { - AssertErrorNotOccurred(); - } - } - - public class TestImplicitConversion - { - public static implicit operator string(TestImplicitConversion symbol) - { - return "implicit to string"; - } - public static implicit operator TestImplicitConversion(string symbol) - { - return new TestImplicitConversion(); - } - } - - public class ErroredImplicitConversion - { - public static implicit operator string(ErroredImplicitConversion symbol) - { - throw new ArgumentException(); - } - public static implicit operator ErroredImplicitConversion(string symbol) - { - throw new ArgumentException(); - } - } - - public class GenericClassBase - where J : class - { - public int Value = 0; - - public void TestNonStaticGenericMethod(GenericClassBase test) - where T : class - { - test.Value = 1; - } - } - - // Used to test that when a generic option is available but the parameter is already typed it doesn't - // match to the wrong one. This is an example of a typed generic parameter - public static void TestGenericMethod(GenericClassBase test) - { - test.Value = 15; - } - - public static void TestGenericMethod(GenericClassBase test) - where T : class - { - test.Value = 1; - } - - // Used in test to verify non-generic is bound and used when generic option is also available - public static void TestGenericMethod(TestGenericClass3 class3) - { - class3.Value = 10; - } - - // Used in test to verify generic binding when converted PyTypes are involved (timedelta -> TimeSpan) - public static void TestGenericMethod(GenericClassBase test, TimeSpan span) - where T : class - { - test.Value = span.Hours; - } - - // Used in test to verify generic binding when defaults are used - public static void TestGenericMethodWithDefault(GenericClassBase test, int value = 25) - where T : class - { - test.Value = value; - } - - // Used in test to verify generic binding when null defaults are used - public static void TestGenericMethodWithNullDefault(GenericClassBase test, Object testObj = null) - where T : class - { - if (testObj == null) - { - test.Value = 10; - } - else - { - test.Value = 20; - } - } - - public class ReferenceClass1 - { } - - public class ReferenceClass2 - { } - - public class ReferenceClass3 - { } - - public class TestGenericClass1 : GenericClassBase - { } - - public class TestGenericClass2 : GenericClassBase - { } - - public class TestGenericClass3 : GenericClassBase - { } - - public class TestGenericClass4 : GenericClassBase - { } - - public class MultipleGenericClassBase - where T : class - where K : class - { - public int Value = 0; - } - - public static void TestMultipleGenericMethod(MultipleGenericClassBase test) - where T : class - where K : class - { - test.Value = 1; - } - - public class TestMultipleGenericClass1 : MultipleGenericClassBase - { } - - public class TestMultipleGenericClass2 : MultipleGenericClassBase - { } - - public static void TestMultipleGenericParamsMethod(GenericClassBase singleGeneric, MultipleGenericClassBase doubleGeneric) - where T : class - where K : class - { - singleGeneric.Value = 1; - doubleGeneric.Value = 1; - } - - public static void TestMultipleGenericParamsMethod2(GenericClassBase singleGeneric, MultipleGenericClassBase doubleGeneric) - where T : class - where K : class - { - singleGeneric.Value = 1; - doubleGeneric.Value = 1; - } - - public enum SomeEnu - { - A = 1, - B = 2, - } - } -} + using var pyArg = new CSharpClass().ToPython(); + + Assert.DoesNotThrow(() => + { + // We are passing a PyObject and not using the named arguments, + // that overload must be called without converting the PyObject to CSharpClass + pyInstance.InvokeMethod("Method", pyArg); + }); + + Assert.AreEqual("Overload 4", instance.CalledMethodMessage); + + Assert.IsFalse(Exceptions.ErrorOccurred()); + } + + [Test] + public void BindsConstructorToSnakeCasedArgumentsVersion([Values] bool useCamelCase, [Values] bool passOptionalArgument) + { + using var _ = Py.GIL(); + + var argument1Name = useCamelCase ? "someArgument" : "some_argument"; + var argument2Name = useCamelCase ? "anotherArgument" : "another_argument"; + var argument2Code = passOptionalArgument ? $", {argument2Name}=\"another argument value\"" : ""; + + var module = PyModule.FromString("BindsConstructorToSnakeCasedArgumentsVersion", @$" +from clr import AddReference +AddReference(""System"") +from Python.EmbeddingTest import * + +def create_instance(): + return TestMethodBinder.CSharpModel({argument1Name}=1{argument2Code}) +"); + var exception = Assert.Throws(() => module.GetAttr("create_instance").Invoke()); + var sourceException = exception.InnerException; + Assert.IsInstanceOf(sourceException); + + 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); + } + + [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) + { + return test.Month; + } + + public class CSharpModel + { + public static string MethodCalled { get; set; } + public static dynamic ProvidedArgument; + public List SomeList { get; set; } + + public CSharpModel() + { + SomeList = new List + { + new TestImplicitConversion() + }; + } + + public CSharpModel(int someArgument, string anotherArgument = "another argument default value") + { + throw new NotImplementedException($"Constructor with arguments: someArgument={someArgument}. anotherArgument=\"{anotherArgument}\""); + } + + public void TestList(List conversions) + { + if (!conversions.Any()) + { + throw new ArgumentException("We expect at least an instance"); + } + } + + public void TestEnumerable(IEnumerable conversions) + { + if (!conversions.Any()) + { + throw new ArgumentException("We expect at least an instance"); + } + } + + public bool SomeMethod() + { + return true; + } + + public virtual string OnlyClass(TestImplicitConversion data) + { + return "OnlyClass impl"; + } + + public virtual string OnlyString(string data) + { + return "OnlyString impl: " + data; + } + + public virtual string InvokeModel(string data) + { + return "string impl: " + data; + } + + public virtual string InvokeModel(TestImplicitConversion data) + { + return "TestImplicitConversion impl"; + } + + public void NumericalArgumentMethod(int value) + { + ProvidedArgument = value; + } + public void NumericalArgumentMethod(float value) + { + ProvidedArgument = value; + } + public void NumericalArgumentMethod(double value) + { + ProvidedArgument = value; + } + public void NumericalArgumentMethod(decimal value) + { + ProvidedArgument = value; + } + public void EnumerableKeyValuePair(IEnumerable> value) + { + ProvidedArgument = value; + } + public void ListKeyValuePair(List> value) + { + ProvidedArgument = value; + } + + public void MethodWithParams(decimal value, params string[] argument) + { + + } + + public void ListReadOnlyCollection(IReadOnlyCollection collection) + { + MethodCalled = "List(IReadOnlyCollection collection)"; + } + public void List(List collection) + { + MethodCalled = "List(List collection)"; + } + public void ListEnumerable(IEnumerable collection) + { + MethodCalled = "List(IEnumerable collection)"; + } + + private static void AssertErrorNotOccurred() + { + using (Py.GIL()) + { + if (Exceptions.ErrorOccurred()) + { + throw new Exception("Error occurred"); + } + } + } + + public static void MethodDateTimeAndTimeSpan(CSharpModel pepe, SomeEnu @someEnu, int integer, double? jose = null, double? pinocho = null) + { + AssertErrorNotOccurred(); + } + public static void MethodDateTimeAndTimeSpan(CSharpModel pepe, DateTime dateTime, SomeEnu someEnu, double? jose = null, double? pinocho = null) + { + AssertErrorNotOccurred(); + } + public static void MethodDateTimeAndTimeSpan(CSharpModel pepe, TimeSpan timeSpan, SomeEnu someEnu, double? jose = null, double? pinocho = null) + { + AssertErrorNotOccurred(); + } + public static void MethodDateTimeAndTimeSpan(CSharpModel pepe, Func func, SomeEnu someEnu, double? jose = null, double? pinocho = null) + { + AssertErrorNotOccurred(); + } + } + + public class TestImplicitConversion + { + public static implicit operator string(TestImplicitConversion symbol) + { + return "implicit to string"; + } + public static implicit operator TestImplicitConversion(string symbol) + { + return new TestImplicitConversion(); + } + } + + public class ErroredImplicitConversion + { + public static implicit operator string(ErroredImplicitConversion symbol) + { + throw new ArgumentException(); + } + public static implicit operator ErroredImplicitConversion(string symbol) + { + throw new ArgumentException(); + } + } + + public class GenericClassBase + where J : class + { + public int Value = 0; + + public void TestNonStaticGenericMethod(GenericClassBase test) + where T : class + { + test.Value = 1; + } + } + + // Used to test that when a generic option is available but the parameter is already typed it doesn't + // match to the wrong one. This is an example of a typed generic parameter + public static void TestGenericMethod(GenericClassBase test) + { + test.Value = 15; + } + + public static void TestGenericMethod(GenericClassBase test) + where T : class + { + test.Value = 1; + } + + // Used in test to verify non-generic is bound and used when generic option is also available + public static void TestGenericMethod(TestGenericClass3 class3) + { + class3.Value = 10; + } + + // Used in test to verify generic binding when converted PyTypes are involved (timedelta -> TimeSpan) + public static void TestGenericMethod(GenericClassBase test, TimeSpan span) + where T : class + { + test.Value = span.Hours; + } + + // Used in test to verify generic binding when defaults are used + public static void TestGenericMethodWithDefault(GenericClassBase test, int value = 25) + where T : class + { + test.Value = value; + } + + // Used in test to verify generic binding when null defaults are used + public static void TestGenericMethodWithNullDefault(GenericClassBase test, Object testObj = null) + where T : class + { + if (testObj == null) + { + test.Value = 10; + } + else + { + test.Value = 20; + } + } + + public class ReferenceClass1 + { } + + public class ReferenceClass2 + { } + + public class ReferenceClass3 + { } + + public class TestGenericClass1 : GenericClassBase + { } + + public class TestGenericClass2 : GenericClassBase + { } + + public class TestGenericClass3 : GenericClassBase + { } + + public class TestGenericClass4 : GenericClassBase + { } + + public class MultipleGenericClassBase + where T : class + where K : class + { + public int Value = 0; + } + + public static void TestMultipleGenericMethod(MultipleGenericClassBase test) + where T : class + where K : class + { + test.Value = 1; + } + + public class TestMultipleGenericClass1 : MultipleGenericClassBase + { } + + public class TestMultipleGenericClass2 : MultipleGenericClassBase + { } + + public static void TestMultipleGenericParamsMethod(GenericClassBase singleGeneric, MultipleGenericClassBase doubleGeneric) + where T : class + where K : class + { + singleGeneric.Value = 1; + doubleGeneric.Value = 1; + } + + public static void TestMultipleGenericParamsMethod2(GenericClassBase singleGeneric, MultipleGenericClassBase doubleGeneric) + where T : class + where K : class + { + singleGeneric.Value = 1; + doubleGeneric.Value = 1; + } + + public enum SomeEnu + { + A = 1, + B = 2, + } + } +} diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index f598da499..bd5fe1ad7 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -1,1151 +1,1203 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using System.Reflection; -using System.Text; - -namespace Python.Runtime -{ - /// - /// A MethodBinder encapsulates information about a (possibly overloaded) - /// managed method, and is responsible for selecting the right method given - /// a set of Python arguments. This is also used as a base class for the - /// ConstructorBinder, a minor variation used to invoke constructors. - /// - [Serializable] - internal class MethodBinder - { - [NonSerialized] - private List list; - [NonSerialized] - private static Dictionary _resolvedGenericsCache = new(); - public const bool DefaultAllowThreads = true; - public bool allow_threads = DefaultAllowThreads; - public bool init = false; - - internal MethodBinder(List list) - { - this.list = list; - } - - internal MethodBinder() - { - list = new List(); - } - - internal MethodBinder(MethodInfo mi) - { - list = new List { new MethodInformation(mi, true) }; - } - - public int Count - { - get { return list.Count; } - } - - internal void AddMethod(MethodBase m, bool isOriginal) - { - // we added a new method so we have to re sort the method list - init = false; - list.Add(new MethodInformation(m, isOriginal)); - } - - /// - /// Given a sequence of MethodInfo and a sequence of types, return the - /// MethodInfo that matches the signature represented by those types. - /// - internal static MethodBase? MatchSignature(MethodBase[] mi, Type[] tp) - { - if (tp == null) - { - return null; - } - int count = tp.Length; - foreach (MethodBase t in mi) - { - ParameterInfo[] pi = t.GetParameters(); - if (pi.Length != count) - { - continue; - } - for (var n = 0; n < pi.Length; n++) - { - if (tp[n] != pi[n].ParameterType) - { - break; - } - if (n == pi.Length - 1) - { - return t; - } - } - } - return null; - } - - /// - /// Given a sequence of MethodInfo and a sequence of type parameters, - /// return the MethodInfo that represents the matching closed generic. - /// - internal static List MatchParameters(MethodBinder binder, Type[] tp) - { - if (tp == null) - { - return null; - } - int count = tp.Length; - var result = new List(count); - foreach (var methodInformation in binder.list) - { - var t = methodInformation.MethodBase; - if (!t.IsGenericMethodDefinition) - { - continue; - } - Type[] args = t.GetGenericArguments(); - if (args.Length != count) - { - continue; - } - try - { - // MakeGenericMethod can throw ArgumentException if the type parameters do not obey the constraints. - MethodInfo method = ((MethodInfo)t).MakeGenericMethod(tp); - Exceptions.Clear(); - result.Add(new MethodInformation(method, methodInformation.IsOriginal)); - } - catch (ArgumentException e) - { - Exceptions.SetError(e); - // The error will remain set until cleared by a successful match. - } - } - return result; - } - - // Given a generic method and the argsTypes previously matched with it, - // generate the matching method - internal static MethodInfo ResolveGenericMethod(MethodInfo method, Object[] args) - { - // No need to resolve a method where generics are already assigned - if (!method.ContainsGenericParameters) - { - return method; - } - - bool shouldCache = method.DeclaringType != null; - string key = null; - - // Check our resolved generics cache first - if (shouldCache) - { - key = method.DeclaringType.AssemblyQualifiedName + method.ToString() + string.Join(",", args.Select(x => x?.GetType())); - if (_resolvedGenericsCache.TryGetValue(key, out var cachedMethod)) - { - return cachedMethod; - } - } - - // Get our matching generic types to create our method - var methodGenerics = method.GetGenericArguments().Where(x => x.IsGenericParameter).ToArray(); - var resolvedGenericsTypes = new Type[methodGenerics.Length]; - int resolvedGenerics = 0; - - var parameters = method.GetParameters(); - - // Iterate to length of ArgTypes since default args are plausible - for (int k = 0; k < args.Length; k++) - { - if (args[k] == null) - { - continue; - } - - var argType = args[k].GetType(); - var parameterType = parameters[k].ParameterType; - - // Ignore those without generic params - if (!parameterType.ContainsGenericParameters) - { - continue; - } - - // The parameters generic definition - var paramGenericDefinition = parameterType.GetGenericTypeDefinition(); - - // For the arg that matches this param index, determine the matching type for the generic - var currentType = argType; - while (currentType != null) - { - - // Check the current type for generic type definition - var genericType = currentType.IsGenericType ? currentType.GetGenericTypeDefinition() : null; - - // If the generic type matches our params generic definition, this is our match - // go ahead and match these types to this arg - if (paramGenericDefinition == genericType) - { - - // The matching generic for this method parameter - var paramGenerics = parameterType.GenericTypeArguments; - var argGenericsResolved = currentType.GenericTypeArguments; - - for (int j = 0; j < paramGenerics.Length; j++) - { - - // Get the final matching index for our resolved types array for this params generic - var index = Array.IndexOf(methodGenerics, paramGenerics[j]); - - if (resolvedGenericsTypes[index] == null) - { - // Add it, and increment our count - resolvedGenericsTypes[index] = argGenericsResolved[j]; - resolvedGenerics++; - } - else if (resolvedGenericsTypes[index] != argGenericsResolved[j]) - { - // If we have two resolved types for the same generic we have a problem - throw new ArgumentException("ResolveGenericMethod(): Generic method mismatch on argument types"); - } - } - - break; - } - - // Step up the inheritance tree - currentType = currentType.BaseType; - } - } - - try - { - if (resolvedGenerics != methodGenerics.Length) - { - throw new Exception($"ResolveGenericMethod(): Count of resolved generics {resolvedGenerics} does not match method generic count {methodGenerics.Length}."); - } - - method = method.MakeGenericMethod(resolvedGenericsTypes); - - if (shouldCache) - { - // Add to cache - _resolvedGenericsCache.Add(key, method); - } - } - catch (ArgumentException e) - { - // Will throw argument exception if improperly matched - Exceptions.SetError(e); - } +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Reflection; +using System.Text; + +namespace Python.Runtime +{ + /// + /// A MethodBinder encapsulates information about a (possibly overloaded) + /// managed method, and is responsible for selecting the right method given + /// a set of Python arguments. This is also used as a base class for the + /// ConstructorBinder, a minor variation used to invoke constructors. + /// + [Serializable] + internal class MethodBinder + { + [NonSerialized] + private List list; + [NonSerialized] + private static Dictionary _resolvedGenericsCache = new(); + public const bool DefaultAllowThreads = true; + public bool allow_threads = DefaultAllowThreads; + public bool init = false; + + internal MethodBinder(List list) + { + this.list = list; + } + + internal MethodBinder() + { + list = new List(); + } + + internal MethodBinder(MethodInfo mi) + { + list = new List { new MethodInformation(mi, true) }; + } + + public int Count + { + get { return list.Count; } + } + + internal void AddMethod(MethodBase m, bool isOriginal) + { + // we added a new method so we have to re sort the method list + init = false; + list.Add(new MethodInformation(m, isOriginal)); + } + + /// + /// Given a sequence of MethodInfo and a sequence of types, return the + /// MethodInfo that matches the signature represented by those types. + /// + internal static MethodBase? MatchSignature(MethodBase[] mi, Type[] tp) + { + if (tp == null) + { + return null; + } + int count = tp.Length; + foreach (MethodBase t in mi) + { + ParameterInfo[] pi = t.GetParameters(); + if (pi.Length != count) + { + continue; + } + for (var n = 0; n < pi.Length; n++) + { + if (tp[n] != pi[n].ParameterType) + { + break; + } + if (n == pi.Length - 1) + { + return t; + } + } + } + return null; + } + + /// + /// Given a sequence of MethodInfo and a sequence of type parameters, + /// return the MethodInfo that represents the matching closed generic. + /// + internal static List MatchParameters(MethodBinder binder, Type[] tp) + { + if (tp == null) + { + return null; + } + int count = tp.Length; + var result = new List(count); + foreach (var methodInformation in binder.list) + { + var t = methodInformation.MethodBase; + if (!t.IsGenericMethodDefinition) + { + continue; + } + Type[] args = t.GetGenericArguments(); + if (args.Length != count) + { + continue; + } + try + { + // MakeGenericMethod can throw ArgumentException if the type parameters do not obey the constraints. + MethodInfo method = ((MethodInfo)t).MakeGenericMethod(tp); + Exceptions.Clear(); + result.Add(new MethodInformation(method, methodInformation.IsOriginal)); + } + catch (ArgumentException e) + { + Exceptions.SetError(e); + // The error will remain set until cleared by a successful match. + } + } + return result; + } + + // Given a generic method and the argsTypes previously matched with it, + // generate the matching method + internal static MethodInfo ResolveGenericMethod(MethodInfo method, Object[] args) + { + // No need to resolve a method where generics are already assigned + if (!method.ContainsGenericParameters) + { + return method; + } + + bool shouldCache = method.DeclaringType != null; + string key = null; + + // Check our resolved generics cache first + if (shouldCache) + { + key = method.DeclaringType.AssemblyQualifiedName + method.ToString() + string.Join(",", args.Select(x => x?.GetType())); + if (_resolvedGenericsCache.TryGetValue(key, out var cachedMethod)) + { + return cachedMethod; + } + } + + // Get our matching generic types to create our method + var methodGenerics = method.GetGenericArguments().Where(x => x.IsGenericParameter).ToArray(); + var resolvedGenericsTypes = new Type[methodGenerics.Length]; + int resolvedGenerics = 0; + + var parameters = method.GetParameters(); + + // Iterate to length of ArgTypes since default args are plausible + for (int k = 0; k < args.Length; k++) + { + if (args[k] == null) + { + continue; + } + + var argType = args[k].GetType(); + var parameterType = parameters[k].ParameterType; + + // Ignore those without generic params + if (!parameterType.ContainsGenericParameters) + { + continue; + } + + // The parameters generic definition + var paramGenericDefinition = parameterType.GetGenericTypeDefinition(); + + // For the arg that matches this param index, determine the matching type for the generic + var currentType = argType; + while (currentType != null) + { + + // Check the current type for generic type definition + var genericType = currentType.IsGenericType ? currentType.GetGenericTypeDefinition() : null; + + // If the generic type matches our params generic definition, this is our match + // go ahead and match these types to this arg + if (paramGenericDefinition == genericType) + { + + // The matching generic for this method parameter + var paramGenerics = parameterType.GenericTypeArguments; + var argGenericsResolved = currentType.GenericTypeArguments; + + for (int j = 0; j < paramGenerics.Length; j++) + { + + // Get the final matching index for our resolved types array for this params generic + var index = Array.IndexOf(methodGenerics, paramGenerics[j]); + + if (resolvedGenericsTypes[index] == null) + { + // Add it, and increment our count + resolvedGenericsTypes[index] = argGenericsResolved[j]; + resolvedGenerics++; + } + else if (resolvedGenericsTypes[index] != argGenericsResolved[j]) + { + // If we have two resolved types for the same generic we have a problem + throw new ArgumentException("ResolveGenericMethod(): Generic method mismatch on argument types"); + } + } + + break; + } + + // Step up the inheritance tree + currentType = currentType.BaseType; + } + } + + try + { + if (resolvedGenerics != methodGenerics.Length) + { + throw new Exception($"ResolveGenericMethod(): Count of resolved generics {resolvedGenerics} does not match method generic count {methodGenerics.Length}."); + } + + method = method.MakeGenericMethod(resolvedGenericsTypes); + + if (shouldCache) + { + // Add to cache + _resolvedGenericsCache.Add(key, method); + } + } + catch (ArgumentException e) + { + // Will throw argument exception if improperly matched + Exceptions.SetError(e); + } + + return method; + } + + + /// + /// Given a sequence of MethodInfo and two sequences of type parameters, + /// return the MethodInfo that matches the signature and the closed generic. + /// + internal static MethodInfo MatchSignatureAndParameters(MethodBase[] mi, Type[] genericTp, Type[] sigTp) + { + if (genericTp == null || sigTp == null) + { + return null; + } + int genericCount = genericTp.Length; + int signatureCount = sigTp.Length; + foreach (MethodInfo t in mi) + { + if (!t.IsGenericMethodDefinition) + { + continue; + } + Type[] genericArgs = t.GetGenericArguments(); + if (genericArgs.Length != genericCount) + { + continue; + } + ParameterInfo[] pi = t.GetParameters(); + if (pi.Length != signatureCount) + { + continue; + } + for (var n = 0; n < pi.Length; n++) + { + if (sigTp[n] != pi[n].ParameterType) + { + break; + } + if (n == pi.Length - 1) + { + MethodInfo match = t; + if (match.IsGenericMethodDefinition) + { + // FIXME: typeArgs not used + Type[] typeArgs = match.GetGenericArguments(); + return match.MakeGenericMethod(genericTp); + } + return match; + } + } + } + return null; + } + + + /// + /// Return the array of MethodInfo for this method. The result array + /// is arranged in order of precedence (done lazily to avoid doing it + /// at all for methods that are never called). + /// + internal List GetMethods() + { + if (!init) + { + // I'm sure this could be made more efficient. + list.Sort(new MethodSorter()); + init = true; + } + return list; + } + + /// + /// Precedence algorithm largely lifted from Jython - the concerns are + /// generally the same so we'll start with this and tweak as necessary. + /// + /// + /// Based from Jython `org.python.core.ReflectedArgs.precedence` + /// See: https://github.com/jythontools/jython/blob/master/src/org/python/core/ReflectedArgs.java#L192 + /// + private static int GetPrecedence(MethodInformation methodInformation) + { + ParameterInfo[] pi = methodInformation.ParameterInfo; + var mi = methodInformation.MethodBase; + int val = mi.IsStatic ? 3000 : 0; + int num = pi.Length; - return method; + var isOperatorMethod = OperatorMethod.IsOperatorMethod(methodInformation.MethodBase); + + val += mi.IsGenericMethod ? 1 : 0; + for (var i = 0; i < num; i++) + { + val += ArgPrecedence(pi[i].ParameterType, isOperatorMethod); + } + + var info = mi as MethodInfo; + if (info != null) + { + val += ArgPrecedence(info.ReturnType, isOperatorMethod); + if (mi.DeclaringType == mi.ReflectedType) + { + val += methodInformation.IsOriginal ? 0 : 300000; + } + else + { + val += methodInformation.IsOriginal ? 2000 : 400000; + } + } + + return val; } - /// - /// Given a sequence of MethodInfo and two sequences of type parameters, - /// return the MethodInfo that matches the signature and the closed generic. + /// Gets the precedence of a method's arguments, considering only those arguments that have been matched, + /// that is, those that are not default values. /// - internal static MethodInfo MatchSignatureAndParameters(MethodBase[] mi, Type[] genericTp, Type[] sigTp) + private static int GetMatchedArgumentsPrecedence(MethodInformation method, int matchedPositionalArgsCount, IEnumerable matchedKwargsNames) { - if (genericTp == null || sigTp == null) + var isOperatorMethod = OperatorMethod.IsOperatorMethod(method.MethodBase); + var pi = method.ParameterInfo; + var val = 0; + for (var i = 0; i < pi.Length; i++) { - return null; - } - int genericCount = genericTp.Length; - int signatureCount = sigTp.Length; - foreach (MethodInfo t in mi) - { - if (!t.IsGenericMethodDefinition) - { - continue; - } - Type[] genericArgs = t.GetGenericArguments(); - if (genericArgs.Length != genericCount) - { - continue; - } - ParameterInfo[] pi = t.GetParameters(); - if (pi.Length != signatureCount) + if (i < matchedPositionalArgsCount || matchedKwargsNames.Contains(pi[i].Name)) { - continue; - } - for (var n = 0; n < pi.Length; n++) - { - if (sigTp[n] != pi[n].ParameterType) - { - break; - } - if (n == pi.Length - 1) - { - MethodInfo match = t; - if (match.IsGenericMethodDefinition) - { - // FIXME: typeArgs not used - Type[] typeArgs = match.GetGenericArguments(); - return match.MakeGenericMethod(genericTp); - } - return match; - } + val += ArgPrecedence(pi[i].ParameterType, isOperatorMethod); } } - return null; - } - - - /// - /// Return the array of MethodInfo for this method. The result array - /// is arranged in order of precedence (done lazily to avoid doing it - /// at all for methods that are never called). - /// - internal List GetMethods() - { - if (!init) - { - // I'm sure this could be made more efficient. - list.Sort(new MethodSorter()); - init = true; - } - return list; - } - - /// - /// Precedence algorithm largely lifted from Jython - the concerns are - /// generally the same so we'll start with this and tweak as necessary. - /// - /// - /// Based from Jython `org.python.core.ReflectedArgs.precedence` - /// See: https://github.com/jythontools/jython/blob/master/src/org/python/core/ReflectedArgs.java#L192 - /// - private static int GetPrecedence(MethodInformation methodInformation) - { - ParameterInfo[] pi = methodInformation.ParameterInfo; - var mi = methodInformation.MethodBase; - int val = mi.IsStatic ? 3000 : 0; - int num = pi.Length; - - val += mi.IsGenericMethod ? 1 : 0; - for (var i = 0; i < num; i++) - { - val += ArgPrecedence(pi[i].ParameterType, methodInformation); - } + var mi = method.MethodBase; var info = mi as MethodInfo; if (info != null) { - val += ArgPrecedence(info.ReturnType, methodInformation); - if (mi.DeclaringType == mi.ReflectedType) - { - val += methodInformation.IsOriginal ? 0 : 300000; - } - else - { - val += methodInformation.IsOriginal ? 2000 : 400000; - } + val += ArgPrecedence(info.ReturnType, isOperatorMethod); } - return val; - } - - /// - /// Return a precedence value for a particular Type object. - /// - internal static int ArgPrecedence(Type t, MethodInformation mi) - { - Type objectType = typeof(object); - if (t == objectType) - { - return 3000; - } - - if (t.IsAssignableFrom(typeof(PyObject)) && !OperatorMethod.IsOperatorMethod(mi.MethodBase)) - { - 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) - { - case TypeCode.Object: - return 1; - - // we place higher precision methods at the top - case TypeCode.Decimal: - return 2; - case TypeCode.Double: - return 3; - case TypeCode.Single: - return 4; - - case TypeCode.Int64: - return 21; - case TypeCode.Int32: - return 22; - case TypeCode.Int16: - return 23; - case TypeCode.UInt64: - return 24; - case TypeCode.UInt32: - return 25; - case TypeCode.UInt16: - return 26; - case TypeCode.Char: - return 27; - case TypeCode.Byte: - return 28; - case TypeCode.SByte: - return 29; - - case TypeCode.String: - return 30; - - case TypeCode.Boolean: - return 40; - } - - return 2000; - } - - /// - /// Bind the given Python instance and arguments to a particular method - /// overload and return a structure that contains the converted Python - /// instance, converted arguments and the correct method to call. - /// - internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw) - { - return Bind(inst, args, kw, null); - } - - internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase info) - { - // If we have KWArgs create dictionary and collect them - Dictionary kwArgDict = null; - if (kw != null) - { - 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); - for (int i = 0; i < pyKwArgsCount; ++i) - { - var keyStr = Runtime.GetManagedString(Runtime.PyList_GetItem(keylist.Borrow(), i)); - BorrowedReference value = Runtime.PyList_GetItem(valueList.Borrow(), i); - kwArgDict[keyStr!] = new PyObject(value); - } - } - var hasNamedArgs = kwArgDict != null && kwArgDict.Count > 0; - - // Fetch our methods we are going to attempt to match and bind too. - var methods = info == null ? GetMethods() + } + + /// + /// Return a precedence value for a particular Type object. + /// + internal static int ArgPrecedence(Type t, bool isOperatorMethod) + { + Type objectType = typeof(object); + if (t == objectType) + { + return 3000; + } + + if (t.IsAssignableFrom(typeof(PyObject)) && !isOperatorMethod) + { + return -3000; + } + + if (t.IsArray) + { + Type e = t.GetElementType(); + if (e == objectType) + { + return 2500; + } + return 100 + ArgPrecedence(e, isOperatorMethod); + } + + TypeCode tc = Type.GetTypeCode(t); + // TODO: Clean up + switch (tc) + { + case TypeCode.Object: + return 1; + + // we place higher precision methods at the top + case TypeCode.Decimal: + return 2; + case TypeCode.Double: + return 3; + case TypeCode.Single: + return 4; + + case TypeCode.Int64: + return 21; + case TypeCode.Int32: + return 22; + case TypeCode.Int16: + return 23; + case TypeCode.UInt64: + return 24; + case TypeCode.UInt32: + return 25; + case TypeCode.UInt16: + return 26; + case TypeCode.Char: + return 27; + case TypeCode.Byte: + return 28; + case TypeCode.SByte: + return 29; + + case TypeCode.String: + return 30; + + case TypeCode.Boolean: + return 40; + } + + return 2000; + } + + /// + /// Bind the given Python instance and arguments to a particular method + /// overload and return a structure that contains the converted Python + /// instance, converted arguments and the correct method to call. + /// + internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw) + { + return Bind(inst, args, kw, null); + } + + internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase info) + { + // If we have KWArgs create dictionary and collect them + Dictionary kwArgDict = null; + if (kw != null) + { + 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); + for (int i = 0; i < pyKwArgsCount; ++i) + { + var keyStr = Runtime.GetManagedString(Runtime.PyList_GetItem(keylist.Borrow(), i)); + BorrowedReference value = Runtime.PyList_GetItem(valueList.Borrow(), i); + kwArgDict[keyStr!] = new PyObject(value); + } + } + var hasNamedArgs = kwArgDict != null && kwArgDict.Count > 0; + + // Fetch our methods we are going to attempt to match and bind too. + var methods = info == null ? GetMethods() : new List(1) { new MethodInformation(info, true) }; - var matches = new List(methods.Count); - List matchesUsingImplicitConversion = null; - - for (var i = 0; i < methods.Count; i++) + if (methods.Any(m => m.MethodBase.Name.StartsWith("History"))) { - var methodInformation = methods[i]; - // Relevant method variables - var mi = methodInformation.MethodBase; - var pi = methodInformation.ParameterInfo; - // Avoid accessing the parameter names property unless necessary - var paramNames = hasNamedArgs ? methodInformation.ParameterNames : Array.Empty(); - int pyArgCount = (int)Runtime.PyTuple_Size(args); - // Special case for operators - bool isOperator = OperatorMethod.IsOperatorMethod(mi); - // Binary operator methods will have 2 CLR args but only one Python arg - // (unary operators will have 1 less each), since Python operator methods are bound. - isOperator = isOperator && pyArgCount == pi.Length - 1; - bool isReverse = isOperator && OperatorMethod.IsReverse((MethodInfo)mi); // Only cast if isOperator. - if (isReverse && OperatorMethod.IsComparisonOp((MethodInfo)mi)) - continue; // Comparison operators in Python have no reverse mode. - // Preprocessing pi to remove either the first or second argument. - if (isOperator && !isReverse) - { - // The first Python arg is the right operand, while the bound instance is the left. - // We need to skip the first (left operand) CLR argument. - pi = pi.Skip(1).ToArray(); - } - else if (isOperator && isReverse) - { - // The first Python arg is the left operand. - // We need to take the first CLR argument. - pi = pi.Take(1).ToArray(); - } - - // Must be done after IsOperator section - int clrArgCount = pi.Length; - - if (CheckMethodArgumentsMatch(clrArgCount, - pyArgCount, - kwArgDict, - pi, - paramNames, - out bool paramsArray, - out ArrayList defaultArgList)) - { - var outs = 0; - var margs = new object[clrArgCount]; - - int paramsArrayIndex = paramsArray ? pi.Length - 1 : -1; // -1 indicates no paramsArray - var usedImplicitConversion = false; - var kwargsMatched = 0; - - // Conversion loop for each parameter - for (int paramIndex = 0; paramIndex < clrArgCount; paramIndex++) - { - PyObject tempPyObject = null; - BorrowedReference op = null; // Python object to be converted; not yet set - var parameter = pi[paramIndex]; // Clr parameter we are targeting - object arg; // Python -> Clr argument - - // Check positional arguments first and then check for named arguments and optional values - if (paramIndex >= pyArgCount) - { - var hasNamedParam = kwArgDict == null ? false : kwArgDict.TryGetValue(paramNames[paramIndex], out tempPyObject); - - // All positional arguments have been used: - // Check our KWargs for this parameter - if (hasNamedParam) - { - kwargsMatched++; - if (tempPyObject != null) - { - op = tempPyObject; - } - } - else if (parameter.IsOptional && !(hasNamedParam || (paramsArray && paramIndex == paramsArrayIndex))) - { - if (defaultArgList != null) - { - margs[paramIndex] = defaultArgList[paramIndex - pyArgCount]; - } - - continue; - } - } - - NewReference tempObject = default; - - // At this point, if op is IntPtr.Zero we don't have a KWArg and are not using default - if (op == null) - { - // If we have reached the paramIndex - if (paramsArrayIndex == paramIndex) - { - op = HandleParamsArray(args, paramsArrayIndex, pyArgCount, out tempObject); - } - else - { - op = Runtime.PyTuple_GetItem(args, paramIndex); - } - } - - // this logic below handles cases when multiple overloading methods - // are ambiguous, hence comparison between Python and CLR types - // is necessary - Type clrtype = null; - NewReference pyoptype = default; - if (methods.Count > 1) - { - pyoptype = Runtime.PyObject_Type(op); - Exceptions.Clear(); - if (!pyoptype.IsNull()) - { - clrtype = Converter.GetTypeByAlias(pyoptype.Borrow()); - } - pyoptype.Dispose(); - } - - - if (clrtype != null) - { - var typematch = false; - - if ((parameter.ParameterType != typeof(object)) && (parameter.ParameterType != clrtype)) - { - var pytype = Converter.GetPythonTypeByAlias(parameter.ParameterType); - pyoptype = Runtime.PyObject_Type(op); - Exceptions.Clear(); - if (!pyoptype.IsNull()) - { - if (pytype != pyoptype.Borrow()) - { - typematch = false; - } - else - { - typematch = true; - clrtype = parameter.ParameterType; - } - } - if (!typematch) - { - // this takes care of nullables - var underlyingType = Nullable.GetUnderlyingType(parameter.ParameterType); - if (underlyingType == null) - { - underlyingType = parameter.ParameterType; - } - // this takes care of enum values - TypeCode argtypecode = Type.GetTypeCode(underlyingType); - TypeCode paramtypecode = Type.GetTypeCode(clrtype); - if (argtypecode == paramtypecode) - { - typematch = true; - clrtype = parameter.ParameterType; - } - // we won't take matches using implicit conversions if there is already a match - // not using implicit conversions - else if (matches.Count == 0) - { - // accepts non-decimal numbers in decimal parameters - if (underlyingType == typeof(decimal)) - { - clrtype = parameter.ParameterType; - usedImplicitConversion |= typematch = Converter.ToManaged(op, clrtype, out arg, false); - } - if (!typematch) - { - // this takes care of implicit conversions - var opImplicit = parameter.ParameterType.GetMethod("op_Implicit", new[] { clrtype }); - if (opImplicit != null) - { - usedImplicitConversion |= typematch = opImplicit.ReturnType == parameter.ParameterType; - clrtype = parameter.ParameterType; - } - } - } - } - pyoptype.Dispose(); - if (!typematch) - { - tempObject.Dispose(); - margs = null; - break; - } - } - else - { - clrtype = parameter.ParameterType; - } - } - else - { - clrtype = parameter.ParameterType; - } - - if (parameter.IsOut || clrtype.IsByRef) - { - outs++; - } - - if (!Converter.ToManaged(op, clrtype, out arg, false)) - { - tempObject.Dispose(); - margs = null; - break; - } - tempObject.Dispose(); - - margs[paramIndex] = arg; - - } - - if (margs == null) - { - continue; - } - - if (isOperator) - { - if (inst != null) - { - if (ManagedType.GetManagedObject(inst) is CLRObject co) - { - bool isUnary = pyArgCount == 0; - // Postprocessing to extend margs. - var margsTemp = isUnary ? new object[1] : new object[2]; - // If reverse, the bound instance is the right operand. - int boundOperandIndex = isReverse ? 1 : 0; - // If reverse, the passed instance is the left operand. - int passedOperandIndex = isReverse ? 0 : 1; - margsTemp[boundOperandIndex] = co.inst; - if (!isUnary) - { - margsTemp[passedOperandIndex] = margs[0]; - } - margs = margsTemp; - } - else continue; - } - } - - var match = new MatchedMethod(kwargsMatched, margs, outs, mi); - if (usedImplicitConversion) - { - if (matchesUsingImplicitConversion == null) - { - matchesUsingImplicitConversion = new List(); - } - matchesUsingImplicitConversion.Add(match); - } - else - { - matches.Add(match); - // We don't need the matches using implicit conversion anymore, we can free the memory - matchesUsingImplicitConversion = null; - } - } } - if (matches.Count > 0 || (matchesUsingImplicitConversion != null && matchesUsingImplicitConversion.Count > 0)) + int pyArgCount = (int)Runtime.PyTuple_Size(args); + var matches = new List(methods.Count); + List matchesUsingImplicitConversion = null; + + for (var i = 0; i < methods.Count; i++) + { + var methodInformation = methods[i]; + // Relevant method variables + var mi = methodInformation.MethodBase; + var pi = methodInformation.ParameterInfo; + // Avoid accessing the parameter names property unless necessary + var paramNames = hasNamedArgs ? methodInformation.ParameterNames : Array.Empty(); + + // Special case for operators + bool isOperator = OperatorMethod.IsOperatorMethod(mi); + // Binary operator methods will have 2 CLR args but only one Python arg + // (unary operators will have 1 less each), since Python operator methods are bound. + isOperator = isOperator && pyArgCount == pi.Length - 1; + bool isReverse = isOperator && OperatorMethod.IsReverse((MethodInfo)mi); // Only cast if isOperator. + if (isReverse && OperatorMethod.IsComparisonOp((MethodInfo)mi)) + continue; // Comparison operators in Python have no reverse mode. + // Preprocessing pi to remove either the first or second argument. + if (isOperator && !isReverse) + { + // The first Python arg is the right operand, while the bound instance is the left. + // We need to skip the first (left operand) CLR argument. + pi = pi.Skip(1).ToArray(); + } + else if (isOperator && isReverse) + { + // The first Python arg is the left operand. + // We need to take the first CLR argument. + pi = pi.Take(1).ToArray(); + } + + // Must be done after IsOperator section + int clrArgCount = pi.Length; + + if (CheckMethodArgumentsMatch(clrArgCount, + pyArgCount, + kwArgDict, + pi, + paramNames, + out bool paramsArray, + out ArrayList defaultArgList)) + { + var outs = 0; + var margs = new object[clrArgCount]; + + int paramsArrayIndex = paramsArray ? pi.Length - 1 : -1; // -1 indicates no paramsArray + var usedImplicitConversion = false; + var kwargsMatched = 0; + + // Conversion loop for each parameter + for (int paramIndex = 0; paramIndex < clrArgCount; paramIndex++) + { + PyObject tempPyObject = null; + BorrowedReference op = null; // Python object to be converted; not yet set + var parameter = pi[paramIndex]; // Clr parameter we are targeting + object arg; // Python -> Clr argument + + // Check positional arguments first and then check for named arguments and optional values + if (paramIndex >= pyArgCount) + { + var hasNamedParam = kwArgDict == null ? false : kwArgDict.TryGetValue(paramNames[paramIndex], out tempPyObject); + + // All positional arguments have been used: + // Check our KWargs for this parameter + if (hasNamedParam) + { + kwargsMatched++; + if (tempPyObject != null) + { + op = tempPyObject; + } + } + else if (parameter.IsOptional && !(hasNamedParam || (paramsArray && paramIndex == paramsArrayIndex))) + { + if (defaultArgList != null) + { + margs[paramIndex] = defaultArgList[paramIndex - pyArgCount]; + } + + continue; + } + } + + NewReference tempObject = default; + + // At this point, if op is IntPtr.Zero we don't have a KWArg and are not using default + if (op == null) + { + // If we have reached the paramIndex + if (paramsArrayIndex == paramIndex) + { + op = HandleParamsArray(args, paramsArrayIndex, pyArgCount, out tempObject); + } + else + { + op = Runtime.PyTuple_GetItem(args, paramIndex); + } + } + + // this logic below handles cases when multiple overloading methods + // are ambiguous, hence comparison between Python and CLR types + // is necessary + Type clrtype = null; + NewReference pyoptype = default; + if (methods.Count > 1) + { + pyoptype = Runtime.PyObject_Type(op); + Exceptions.Clear(); + if (!pyoptype.IsNull()) + { + clrtype = Converter.GetTypeByAlias(pyoptype.Borrow()); + } + pyoptype.Dispose(); + } + + + if (clrtype != null) + { + var typematch = false; + + if ((parameter.ParameterType != typeof(object)) && (parameter.ParameterType != clrtype)) + { + var pytype = Converter.GetPythonTypeByAlias(parameter.ParameterType); + pyoptype = Runtime.PyObject_Type(op); + Exceptions.Clear(); + if (!pyoptype.IsNull()) + { + if (pytype != pyoptype.Borrow()) + { + typematch = false; + } + else + { + typematch = true; + clrtype = parameter.ParameterType; + } + } + if (!typematch) + { + // this takes care of nullables + var underlyingType = Nullable.GetUnderlyingType(parameter.ParameterType); + if (underlyingType == null) + { + underlyingType = parameter.ParameterType; + } + // this takes care of enum values + TypeCode argtypecode = Type.GetTypeCode(underlyingType); + TypeCode paramtypecode = Type.GetTypeCode(clrtype); + if (argtypecode == paramtypecode) + { + typematch = true; + clrtype = parameter.ParameterType; + } + // we won't take matches using implicit conversions if there is already a match + // not using implicit conversions + else if (matches.Count == 0) + { + // accepts non-decimal numbers in decimal parameters + if (underlyingType == typeof(decimal)) + { + clrtype = parameter.ParameterType; + usedImplicitConversion |= typematch = Converter.ToManaged(op, clrtype, out arg, false); + } + if (!typematch) + { + // this takes care of implicit conversions + var opImplicit = parameter.ParameterType.GetMethod("op_Implicit", new[] { clrtype }); + if (opImplicit != null) + { + usedImplicitConversion |= typematch = opImplicit.ReturnType == parameter.ParameterType; + clrtype = parameter.ParameterType; + } + } + } + } + pyoptype.Dispose(); + if (!typematch) + { + tempObject.Dispose(); + margs = null; + break; + } + } + else + { + clrtype = parameter.ParameterType; + } + } + else + { + clrtype = parameter.ParameterType; + } + + if (parameter.IsOut || clrtype.IsByRef) + { + outs++; + } + + if (!Converter.ToManaged(op, clrtype, out arg, false)) + { + tempObject.Dispose(); + margs = null; + break; + } + tempObject.Dispose(); + + margs[paramIndex] = arg; + + } + + if (margs == null) + { + continue; + } + + if (isOperator) + { + if (inst != null) + { + if (ManagedType.GetManagedObject(inst) is CLRObject co) + { + bool isUnary = pyArgCount == 0; + // Postprocessing to extend margs. + var margsTemp = isUnary ? new object[1] : new object[2]; + // If reverse, the bound instance is the right operand. + int boundOperandIndex = isReverse ? 1 : 0; + // If reverse, the passed instance is the left operand. + int passedOperandIndex = isReverse ? 0 : 1; + margsTemp[boundOperandIndex] = co.inst; + if (!isUnary) + { + margsTemp[passedOperandIndex] = margs[0]; + } + margs = margsTemp; + } + else continue; + } + } + + var match = new MatchedMethod(kwargsMatched, margs, outs, mi); + if (usedImplicitConversion) + { + if (matchesUsingImplicitConversion == null) + { + matchesUsingImplicitConversion = new List(); + } + matchesUsingImplicitConversion.Add(match); + } + else + { + matches.Add(match); + // We don't need the matches using implicit conversion anymore, we can free the memory + matchesUsingImplicitConversion = null; + } + } + } + + if (matches.Count > 0 || (matchesUsingImplicitConversion != null && matchesUsingImplicitConversion.Count > 0)) { - // We favor matches that do not use implicit conversion - var matchesTouse = matches.Count > 0 ? matches : matchesUsingImplicitConversion; - + // We favor matches that do not use implicit conversion + var matchesTouse = matches.Count > 0 ? matches : matchesUsingImplicitConversion; + // The best match would be the one with the most named arguments matched - var bestMatch = matchesTouse.MaxBy(x => x.KwargsMatched); - var margs = bestMatch.ManagedArgs; - var outs = bestMatch.Outs; - var mi = bestMatch.Method; - - object? target = null; - if (!mi.IsStatic && inst != null) - { - //CLRObject co = (CLRObject)ManagedType.GetManagedObject(inst); - // InvalidCastException: Unable to cast object of type - // 'Python.Runtime.ClassObject' to type 'Python.Runtime.CLRObject' - - // Sanity check: this ensures a graceful exit if someone does - // something intentionally wrong like call a non-static method - // on the class rather than on an instance of the class. - // XXX maybe better to do this before all the other rigmarole. - if (ManagedType.GetManagedObject(inst) is CLRObject co) - { - target = co.inst; - } - else - { - Exceptions.SetError(Exceptions.TypeError, "Invoked a non-static method with an invalid instance"); - return null; - } - } - - // If this match is generic we need to resolve it with our types. - // Store this generic match to be used if no others match - if (mi.IsGenericMethod) - { - mi = ResolveGenericMethod((MethodInfo)mi, margs); - } - - return new Binding(mi, target, margs, outs); - } - - return null; - } - - static BorrowedReference HandleParamsArray(BorrowedReference args, int arrayStart, int pyArgCount, out NewReference tempObject) - { - BorrowedReference op; - tempObject = default; - // for a params method, we may have a sequence or single/multiple items - // here we look to see if the item at the paramIndex is there or not - // and then if it is a sequence itself. - if ((pyArgCount - arrayStart) == 1) - { - // we only have one argument left, so we need to check it - // to see if it is a sequence or a single item - BorrowedReference item = Runtime.PyTuple_GetItem(args, arrayStart); - if (!Runtime.PyString_Check(item) && (Runtime.PySequence_Check(item) || (ManagedType.GetManagedObject(item) as CLRObject)?.inst is IEnumerable)) + var maxKwargsMatched = matchesTouse.Max(x => x.KwargsMatched); + // Don't materialize the enumerable, just enumerate twice if necessary to avoid creating a collection instance. + var bestMatches = matchesTouse.Where(x => x.KwargsMatched == maxKwargsMatched); + var bestMatchesCount = bestMatches.Count(); + + MatchedMethod bestMatch; + // Multiple best matches, we can still resolve the ambiguity because + // some method might take precedence if it received PyObject instances. + // So let's get the best match by the precedence of the actual passed arguments, + // without considering optional arguments without a passed value + if (bestMatchesCount > 1) { - // it's a sequence (and not a string), so we use it as the op - op = item; + bestMatch = bestMatches.MinBy(x => GetMatchedArgumentsPrecedence(methods.First(m => m.MethodBase == x.Method), pyArgCount, + kwArgDict?.Keys ?? Enumerable.Empty())); } else { - tempObject = Runtime.PyTuple_GetSlice(args, arrayStart, pyArgCount); - op = tempObject.Borrow(); - } - } - else - { - tempObject = Runtime.PyTuple_GetSlice(args, arrayStart, pyArgCount); - op = tempObject.Borrow(); - } - return op; - } - - /// - /// This helper method will perform an initial check to determine if we found a matching - /// method based on its parameters count and type - /// - /// - /// We required both the parameters info and the parameters names to perform this check. - /// The CLR method parameters info is required to match the parameters count and type. - /// The names are required to perform an accurate match, since the method can be the snake-cased version. - /// - private bool CheckMethodArgumentsMatch(int clrArgCount, - int pyArgCount, - Dictionary kwargDict, - ParameterInfo[] parameterInfo, - string[] parameterNames, - out bool paramsArray, - out ArrayList defaultArgList) - { - var match = false; - - // Prepare our outputs - defaultArgList = null; - paramsArray = false; - if (parameterInfo.Length > 0) - { - var lastParameterInfo = parameterInfo[parameterInfo.Length - 1]; - if (lastParameterInfo.ParameterType.IsArray) - { - paramsArray = Attribute.IsDefined(lastParameterInfo, typeof(ParamArrayAttribute)); + bestMatch = bestMatches.First(); } - } - - // First if we have anys kwargs, look at the function for matching args - if (kwargDict != null && kwargDict.Count > 0) - { - // If the method doesn't have all of these kw args, it is not a match - // Otherwise just continue on to see if it is a match - if (!kwargDict.All(x => parameterNames.Any(paramName => x.Key == paramName))) - { - return false; - } - } - - // If they have the exact same amount of args they do match - // Must check kwargs because it contains additional args - if (pyArgCount == clrArgCount && (kwargDict == null || kwargDict.Count == 0)) - { - match = true; - } - else if (pyArgCount < clrArgCount) - { - // every parameter past 'pyArgCount' must have either - // a corresponding keyword argument or a default parameter - match = true; - defaultArgList = new ArrayList(); - for (var v = pyArgCount; v < clrArgCount && match; v++) - { - if (kwargDict != null && kwargDict.ContainsKey(parameterNames[v])) - { - // we have a keyword argument for this parameter, - // no need to check for a default parameter, but put a null - // placeholder in defaultArgList - defaultArgList.Add(null); - } - else if (parameterInfo[v].IsOptional) - { - // IsOptional will be true if the parameter has a default value, - // or if the parameter has the [Optional] attribute specified. - if (parameterInfo[v].HasDefaultValue) - { - defaultArgList.Add(parameterInfo[v].DefaultValue); - } - else - { - // [OptionalAttribute] was specified for the parameter. - // See https://stackoverflow.com/questions/3416216/optionalattribute-parameters-default-value - // for rules on determining the value to pass to the parameter - var type = parameterInfo[v].ParameterType; - if (type == typeof(object)) - defaultArgList.Add(Type.Missing); - else if (type.IsValueType) - defaultArgList.Add(Activator.CreateInstance(type)); - else - defaultArgList.Add(null); - } - } - else if (!paramsArray) - { - // If there is no KWArg or Default value, then this isn't a match - match = false; - } - } - } - else if (pyArgCount > clrArgCount && clrArgCount > 0 && paramsArray) - { - // This is a `foo(params object[] bar)` style method - // We will handle the params later - match = true; - } - return match; - } - - internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw) - { - return Invoke(inst, args, kw, null, null); + + var margs = bestMatch.ManagedArgs; + var outs = bestMatch.Outs; + var mi = bestMatch.Method; + + object? target = null; + if (!mi.IsStatic && inst != null) + { + //CLRObject co = (CLRObject)ManagedType.GetManagedObject(inst); + // InvalidCastException: Unable to cast object of type + // 'Python.Runtime.ClassObject' to type 'Python.Runtime.CLRObject' + + // Sanity check: this ensures a graceful exit if someone does + // something intentionally wrong like call a non-static method + // on the class rather than on an instance of the class. + // XXX maybe better to do this before all the other rigmarole. + if (ManagedType.GetManagedObject(inst) is CLRObject co) + { + target = co.inst; + } + else + { + Exceptions.SetError(Exceptions.TypeError, "Invoked a non-static method with an invalid instance"); + return null; + } + } + + // If this match is generic we need to resolve it with our types. + // Store this generic match to be used if no others match + if (mi.IsGenericMethod) + { + mi = ResolveGenericMethod((MethodInfo)mi, margs); + } + + return new Binding(mi, target, margs, outs); + } + + return null; } - internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase info) - { - return Invoke(inst, args, kw, info, null); - } - - internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase info, MethodInfo[] methodinfo) - { - Binding binding = Bind(inst, args, kw, info); - object result; - IntPtr ts = IntPtr.Zero; - - if (binding == null) - { - // If we already have an exception pending, don't create a new one - if (!Exceptions.ErrorOccurred()) - { - var value = new StringBuilder("No method matches given arguments"); - if (methodinfo != null && methodinfo.Length > 0) - { - value.Append($" for {methodinfo[0].Name}"); - } - else if (list.Count > 0) - { - value.Append($" for {list[0].MethodBase.Name}"); - } - - value.Append(": "); - AppendArgumentTypes(to: value, args); - Exceptions.RaiseTypeError(value.ToString()); - } - - return default; - } - - if (allow_threads) - { - ts = PythonEngine.BeginAllowThreads(); - } - - try - { - result = binding.info.Invoke(binding.inst, BindingFlags.Default, null, binding.args, null); - } - catch (Exception e) - { - if (e.InnerException != null) - { - e = e.InnerException; - } - if (allow_threads) - { - PythonEngine.EndAllowThreads(ts); - } - Exceptions.SetError(e); - return default; - } - - if (allow_threads) - { - PythonEngine.EndAllowThreads(ts); - } - - // If there are out parameters, we return a tuple containing - // the result followed by the out parameters. If there is only - // one out parameter and the return type of the method is void, - // we return the out parameter as the result to Python (for - // code compatibility with ironpython). - - var returnType = binding.info.IsConstructor ? typeof(void) : ((MethodInfo)binding.info).ReturnType; - - if (binding.outs > 0) - { - ParameterInfo[] pi = binding.info.GetParameters(); - int c = pi.Length; - var n = 0; - - bool isVoid = returnType == typeof(void); - int tupleSize = binding.outs + (isVoid ? 0 : 1); - using var t = Runtime.PyTuple_New(tupleSize); - if (!isVoid) - { - using var v = Converter.ToPython(result, returnType); - Runtime.PyTuple_SetItem(t.Borrow(), n, v.Steal()); - n++; - } - - for (var i = 0; i < c; i++) - { - Type pt = pi[i].ParameterType; - if (pt.IsByRef) - { - using var v = Converter.ToPython(binding.args[i], pt.GetElementType()); - Runtime.PyTuple_SetItem(t.Borrow(), n, v.Steal()); - n++; - } - } - - if (binding.outs == 1 && returnType == typeof(void)) - { - BorrowedReference item = Runtime.PyTuple_GetItem(t.Borrow(), 0); - return new NewReference(item); - } - - return new NewReference(t.Borrow()); - } - - return Converter.ToPython(result, returnType); - } - - /// - /// Utility class to store the information about a - /// - [Serializable] - internal class MethodInformation - { - private ParameterInfo[] _parameterInfo; - private string[] _parametersNames; - - public MethodBase MethodBase { get; } - - public bool IsOriginal { get; set; } - - public ParameterInfo[] ParameterInfo - { - get - { - _parameterInfo ??= MethodBase.GetParameters(); - return _parameterInfo; - } - } - - public string[] ParameterNames - { - get - { - if (_parametersNames == null) - { - if (IsOriginal) - { - _parametersNames = ParameterInfo.Select(pi => pi.Name).ToArray(); - } - else - { - _parametersNames = ParameterInfo.Select(pi => pi.Name.ToSnakeCase()).ToArray(); - } - } - return _parametersNames; - } - } - - public MethodInformation(MethodBase methodBase, bool isOriginal) - { - MethodBase = methodBase; - IsOriginal = isOriginal; - } - - public override string ToString() - { - return MethodBase.ToString(); - } - } - - /// - /// Utility class to sort method info by parameter type precedence. - /// - private class MethodSorter : IComparer - { - public int Compare(MethodInformation x, MethodInformation y) - { - int p1 = GetPrecedence(x); - int p2 = GetPrecedence(y); - if (p1 < p2) - { - return -1; - } - if (p1 > p2) - { - return 1; - } - return 0; - } - } - - private readonly struct MatchedMethod - { - public int KwargsMatched { get; } - public object?[] ManagedArgs { get; } - public int Outs { get; } - public MethodBase Method { get; } - - public MatchedMethod(int kwargsMatched, object?[] margs, int outs, MethodBase mb) - { - KwargsMatched = kwargsMatched; - ManagedArgs = margs; - Outs = outs; - Method = mb; - } - } - - protected static void AppendArgumentTypes(StringBuilder to, BorrowedReference args) - { - long argCount = Runtime.PyTuple_Size(args); - to.Append("("); - for (nint argIndex = 0; argIndex < argCount; argIndex++) - { - BorrowedReference arg = Runtime.PyTuple_GetItem(args, argIndex); - if (arg != null) - { - BorrowedReference type = Runtime.PyObject_TYPE(arg); - if (type != null) - { - using var description = Runtime.PyObject_Str(type); - if (description.IsNull()) - { - Exceptions.Clear(); - to.Append(Util.BadStr); - } - else - { - to.Append(Runtime.GetManagedString(description.Borrow())); - } - } - } - - if (argIndex + 1 < argCount) - to.Append(", "); - } - to.Append(')'); - } - } - - - /// - /// A Binding is a utility instance that bundles together a MethodInfo - /// representing a method to call, a (possibly null) target instance for - /// the call, and the arguments for the call (all as managed values). - /// - internal class Binding - { - public MethodBase info; - public object[] args; - public object inst; - public int outs; - - internal Binding(MethodBase info, object inst, object[] args, int outs) - { - this.info = info; - this.inst = inst; - this.args = args; - this.outs = outs; - } - } -} + static BorrowedReference HandleParamsArray(BorrowedReference args, int arrayStart, int pyArgCount, out NewReference tempObject) + { + BorrowedReference op; + tempObject = default; + // for a params method, we may have a sequence or single/multiple items + // here we look to see if the item at the paramIndex is there or not + // and then if it is a sequence itself. + if ((pyArgCount - arrayStart) == 1) + { + // we only have one argument left, so we need to check it + // to see if it is a sequence or a single item + BorrowedReference item = Runtime.PyTuple_GetItem(args, arrayStart); + if (!Runtime.PyString_Check(item) && (Runtime.PySequence_Check(item) || (ManagedType.GetManagedObject(item) as CLRObject)?.inst is IEnumerable)) + { + // it's a sequence (and not a string), so we use it as the op + op = item; + } + else + { + tempObject = Runtime.PyTuple_GetSlice(args, arrayStart, pyArgCount); + op = tempObject.Borrow(); + } + } + else + { + tempObject = Runtime.PyTuple_GetSlice(args, arrayStart, pyArgCount); + op = tempObject.Borrow(); + } + return op; + } + + /// + /// This helper method will perform an initial check to determine if we found a matching + /// method based on its parameters count and type + /// + /// + /// We required both the parameters info and the parameters names to perform this check. + /// The CLR method parameters info is required to match the parameters count and type. + /// The names are required to perform an accurate match, since the method can be the snake-cased version. + /// + private bool CheckMethodArgumentsMatch(int clrArgCount, + int pyArgCount, + Dictionary kwargDict, + ParameterInfo[] parameterInfo, + string[] parameterNames, + out bool paramsArray, + out ArrayList defaultArgList) + { + var match = false; + + // Prepare our outputs + defaultArgList = null; + paramsArray = false; + if (parameterInfo.Length > 0) + { + var lastParameterInfo = parameterInfo[parameterInfo.Length - 1]; + if (lastParameterInfo.ParameterType.IsArray) + { + paramsArray = Attribute.IsDefined(lastParameterInfo, typeof(ParamArrayAttribute)); + } + } + + // First if we have anys kwargs, look at the function for matching args + if (kwargDict != null && kwargDict.Count > 0) + { + // If the method doesn't have all of these kw args, it is not a match + // Otherwise just continue on to see if it is a match + if (!kwargDict.All(x => parameterNames.Any(paramName => x.Key == paramName))) + { + return false; + } + } + + // If they have the exact same amount of args they do match + // Must check kwargs because it contains additional args + if (pyArgCount == clrArgCount && (kwargDict == null || kwargDict.Count == 0)) + { + match = true; + } + else if (pyArgCount < clrArgCount) + { + // every parameter past 'pyArgCount' must have either + // a corresponding keyword argument or a default parameter + match = true; + defaultArgList = new ArrayList(); + for (var v = pyArgCount; v < clrArgCount && match; v++) + { + if (kwargDict != null && kwargDict.ContainsKey(parameterNames[v])) + { + // we have a keyword argument for this parameter, + // no need to check for a default parameter, but put a null + // placeholder in defaultArgList + defaultArgList.Add(null); + } + else if (parameterInfo[v].IsOptional) + { + // IsOptional will be true if the parameter has a default value, + // or if the parameter has the [Optional] attribute specified. + if (parameterInfo[v].HasDefaultValue) + { + defaultArgList.Add(parameterInfo[v].DefaultValue); + } + else + { + // [OptionalAttribute] was specified for the parameter. + // See https://stackoverflow.com/questions/3416216/optionalattribute-parameters-default-value + // for rules on determining the value to pass to the parameter + var type = parameterInfo[v].ParameterType; + if (type == typeof(object)) + defaultArgList.Add(Type.Missing); + else if (type.IsValueType) + defaultArgList.Add(Activator.CreateInstance(type)); + else + defaultArgList.Add(null); + } + } + else if (!paramsArray) + { + // If there is no KWArg or Default value, then this isn't a match + match = false; + } + } + } + else if (pyArgCount > clrArgCount && clrArgCount > 0 && paramsArray) + { + // This is a `foo(params object[] bar)` style method + // We will handle the params later + match = true; + } + return match; + } + + internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw) + { + return Invoke(inst, args, kw, null, null); + } + + internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase info) + { + return Invoke(inst, args, kw, info, null); + } + + internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase info, MethodInfo[] methodinfo) + { + Binding binding = Bind(inst, args, kw, info); + object result; + IntPtr ts = IntPtr.Zero; + + if (binding == null) + { + // If we already have an exception pending, don't create a new one + if (!Exceptions.ErrorOccurred()) + { + var value = new StringBuilder("No method matches given arguments"); + if (methodinfo != null && methodinfo.Length > 0) + { + value.Append($" for {methodinfo[0].Name}"); + } + else if (list.Count > 0) + { + value.Append($" for {list[0].MethodBase.Name}"); + } + + value.Append(": "); + AppendArgumentTypes(to: value, args); + Exceptions.RaiseTypeError(value.ToString()); + } + + return default; + } + + if (allow_threads) + { + ts = PythonEngine.BeginAllowThreads(); + } + + try + { + result = binding.info.Invoke(binding.inst, BindingFlags.Default, null, binding.args, null); + } + catch (Exception e) + { + if (e.InnerException != null) + { + e = e.InnerException; + } + if (allow_threads) + { + PythonEngine.EndAllowThreads(ts); + } + Exceptions.SetError(e); + return default; + } + + if (allow_threads) + { + PythonEngine.EndAllowThreads(ts); + } + + // If there are out parameters, we return a tuple containing + // the result followed by the out parameters. If there is only + // one out parameter and the return type of the method is void, + // we return the out parameter as the result to Python (for + // code compatibility with ironpython). + + var returnType = binding.info.IsConstructor ? typeof(void) : ((MethodInfo)binding.info).ReturnType; + + if (binding.outs > 0) + { + ParameterInfo[] pi = binding.info.GetParameters(); + int c = pi.Length; + var n = 0; + + bool isVoid = returnType == typeof(void); + int tupleSize = binding.outs + (isVoid ? 0 : 1); + using var t = Runtime.PyTuple_New(tupleSize); + if (!isVoid) + { + using var v = Converter.ToPython(result, returnType); + Runtime.PyTuple_SetItem(t.Borrow(), n, v.Steal()); + n++; + } + + for (var i = 0; i < c; i++) + { + Type pt = pi[i].ParameterType; + if (pt.IsByRef) + { + using var v = Converter.ToPython(binding.args[i], pt.GetElementType()); + Runtime.PyTuple_SetItem(t.Borrow(), n, v.Steal()); + n++; + } + } + + if (binding.outs == 1 && returnType == typeof(void)) + { + BorrowedReference item = Runtime.PyTuple_GetItem(t.Borrow(), 0); + return new NewReference(item); + } + + return new NewReference(t.Borrow()); + } + + return Converter.ToPython(result, returnType); + } + + /// + /// Utility class to store the information about a + /// + [Serializable] + internal class MethodInformation + { + private ParameterInfo[] _parameterInfo; + private string[] _parametersNames; + + public MethodBase MethodBase { get; } + + public bool IsOriginal { get; set; } + + public ParameterInfo[] ParameterInfo + { + get + { + _parameterInfo ??= MethodBase.GetParameters(); + return _parameterInfo; + } + } + + public string[] ParameterNames + { + get + { + if (_parametersNames == null) + { + if (IsOriginal) + { + _parametersNames = ParameterInfo.Select(pi => pi.Name).ToArray(); + } + else + { + _parametersNames = ParameterInfo.Select(pi => pi.Name.ToSnakeCase()).ToArray(); + } + } + return _parametersNames; + } + } + + public MethodInformation(MethodBase methodBase, bool isOriginal) + { + MethodBase = methodBase; + IsOriginal = isOriginal; + } + + public override string ToString() + { + return MethodBase.ToString(); + } + } + + /// + /// Utility class to sort method info by parameter type precedence. + /// + private class MethodSorter : IComparer + { + public int Compare(MethodInformation x, MethodInformation y) + { + int p1 = GetPrecedence(x); + int p2 = GetPrecedence(y); + if (p1 < p2) + { + return -1; + } + if (p1 > p2) + { + return 1; + } + return 0; + } + } + + private readonly struct MatchedMethod + { + public int KwargsMatched { get; } + public object?[] ManagedArgs { get; } + public int Outs { get; } + public MethodBase Method { get; } + + public MatchedMethod(int kwargsMatched, object?[] margs, int outs, MethodBase mb) + { + KwargsMatched = kwargsMatched; + ManagedArgs = margs; + Outs = outs; + Method = mb; + } + } + + protected static void AppendArgumentTypes(StringBuilder to, BorrowedReference args) + { + long argCount = Runtime.PyTuple_Size(args); + to.Append("("); + for (nint argIndex = 0; argIndex < argCount; argIndex++) + { + BorrowedReference arg = Runtime.PyTuple_GetItem(args, argIndex); + if (arg != null) + { + BorrowedReference type = Runtime.PyObject_TYPE(arg); + if (type != null) + { + using var description = Runtime.PyObject_Str(type); + if (description.IsNull()) + { + Exceptions.Clear(); + to.Append(Util.BadStr); + } + else + { + to.Append(Runtime.GetManagedString(description.Borrow())); + } + } + } + + if (argIndex + 1 < argCount) + to.Append(", "); + } + to.Append(')'); + } + } + + + /// + /// A Binding is a utility instance that bundles together a MethodInfo + /// representing a method to call, a (possibly null) target instance for + /// the call, and the arguments for the call (all as managed values). + /// + internal class Binding + { + public MethodBase info; + public object[] args; + public object inst; + public int outs; + + internal Binding(MethodBase info, object inst, object[] args, int outs) + { + this.info = info; + this.inst = inst; + this.args = args; + this.outs = outs; + } + } +} From a1a7e7277653debc9b7aa27747105acb48debdd9 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 31 Oct 2024 09:30:53 -0400 Subject: [PATCH 2/8] Housekeeping --- src/embed_tests/TestMethodBinder.cs | 2643 +++++++++++++-------------- src/runtime/MethodBinder.cs | 2292 +++++++++++------------ 2 files changed, 2467 insertions(+), 2468 deletions(-) diff --git a/src/embed_tests/TestMethodBinder.cs b/src/embed_tests/TestMethodBinder.cs index 78aa6d1f2..d7322135c 100644 --- a/src/embed_tests/TestMethodBinder.cs +++ b/src/embed_tests/TestMethodBinder.cs @@ -1,1330 +1,1329 @@ -using System; -using System.Linq; -using Python.Runtime; -using NUnit.Framework; -using System.Collections.Generic; -using System.Diagnostics; -using static Python.Runtime.Py; - -namespace Python.EmbeddingTest -{ - public class TestMethodBinder - { - private static dynamic module; - private static string testModule = @" -from datetime import * -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * -class PythonModel(TestMethodBinder.CSharpModel): - def TestA(self): - return self.OnlyString(TestMethodBinder.TestImplicitConversion()) - def TestB(self): - return self.OnlyClass('input string') - def TestC(self): - return self.InvokeModel('input string') - def TestD(self): - return self.InvokeModel(TestMethodBinder.TestImplicitConversion()) - def TestE(self, array): - return array.Length == 2 - def TestF(self): - model = TestMethodBinder.CSharpModel() - model.TestEnumerable(model.SomeList) - def TestG(self): - model = TestMethodBinder.CSharpModel() - model.TestList(model.SomeList) - def TestH(self): - return self.OnlyString(TestMethodBinder.ErroredImplicitConversion()) - def MethodTimeSpanTest(self): - TestMethodBinder.CSharpModel.MethodDateTimeAndTimeSpan(self, timedelta(days = 1), TestMethodBinder.SomeEnu.A, pinocho = 0) - TestMethodBinder.CSharpModel.MethodDateTimeAndTimeSpan(self, date(1, 1, 1), TestMethodBinder.SomeEnu.A, pinocho = 0) - TestMethodBinder.CSharpModel.MethodDateTimeAndTimeSpan(self, datetime(1, 1, 1, 1, 1, 1), TestMethodBinder.SomeEnu.A, pinocho = 0) - def NumericalArgumentMethodInteger(self): - self.NumericalArgumentMethod(1) - def NumericalArgumentMethodDouble(self): - self.NumericalArgumentMethod(0.1) - def NumericalArgumentMethodNumpy64Float(self): - self.NumericalArgumentMethod(TestMethodBinder.Numpy.float64(0.1)) - def ListKeyValuePairTest(self): - self.ListKeyValuePair([{'key': 1}]) - self.ListKeyValuePair([]) - def EnumerableKeyValuePairTest(self): - self.EnumerableKeyValuePair([{'key': 1}]) - self.EnumerableKeyValuePair([]) - def MethodWithParamsTest(self): - self.MethodWithParams(1, 'pepe') - - def TestList(self): - model = TestMethodBinder.CSharpModel() - model.List([TestMethodBinder.CSharpModel]) - def TestListReadOnlyCollection(self): - model = TestMethodBinder.CSharpModel() - model.ListReadOnlyCollection([TestMethodBinder.CSharpModel]) - def TestEnumerable(self): - model = TestMethodBinder.CSharpModel() - model.ListEnumerable([TestMethodBinder.CSharpModel])"; - - public static dynamic Numpy; - - [OneTimeSetUp] - public void SetUp() - { +using System; +using System.Linq; +using Python.Runtime; +using NUnit.Framework; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Python.EmbeddingTest +{ + public class TestMethodBinder + { + private static dynamic module; + private static string testModule = @" +from datetime import * +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * +class PythonModel(TestMethodBinder.CSharpModel): + def TestA(self): + return self.OnlyString(TestMethodBinder.TestImplicitConversion()) + def TestB(self): + return self.OnlyClass('input string') + def TestC(self): + return self.InvokeModel('input string') + def TestD(self): + return self.InvokeModel(TestMethodBinder.TestImplicitConversion()) + def TestE(self, array): + return array.Length == 2 + def TestF(self): + model = TestMethodBinder.CSharpModel() + model.TestEnumerable(model.SomeList) + def TestG(self): + model = TestMethodBinder.CSharpModel() + model.TestList(model.SomeList) + def TestH(self): + return self.OnlyString(TestMethodBinder.ErroredImplicitConversion()) + def MethodTimeSpanTest(self): + TestMethodBinder.CSharpModel.MethodDateTimeAndTimeSpan(self, timedelta(days = 1), TestMethodBinder.SomeEnu.A, pinocho = 0) + TestMethodBinder.CSharpModel.MethodDateTimeAndTimeSpan(self, date(1, 1, 1), TestMethodBinder.SomeEnu.A, pinocho = 0) + TestMethodBinder.CSharpModel.MethodDateTimeAndTimeSpan(self, datetime(1, 1, 1, 1, 1, 1), TestMethodBinder.SomeEnu.A, pinocho = 0) + def NumericalArgumentMethodInteger(self): + self.NumericalArgumentMethod(1) + def NumericalArgumentMethodDouble(self): + self.NumericalArgumentMethod(0.1) + def NumericalArgumentMethodNumpy64Float(self): + self.NumericalArgumentMethod(TestMethodBinder.Numpy.float64(0.1)) + def ListKeyValuePairTest(self): + self.ListKeyValuePair([{'key': 1}]) + self.ListKeyValuePair([]) + def EnumerableKeyValuePairTest(self): + self.EnumerableKeyValuePair([{'key': 1}]) + self.EnumerableKeyValuePair([]) + def MethodWithParamsTest(self): + self.MethodWithParams(1, 'pepe') + + def TestList(self): + model = TestMethodBinder.CSharpModel() + model.List([TestMethodBinder.CSharpModel]) + def TestListReadOnlyCollection(self): + model = TestMethodBinder.CSharpModel() + model.ListReadOnlyCollection([TestMethodBinder.CSharpModel]) + def TestEnumerable(self): + model = TestMethodBinder.CSharpModel() + model.ListEnumerable([TestMethodBinder.CSharpModel])"; + + public static dynamic Numpy; + + [OneTimeSetUp] + public void SetUp() + { PythonEngine.Initialize(); - using var _ = Py.GIL(); - - try - { - Numpy = Py.Import("numpy"); - } - catch (PythonException) - { - } - - module = PyModule.FromString("module", testModule).GetAttr("PythonModel").Invoke(); - } - - [OneTimeTearDown] - public void Dispose() - { - PythonEngine.Shutdown(); - } - - [Test] - public void MethodCalledList() - { - using (Py.GIL()) - module.TestList(); - Assert.AreEqual("List(List collection)", CSharpModel.MethodCalled); - } - - [Test] - public void MethodCalledReadOnlyCollection() - { - using (Py.GIL()) - module.TestListReadOnlyCollection(); - Assert.AreEqual("List(IReadOnlyCollection collection)", CSharpModel.MethodCalled); - } - - [Test] - public void MethodCalledEnumerable() - { - using (Py.GIL()) - module.TestEnumerable(); - Assert.AreEqual("List(IEnumerable collection)", CSharpModel.MethodCalled); - } - - [Test] - public void ListToEnumerableExpectingMethod() - { - using (Py.GIL()) - Assert.DoesNotThrow(() => module.TestF()); - } - - [Test] - public void ListToListExpectingMethod() - { - using (Py.GIL()) - Assert.DoesNotThrow(() => module.TestG()); - } - - [Test] - public void ImplicitConversionToString() - { - using (Py.GIL()) - { - var data = (string)module.TestA(); - // we assert implicit conversion took place - Assert.AreEqual("OnlyString impl: implicit to string", data); - } - } - - [Test] - public void ImplicitConversionToClass() - { - using (Py.GIL()) - { - var data = (string)module.TestB(); - // we assert implicit conversion took place - Assert.AreEqual("OnlyClass impl", data); - } - } - - // Reproduces a bug in which program explodes when implicit conversion fails - // in Linux - [Test] - public void ImplicitConversionErrorHandling() - { - using (Py.GIL()) - { - var errorCaught = false; - try - { - var data = (string)module.TestH(); - } - catch (Exception e) - { - errorCaught = true; - Assert.AreEqual("Failed to implicitly convert Python.EmbeddingTest.TestMethodBinder+ErroredImplicitConversion to System.String", e.Message); - } - - Assert.IsTrue(errorCaught); - } - } - - [Test] - public void WillAvoidUsingImplicitConversionIfPossible_String() - { - using (Py.GIL()) - { - var data = (string)module.TestC(); - // we assert no implicit conversion took place - Assert.AreEqual("string impl: input string", data); - } - } - - [Test] - public void WillAvoidUsingImplicitConversionIfPossible_Class() - { - using (Py.GIL()) - { - var data = (string)module.TestD(); - - // we assert no implicit conversion took place - Assert.AreEqual("TestImplicitConversion impl", data); - } - } - - [Test] - public void ArrayLength() - { - using (Py.GIL()) - { - var array = new[] { "pepe", "pinocho" }; - var data = (bool)module.TestE(array); - - // Assert it is true - Assert.AreEqual(true, data); - } - } - - [Test] - public void MethodDateTimeAndTimeSpan() - { - using (Py.GIL()) - Assert.DoesNotThrow(() => module.MethodTimeSpanTest()); - } - - [Test] - public void NumericalArgumentMethod() - { - using (Py.GIL()) - { - CSharpModel.ProvidedArgument = 0; - - module.NumericalArgumentMethodInteger(); - Assert.AreEqual(typeof(int), CSharpModel.ProvidedArgument.GetType()); - Assert.AreEqual(1, CSharpModel.ProvidedArgument); - - // python float type has double precision - module.NumericalArgumentMethodDouble(); - Assert.AreEqual(typeof(double), CSharpModel.ProvidedArgument.GetType()); - Assert.AreEqual(0.1d, CSharpModel.ProvidedArgument); - - module.NumericalArgumentMethodNumpy64Float(); - Assert.AreEqual(typeof(decimal), CSharpModel.ProvidedArgument.GetType()); - Assert.AreEqual(0.1, CSharpModel.ProvidedArgument); - } - } - - [Test] - // TODO: see GH issue https://github.com/pythonnet/pythonnet/issues/1532 re importing numpy after an engine restart fails - // so moving example test here so we import numpy once - public void TestReadme() - { - using (Py.GIL()) - { - Assert.AreEqual("1.0", Numpy.cos(Numpy.pi * 2).ToString()); - - dynamic sin = Numpy.sin; - StringAssert.StartsWith("-0.95892", sin(5).ToString()); - - double c = Numpy.cos(5) + sin(5); - Assert.AreEqual(-0.675262, c, 0.01); - - dynamic a = Numpy.array(new List { 1, 2, 3 }); - Assert.AreEqual("float64", a.dtype.ToString()); - - dynamic b = Numpy.array(new List { 6, 5, 4 }, Py.kw("dtype", Numpy.int32)); - Assert.AreEqual("int32", b.dtype.ToString()); - - Assert.AreEqual("[ 6. 10. 12.]", (a * b).ToString().Replace(" ", " ")); - } - } - - [Test] - public void NumpyDateTime64() - { - using (Py.GIL()) - { - var number = 10; - var numpyDateTime = Numpy.datetime64("2011-02"); - - object result; - var converted = Converter.ToManaged(numpyDateTime, typeof(DateTime), out result, false); - - Assert.IsTrue(converted); - Assert.AreEqual(new DateTime(2011, 02, 1), result); - } - } - - [Test] - public void ListKeyValuePair() - { - using (Py.GIL()) - Assert.DoesNotThrow(() => module.ListKeyValuePairTest()); - } - - [Test] - public void EnumerableKeyValuePair() - { - using (Py.GIL()) - Assert.DoesNotThrow(() => module.EnumerableKeyValuePairTest()); - } - - [Test] - public void MethodWithParamsPerformance() - { - using (Py.GIL()) - { - var stopwatch = new Stopwatch(); - stopwatch.Start(); - for (var i = 0; i < 100000; i++) - { - module.MethodWithParamsTest(); - } - stopwatch.Stop(); - - Console.WriteLine($"Took: {stopwatch.ElapsedMilliseconds}"); - } - } - - [Test] - public void NumericalArgumentMethodNumpy64FloatPerformance() - { - using (Py.GIL()) - { - var stopwatch = new Stopwatch(); - stopwatch.Start(); - for (var i = 0; i < 100000; i++) - { - module.NumericalArgumentMethodNumpy64Float(); - } - stopwatch.Stop(); - - Console.WriteLine($"Took: {stopwatch.ElapsedMilliseconds}"); - } - } - - [Test] - public void MethodWithParamsTest() - { - using (Py.GIL()) - Assert.DoesNotThrow(() => module.MethodWithParamsTest()); - } - - [Test] - public void TestNonStaticGenericMethodBinding() - { - using (Py.GIL()) - { - // Test matching generic on instance functions - // i.e. function signature is (Generic var1) - - // Run in C# - var class1 = new TestGenericClass1(); - var class2 = new TestGenericClass2(); - - class1.TestNonStaticGenericMethod(class1); - class2.TestNonStaticGenericMethod(class2); - - Assert.AreEqual(1, class1.Value); - Assert.AreEqual(1, class2.Value); - - // Run in Python - Assert.DoesNotThrow(() => PyModule.FromString("test", @" -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * -class1 = TestMethodBinder.TestGenericClass1() -class2 = TestMethodBinder.TestGenericClass2() - -class1.TestNonStaticGenericMethod(class1) -class2.TestNonStaticGenericMethod(class2) - -if class1.Value != 1 or class2.Value != 1: - raise AssertionError('Values were not updated') - ")); - } - } - - [Test] - public void TestGenericMethodBinding() - { - using (Py.GIL()) - { - // Test matching generic - // i.e. function signature is (Generic var1) - - // Run in C# - var class1 = new TestGenericClass1(); - var class2 = new TestGenericClass2(); - - TestGenericMethod(class1); - TestGenericMethod(class2); - - Assert.AreEqual(1, class1.Value); - Assert.AreEqual(1, class2.Value); - - // Run in Python - Assert.DoesNotThrow(() => PyModule.FromString("test", @" -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * -class1 = TestMethodBinder.TestGenericClass1() -class2 = TestMethodBinder.TestGenericClass2() - -TestMethodBinder.TestGenericMethod(class1) -TestMethodBinder.TestGenericMethod(class2) - -if class1.Value != 1 or class2.Value != 1: - raise AssertionError('Values were not updated') -")); - } - } - - [Test] - public void TestMultipleGenericMethodBinding() - { - using (Py.GIL()) - { - // Test matching multiple generics - // i.e. function signature is (Generic var1) - - // Run in C# - var class1 = new TestMultipleGenericClass1(); - var class2 = new TestMultipleGenericClass2(); - - TestMultipleGenericMethod(class1); - TestMultipleGenericMethod(class2); - - Assert.AreEqual(1, class1.Value); - Assert.AreEqual(1, class2.Value); - - // Run in Python - Assert.DoesNotThrow(() => PyModule.FromString("test", @" -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * -class1 = TestMethodBinder.TestMultipleGenericClass1() -class2 = TestMethodBinder.TestMultipleGenericClass2() - -TestMethodBinder.TestMultipleGenericMethod(class1) -TestMethodBinder.TestMultipleGenericMethod(class2) - -if class1.Value != 1 or class2.Value != 1: - raise AssertionError('Values were not updated') -")); - } - } - - [Test] - public void TestMultipleGenericParamMethodBinding() - { - using (Py.GIL()) - { - // Test multiple param generics matching - // i.e. function signature is (Generic1 var1, Generic var2) - - // Run in C# - var class1a = new TestGenericClass1(); - var class1b = new TestMultipleGenericClass1(); - - TestMultipleGenericParamsMethod(class1a, class1b); - - Assert.AreEqual(1, class1a.Value); - Assert.AreEqual(1, class1a.Value); - - - var class2a = new TestGenericClass2(); - var class2b = new TestMultipleGenericClass2(); - - TestMultipleGenericParamsMethod(class2a, class2b); - - Assert.AreEqual(1, class2a.Value); - Assert.AreEqual(1, class2b.Value); - - // Run in Python - Assert.DoesNotThrow(() => PyModule.FromString("test", @" -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * -class1a = TestMethodBinder.TestGenericClass1() -class1b = TestMethodBinder.TestMultipleGenericClass1() - -TestMethodBinder.TestMultipleGenericParamsMethod(class1a, class1b) - -if class1a.Value != 1 or class1b.Value != 1: - raise AssertionError('Values were not updated') - -class2a = TestMethodBinder.TestGenericClass2() -class2b = TestMethodBinder.TestMultipleGenericClass2() - -TestMethodBinder.TestMultipleGenericParamsMethod(class2a, class2b) - -if class2a.Value != 1 or class2b.Value != 1: - raise AssertionError('Values were not updated') -")); - } - } - - [Test] - public void TestMultipleGenericParamMethodBinding_MixedOrder() - { - using (Py.GIL()) - { - // Test matching multiple param generics with mixed order - // i.e. function signature is (Generic1 var1, Generic var2) - - // Run in C# - var class1a = new TestGenericClass2(); - var class1b = new TestMultipleGenericClass1(); - - TestMultipleGenericParamsMethod2(class1a, class1b); - - Assert.AreEqual(1, class1a.Value); - Assert.AreEqual(1, class1a.Value); - - var class2a = new TestGenericClass1(); - var class2b = new TestMultipleGenericClass2(); - - TestMultipleGenericParamsMethod2(class2a, class2b); - - Assert.AreEqual(1, class2a.Value); - Assert.AreEqual(1, class2b.Value); - - // Run in Python - Assert.DoesNotThrow(() => PyModule.FromString("test", @" -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * -class1a = TestMethodBinder.TestGenericClass2() -class1b = TestMethodBinder.TestMultipleGenericClass1() - -TestMethodBinder.TestMultipleGenericParamsMethod2(class1a, class1b) - -if class1a.Value != 1 or class1b.Value != 1: - raise AssertionError('Values were not updated') - -class2a = TestMethodBinder.TestGenericClass1() -class2b = TestMethodBinder.TestMultipleGenericClass2() - -TestMethodBinder.TestMultipleGenericParamsMethod2(class2a, class2b) - -if class2a.Value != 1 or class2b.Value != 1: - raise AssertionError('Values were not updated') -")); - } - } - - [Test] - public void TestPyClassGenericBinding() - { - using (Py.GIL()) - // Overriding our generics in Python we should still match with the generic method - Assert.DoesNotThrow(() => PyModule.FromString("test", @" -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * - -class PyGenericClass(TestMethodBinder.TestGenericClass1): - pass - -class PyMultipleGenericClass(TestMethodBinder.TestMultipleGenericClass1): - pass - -singleGenericClass = PyGenericClass() -multiGenericClass = PyMultipleGenericClass() - -TestMethodBinder.TestGenericMethod(singleGenericClass) -TestMethodBinder.TestMultipleGenericMethod(multiGenericClass) -TestMethodBinder.TestMultipleGenericParamsMethod(singleGenericClass, multiGenericClass) - -if singleGenericClass.Value != 1 or multiGenericClass.Value != 1: - raise AssertionError('Values were not updated') -")); - } - - [Test] - public void TestNonGenericIsUsedWhenAvailable() - { - using (Py.GIL()) - {// Run in C# - var class1 = new TestGenericClass3(); - TestGenericMethod(class1); - Assert.AreEqual(10, class1.Value); - - - // When available, should select non-generic method over generic method - Assert.DoesNotThrow(() => PyModule.FromString("test", @" -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * - -class1 = TestMethodBinder.TestGenericClass3() - -TestMethodBinder.TestGenericMethod(class1) - -if class1.Value != 10: - raise AssertionError('Value was not updated') -")); - } - } - - [Test] - public void TestMatchTypedGenericOverload() - { - using (Py.GIL()) - {// Test to ensure we can match a typed generic overload - // even when there are other matches that would apply. - var class1 = new TestGenericClass4(); - TestGenericMethod(class1); - Assert.AreEqual(15, class1.Value); - - Assert.DoesNotThrow(() => PyModule.FromString("test", @" -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * - -class1 = TestMethodBinder.TestGenericClass4() - -TestMethodBinder.TestGenericMethod(class1) - -if class1.Value != 15: - raise AssertionError('Value was not updated') -")); - } - } - - [Test] - public void TestGenericBindingSpeed() - { - using (Py.GIL()) - { - var stopwatch = new Stopwatch(); - stopwatch.Start(); - for (int i = 0; i < 10000; i++) - { - TestMultipleGenericParamMethodBinding(); - } - stopwatch.Stop(); - - Console.WriteLine($"Took: {stopwatch.ElapsedMilliseconds} ms"); - } - } - - [Test] - public void TestGenericTypeMatchingWithConvertedPyType() - { - // This test ensures that we can still match and bind a generic method when we - // have a converted pytype in the args (py timedelta -> C# TimeSpan) - - using (Py.GIL()) - Assert.DoesNotThrow(() => PyModule.FromString("test", @" -from datetime import timedelta -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * -class1 = TestMethodBinder.TestGenericClass1() - -span = timedelta(hours=5) - -TestMethodBinder.TestGenericMethod(class1, span) - -if class1.Value != 5: - raise AssertionError('Values were not updated properly') -")); - } - - [Test] - public void TestGenericTypeMatchingWithDefaultArgs() - { - // This test ensures that we can still match and bind a generic method when we have default args - - using (Py.GIL()) - Assert.DoesNotThrow(() => PyModule.FromString("test", @" -from datetime import timedelta -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * -class1 = TestMethodBinder.TestGenericClass1() - -TestMethodBinder.TestGenericMethodWithDefault(class1) - -if class1.Value != 25: - raise AssertionError(f'Value was not 25, was {class1.Value}') - -TestMethodBinder.TestGenericMethodWithDefault(class1, 50) - -if class1.Value != 50: - raise AssertionError('Value was not 50, was {class1.Value}') -")); - } - - [Test] - public void TestGenericTypeMatchingWithNullDefaultArgs() - { - // This test ensures that we can still match and bind a generic method when we have \ - // null default args, important because caching by arg types occurs - - using (Py.GIL()) - Assert.DoesNotThrow(() => PyModule.FromString("test", @" -from datetime import timedelta -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * -class1 = TestMethodBinder.TestGenericClass1() - -TestMethodBinder.TestGenericMethodWithNullDefault(class1) - -if class1.Value != 10: - raise AssertionError(f'Value was not 25, was {class1.Value}') - -TestMethodBinder.TestGenericMethodWithNullDefault(class1, class1) - -if class1.Value != 20: - raise AssertionError('Value was not 50, was {class1.Value}') -")); - } - - [Test] - public void TestMatchPyDateToDateTime() - { - using (Py.GIL()) - // This test ensures that we match py datetime.date object to C# DateTime object - Assert.DoesNotThrow(() => PyModule.FromString("test", @" -from datetime import * -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * - -test = date(year=2011, month=5, day=1) -result = TestMethodBinder.GetMonth(test) - -if result != 5: - raise AssertionError('Failed to return expected value 1') -")); - } - - public class OverloadsTestClass - { - - public string Method1(string positionalArg, decimal namedArg1 = 1.2m, int namedArg2 = 123) - { - Console.WriteLine("1"); - return "Method1 Overload 1"; - } - - public string Method1(decimal namedArg1 = 1.2m, int namedArg2 = 123) - { - Console.WriteLine("2"); - return "Method1 Overload 2"; - } - - // ---- - - public string Method2(string arg1, int arg2, decimal arg3, decimal kwarg1 = 1.1m, bool kwarg2 = false, string kwarg3 = "") - { - return "Method2 Overload 1"; - } - - public string Method2(string arg1, int arg2, decimal kwarg1 = 1.1m, bool kwarg2 = false, string kwarg3 = "") - { - return "Method2 Overload 2"; - } - - // ---- - - public string Method3(string arg1, int arg2, float arg3, float kwarg1 = 1.1f, bool kwarg2 = false, string kwarg3 = "") - { - return "Method3 Overload 1"; - } - - public string Method3(string arg1, int arg2, float kwarg1 = 1.1f, bool kwarg2 = false, string kwarg3 = "") - { - return "Method3 Overload 2"; - } - - // ---- - - public string ImplicitConversionSameArgumentCount(string symbol, int quantity, float trailingAmount, bool trailingAsPercentage, string tag = "") - { - return "ImplicitConversionSameArgumentCount 1"; - } - - public string ImplicitConversionSameArgumentCount(string symbol, decimal quantity, decimal trailingAmount, bool trailingAsPercentage, string tag = "") - { - return "ImplicitConversionSameArgumentCount 2"; - } - - public string ImplicitConversionSameArgumentCount2(string symbol, int quantity, float trailingAmount, bool trailingAsPercentage, string tag = "") - { - return "ImplicitConversionSameArgumentCount2 1"; - } - - public string ImplicitConversionSameArgumentCount2(string symbol, float quantity, float trailingAmount, bool trailingAsPercentage, string tag = "") - { - return "ImplicitConversionSameArgumentCount2 2"; - } - - public string ImplicitConversionSameArgumentCount2(string symbol, decimal quantity, float trailingAmount, bool trailingAsPercentage, string tag = "") - { - 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")] - [TestCase("Method1('abc', namedArg1=12.34, namedArg2=321)", "Method1 Overload 1")] - [TestCase("Method2(\"SPY\", 10, 123, kwarg1=1, kwarg2=True)", "Method2 Overload 1")] - [TestCase("Method2(\"SPY\", 10, 123.34, kwarg1=1.23, kwarg2=True)", "Method2 Overload 1")] - [TestCase("Method3(\"SPY\", 10, 123.34, kwarg1=1.23, kwarg2=True)", "Method3 Overload 1")] - public void SelectsRightOverloadWithNamedParameters(string methodCallCode, string expectedResult) - { - using var _ = Py.GIL(); - - dynamic module = PyModule.FromString("SelectsRightOverloadWithNamedParameters", @$" - -def call_method(instance): - return instance.{methodCallCode} -"); - - var instance = new OverloadsTestClass(); - var result = module.call_method(instance).As(); - - Assert.AreEqual(expectedResult, result); - } - - [TestCase("ImplicitConversionSameArgumentCount", "10", "ImplicitConversionSameArgumentCount 1")] - [TestCase("ImplicitConversionSameArgumentCount", "10.1", "ImplicitConversionSameArgumentCount 2")] - [TestCase("ImplicitConversionSameArgumentCount2", "10", "ImplicitConversionSameArgumentCount2 1")] - [TestCase("ImplicitConversionSameArgumentCount2", "10.1", "ImplicitConversionSameArgumentCount2 2")] - public void DisambiguatesOverloadWithSameArgumentCountAndImplicitConversion(string methodName, string quantity, string expectedResult) - { - using var _ = Py.GIL(); - - dynamic module = PyModule.FromString("DisambiguatesOverloadWithSameArgumentCountAndImplicitConversion", @$" -def call_method(instance): - return instance.{methodName}(""SPY"", {quantity}, 123.4, trailingAsPercentage=True) -"); - - var instance = new OverloadsTestClass(); - var result = module.call_method(instance).As(); - - Assert.AreEqual(expectedResult, result); - } - - public class CSharpClass - { - public string CalledMethodMessage { get; private set; } - - public void Method() - { - CalledMethodMessage = "Overload 1"; - } - - public void Method(string stringArgument, decimal decimalArgument = 1.2m) - { - CalledMethodMessage = "Overload 2"; - } - - public void Method(PyObject typeArgument, decimal decimalArgument = 1.2m) - { - CalledMethodMessage = "Overload 3"; - } - } - - [Test] - public void CallsCorrectOverloadWithoutErrors() - { - using var _ = Py.GIL(); - - var module = PyModule.FromString("CallsCorrectOverloadWithoutErrors", @" -from clr import AddReference -AddReference(""System"") -AddReference(""Python.EmbeddingTest"") -from Python.EmbeddingTest import * - -class PythonModel(TestMethodBinder.CSharpModel): - pass - -def call_method(instance): - instance.Method(PythonModel, decimalArgument=1.234) -"); - - var instance = new CSharpClass(); - using var pyInstance = instance.ToPython(); - - Assert.DoesNotThrow(() => - { - module.GetAttr("call_method").Invoke(pyInstance); - }); - - Assert.AreEqual("Overload 3", instance.CalledMethodMessage); - - Assert.IsFalse(Exceptions.ErrorOccurred()); - } - - public class CSharpClass2 - { - public string CalledMethodMessage { get; private set; } - - public void Method() - { - CalledMethodMessage = "Overload 1"; - } - - public void Method(CSharpClass csharpClassArgument, decimal decimalArgument = 1.2m, PyObject pyObjectKArgument = null) - { - CalledMethodMessage = "Overload 2"; - } - - public void Method(PyObject pyObjectArgument, decimal decimalArgument = 1.2m, object objectArgument = null) - { - CalledMethodMessage = "Overload 3"; - } + using var _ = Py.GIL(); + + try + { + Numpy = Py.Import("numpy"); + } + catch (PythonException) + { + } + + module = PyModule.FromString("module", testModule).GetAttr("PythonModel").Invoke(); + } + + [OneTimeTearDown] + public void Dispose() + { + PythonEngine.Shutdown(); + } + + [Test] + public void MethodCalledList() + { + using (Py.GIL()) + module.TestList(); + Assert.AreEqual("List(List collection)", CSharpModel.MethodCalled); + } + + [Test] + public void MethodCalledReadOnlyCollection() + { + using (Py.GIL()) + module.TestListReadOnlyCollection(); + Assert.AreEqual("List(IReadOnlyCollection collection)", CSharpModel.MethodCalled); + } + + [Test] + public void MethodCalledEnumerable() + { + using (Py.GIL()) + module.TestEnumerable(); + Assert.AreEqual("List(IEnumerable collection)", CSharpModel.MethodCalled); + } + + [Test] + public void ListToEnumerableExpectingMethod() + { + using (Py.GIL()) + Assert.DoesNotThrow(() => module.TestF()); + } + + [Test] + public void ListToListExpectingMethod() + { + using (Py.GIL()) + Assert.DoesNotThrow(() => module.TestG()); + } + + [Test] + public void ImplicitConversionToString() + { + using (Py.GIL()) + { + var data = (string)module.TestA(); + // we assert implicit conversion took place + Assert.AreEqual("OnlyString impl: implicit to string", data); + } + } + + [Test] + public void ImplicitConversionToClass() + { + using (Py.GIL()) + { + var data = (string)module.TestB(); + // we assert implicit conversion took place + Assert.AreEqual("OnlyClass impl", data); + } + } + + // Reproduces a bug in which program explodes when implicit conversion fails + // in Linux + [Test] + public void ImplicitConversionErrorHandling() + { + using (Py.GIL()) + { + var errorCaught = false; + try + { + var data = (string)module.TestH(); + } + catch (Exception e) + { + errorCaught = true; + Assert.AreEqual("Failed to implicitly convert Python.EmbeddingTest.TestMethodBinder+ErroredImplicitConversion to System.String", e.Message); + } + + Assert.IsTrue(errorCaught); + } + } + + [Test] + public void WillAvoidUsingImplicitConversionIfPossible_String() + { + using (Py.GIL()) + { + var data = (string)module.TestC(); + // we assert no implicit conversion took place + Assert.AreEqual("string impl: input string", data); + } + } + + [Test] + public void WillAvoidUsingImplicitConversionIfPossible_Class() + { + using (Py.GIL()) + { + var data = (string)module.TestD(); + + // we assert no implicit conversion took place + Assert.AreEqual("TestImplicitConversion impl", data); + } + } + + [Test] + public void ArrayLength() + { + using (Py.GIL()) + { + var array = new[] { "pepe", "pinocho" }; + var data = (bool)module.TestE(array); + + // Assert it is true + Assert.AreEqual(true, data); + } + } + + [Test] + public void MethodDateTimeAndTimeSpan() + { + using (Py.GIL()) + Assert.DoesNotThrow(() => module.MethodTimeSpanTest()); + } + + [Test] + public void NumericalArgumentMethod() + { + using (Py.GIL()) + { + CSharpModel.ProvidedArgument = 0; + + module.NumericalArgumentMethodInteger(); + Assert.AreEqual(typeof(int), CSharpModel.ProvidedArgument.GetType()); + Assert.AreEqual(1, CSharpModel.ProvidedArgument); + + // python float type has double precision + module.NumericalArgumentMethodDouble(); + Assert.AreEqual(typeof(double), CSharpModel.ProvidedArgument.GetType()); + Assert.AreEqual(0.1d, CSharpModel.ProvidedArgument); + + module.NumericalArgumentMethodNumpy64Float(); + Assert.AreEqual(typeof(decimal), CSharpModel.ProvidedArgument.GetType()); + Assert.AreEqual(0.1, CSharpModel.ProvidedArgument); + } + } + + [Test] + // TODO: see GH issue https://github.com/pythonnet/pythonnet/issues/1532 re importing numpy after an engine restart fails + // so moving example test here so we import numpy once + public void TestReadme() + { + using (Py.GIL()) + { + Assert.AreEqual("1.0", Numpy.cos(Numpy.pi * 2).ToString()); + + dynamic sin = Numpy.sin; + StringAssert.StartsWith("-0.95892", sin(5).ToString()); + + double c = Numpy.cos(5) + sin(5); + Assert.AreEqual(-0.675262, c, 0.01); + + dynamic a = Numpy.array(new List { 1, 2, 3 }); + Assert.AreEqual("float64", a.dtype.ToString()); + + dynamic b = Numpy.array(new List { 6, 5, 4 }, Py.kw("dtype", Numpy.int32)); + Assert.AreEqual("int32", b.dtype.ToString()); + + Assert.AreEqual("[ 6. 10. 12.]", (a * b).ToString().Replace(" ", " ")); + } + } + + [Test] + public void NumpyDateTime64() + { + using (Py.GIL()) + { + var number = 10; + var numpyDateTime = Numpy.datetime64("2011-02"); + + object result; + var converted = Converter.ToManaged(numpyDateTime, typeof(DateTime), out result, false); + + Assert.IsTrue(converted); + Assert.AreEqual(new DateTime(2011, 02, 1), result); + } + } + + [Test] + public void ListKeyValuePair() + { + using (Py.GIL()) + Assert.DoesNotThrow(() => module.ListKeyValuePairTest()); + } + + [Test] + public void EnumerableKeyValuePair() + { + using (Py.GIL()) + Assert.DoesNotThrow(() => module.EnumerableKeyValuePairTest()); + } + + [Test] + public void MethodWithParamsPerformance() + { + using (Py.GIL()) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); + for (var i = 0; i < 100000; i++) + { + module.MethodWithParamsTest(); + } + stopwatch.Stop(); + + Console.WriteLine($"Took: {stopwatch.ElapsedMilliseconds}"); + } + } + + [Test] + public void NumericalArgumentMethodNumpy64FloatPerformance() + { + using (Py.GIL()) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); + for (var i = 0; i < 100000; i++) + { + module.NumericalArgumentMethodNumpy64Float(); + } + stopwatch.Stop(); + + Console.WriteLine($"Took: {stopwatch.ElapsedMilliseconds}"); + } + } + + [Test] + public void MethodWithParamsTest() + { + using (Py.GIL()) + Assert.DoesNotThrow(() => module.MethodWithParamsTest()); + } + + [Test] + public void TestNonStaticGenericMethodBinding() + { + using (Py.GIL()) + { + // Test matching generic on instance functions + // i.e. function signature is (Generic var1) + + // Run in C# + var class1 = new TestGenericClass1(); + var class2 = new TestGenericClass2(); + + class1.TestNonStaticGenericMethod(class1); + class2.TestNonStaticGenericMethod(class2); + + Assert.AreEqual(1, class1.Value); + Assert.AreEqual(1, class2.Value); + + // Run in Python + Assert.DoesNotThrow(() => PyModule.FromString("test", @" +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * +class1 = TestMethodBinder.TestGenericClass1() +class2 = TestMethodBinder.TestGenericClass2() + +class1.TestNonStaticGenericMethod(class1) +class2.TestNonStaticGenericMethod(class2) + +if class1.Value != 1 or class2.Value != 1: + raise AssertionError('Values were not updated') + ")); + } + } + + [Test] + public void TestGenericMethodBinding() + { + using (Py.GIL()) + { + // Test matching generic + // i.e. function signature is (Generic var1) + + // Run in C# + var class1 = new TestGenericClass1(); + var class2 = new TestGenericClass2(); + + TestGenericMethod(class1); + TestGenericMethod(class2); + + Assert.AreEqual(1, class1.Value); + Assert.AreEqual(1, class2.Value); + + // Run in Python + Assert.DoesNotThrow(() => PyModule.FromString("test", @" +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * +class1 = TestMethodBinder.TestGenericClass1() +class2 = TestMethodBinder.TestGenericClass2() + +TestMethodBinder.TestGenericMethod(class1) +TestMethodBinder.TestGenericMethod(class2) + +if class1.Value != 1 or class2.Value != 1: + raise AssertionError('Values were not updated') +")); + } + } + + [Test] + public void TestMultipleGenericMethodBinding() + { + using (Py.GIL()) + { + // Test matching multiple generics + // i.e. function signature is (Generic var1) + + // Run in C# + var class1 = new TestMultipleGenericClass1(); + var class2 = new TestMultipleGenericClass2(); + + TestMultipleGenericMethod(class1); + TestMultipleGenericMethod(class2); + + Assert.AreEqual(1, class1.Value); + Assert.AreEqual(1, class2.Value); + + // Run in Python + Assert.DoesNotThrow(() => PyModule.FromString("test", @" +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * +class1 = TestMethodBinder.TestMultipleGenericClass1() +class2 = TestMethodBinder.TestMultipleGenericClass2() + +TestMethodBinder.TestMultipleGenericMethod(class1) +TestMethodBinder.TestMultipleGenericMethod(class2) + +if class1.Value != 1 or class2.Value != 1: + raise AssertionError('Values were not updated') +")); + } + } + + [Test] + public void TestMultipleGenericParamMethodBinding() + { + using (Py.GIL()) + { + // Test multiple param generics matching + // i.e. function signature is (Generic1 var1, Generic var2) + + // Run in C# + var class1a = new TestGenericClass1(); + var class1b = new TestMultipleGenericClass1(); + + TestMultipleGenericParamsMethod(class1a, class1b); + + Assert.AreEqual(1, class1a.Value); + Assert.AreEqual(1, class1a.Value); + + + var class2a = new TestGenericClass2(); + var class2b = new TestMultipleGenericClass2(); + + TestMultipleGenericParamsMethod(class2a, class2b); + + Assert.AreEqual(1, class2a.Value); + Assert.AreEqual(1, class2b.Value); + + // Run in Python + Assert.DoesNotThrow(() => PyModule.FromString("test", @" +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * +class1a = TestMethodBinder.TestGenericClass1() +class1b = TestMethodBinder.TestMultipleGenericClass1() + +TestMethodBinder.TestMultipleGenericParamsMethod(class1a, class1b) + +if class1a.Value != 1 or class1b.Value != 1: + raise AssertionError('Values were not updated') + +class2a = TestMethodBinder.TestGenericClass2() +class2b = TestMethodBinder.TestMultipleGenericClass2() + +TestMethodBinder.TestMultipleGenericParamsMethod(class2a, class2b) + +if class2a.Value != 1 or class2b.Value != 1: + raise AssertionError('Values were not updated') +")); + } + } + + [Test] + public void TestMultipleGenericParamMethodBinding_MixedOrder() + { + using (Py.GIL()) + { + // Test matching multiple param generics with mixed order + // i.e. function signature is (Generic1 var1, Generic var2) + + // Run in C# + var class1a = new TestGenericClass2(); + var class1b = new TestMultipleGenericClass1(); + + TestMultipleGenericParamsMethod2(class1a, class1b); + + Assert.AreEqual(1, class1a.Value); + Assert.AreEqual(1, class1a.Value); + + var class2a = new TestGenericClass1(); + var class2b = new TestMultipleGenericClass2(); + + TestMultipleGenericParamsMethod2(class2a, class2b); + + Assert.AreEqual(1, class2a.Value); + Assert.AreEqual(1, class2b.Value); + + // Run in Python + Assert.DoesNotThrow(() => PyModule.FromString("test", @" +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * +class1a = TestMethodBinder.TestGenericClass2() +class1b = TestMethodBinder.TestMultipleGenericClass1() + +TestMethodBinder.TestMultipleGenericParamsMethod2(class1a, class1b) + +if class1a.Value != 1 or class1b.Value != 1: + raise AssertionError('Values were not updated') + +class2a = TestMethodBinder.TestGenericClass1() +class2b = TestMethodBinder.TestMultipleGenericClass2() + +TestMethodBinder.TestMultipleGenericParamsMethod2(class2a, class2b) + +if class2a.Value != 1 or class2b.Value != 1: + raise AssertionError('Values were not updated') +")); + } + } + + [Test] + public void TestPyClassGenericBinding() + { + using (Py.GIL()) + // Overriding our generics in Python we should still match with the generic method + Assert.DoesNotThrow(() => PyModule.FromString("test", @" +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * + +class PyGenericClass(TestMethodBinder.TestGenericClass1): + pass + +class PyMultipleGenericClass(TestMethodBinder.TestMultipleGenericClass1): + pass + +singleGenericClass = PyGenericClass() +multiGenericClass = PyMultipleGenericClass() + +TestMethodBinder.TestGenericMethod(singleGenericClass) +TestMethodBinder.TestMultipleGenericMethod(multiGenericClass) +TestMethodBinder.TestMultipleGenericParamsMethod(singleGenericClass, multiGenericClass) + +if singleGenericClass.Value != 1 or multiGenericClass.Value != 1: + raise AssertionError('Values were not updated') +")); + } + + [Test] + public void TestNonGenericIsUsedWhenAvailable() + { + using (Py.GIL()) + {// Run in C# + var class1 = new TestGenericClass3(); + TestGenericMethod(class1); + Assert.AreEqual(10, class1.Value); + + + // When available, should select non-generic method over generic method + Assert.DoesNotThrow(() => PyModule.FromString("test", @" +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * + +class1 = TestMethodBinder.TestGenericClass3() + +TestMethodBinder.TestGenericMethod(class1) + +if class1.Value != 10: + raise AssertionError('Value was not updated') +")); + } + } + + [Test] + public void TestMatchTypedGenericOverload() + { + using (Py.GIL()) + {// Test to ensure we can match a typed generic overload + // even when there are other matches that would apply. + var class1 = new TestGenericClass4(); + TestGenericMethod(class1); + Assert.AreEqual(15, class1.Value); + + Assert.DoesNotThrow(() => PyModule.FromString("test", @" +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * + +class1 = TestMethodBinder.TestGenericClass4() + +TestMethodBinder.TestGenericMethod(class1) + +if class1.Value != 15: + raise AssertionError('Value was not updated') +")); + } + } + + [Test] + public void TestGenericBindingSpeed() + { + using (Py.GIL()) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); + for (int i = 0; i < 10000; i++) + { + TestMultipleGenericParamMethodBinding(); + } + stopwatch.Stop(); + + Console.WriteLine($"Took: {stopwatch.ElapsedMilliseconds} ms"); + } + } + + [Test] + public void TestGenericTypeMatchingWithConvertedPyType() + { + // This test ensures that we can still match and bind a generic method when we + // have a converted pytype in the args (py timedelta -> C# TimeSpan) + + using (Py.GIL()) + Assert.DoesNotThrow(() => PyModule.FromString("test", @" +from datetime import timedelta +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * +class1 = TestMethodBinder.TestGenericClass1() + +span = timedelta(hours=5) + +TestMethodBinder.TestGenericMethod(class1, span) + +if class1.Value != 5: + raise AssertionError('Values were not updated properly') +")); + } + + [Test] + public void TestGenericTypeMatchingWithDefaultArgs() + { + // This test ensures that we can still match and bind a generic method when we have default args + + using (Py.GIL()) + Assert.DoesNotThrow(() => PyModule.FromString("test", @" +from datetime import timedelta +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * +class1 = TestMethodBinder.TestGenericClass1() + +TestMethodBinder.TestGenericMethodWithDefault(class1) + +if class1.Value != 25: + raise AssertionError(f'Value was not 25, was {class1.Value}') + +TestMethodBinder.TestGenericMethodWithDefault(class1, 50) + +if class1.Value != 50: + raise AssertionError('Value was not 50, was {class1.Value}') +")); + } + + [Test] + public void TestGenericTypeMatchingWithNullDefaultArgs() + { + // This test ensures that we can still match and bind a generic method when we have \ + // null default args, important because caching by arg types occurs + + using (Py.GIL()) + Assert.DoesNotThrow(() => PyModule.FromString("test", @" +from datetime import timedelta +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * +class1 = TestMethodBinder.TestGenericClass1() + +TestMethodBinder.TestGenericMethodWithNullDefault(class1) + +if class1.Value != 10: + raise AssertionError(f'Value was not 25, was {class1.Value}') + +TestMethodBinder.TestGenericMethodWithNullDefault(class1, class1) + +if class1.Value != 20: + raise AssertionError('Value was not 50, was {class1.Value}') +")); + } + + [Test] + public void TestMatchPyDateToDateTime() + { + using (Py.GIL()) + // This test ensures that we match py datetime.date object to C# DateTime object + Assert.DoesNotThrow(() => PyModule.FromString("test", @" +from datetime import * +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * + +test = date(year=2011, month=5, day=1) +result = TestMethodBinder.GetMonth(test) + +if result != 5: + raise AssertionError('Failed to return expected value 1') +")); + } + + public class OverloadsTestClass + { + + public string Method1(string positionalArg, decimal namedArg1 = 1.2m, int namedArg2 = 123) + { + Console.WriteLine("1"); + return "Method1 Overload 1"; + } + + public string Method1(decimal namedArg1 = 1.2m, int namedArg2 = 123) + { + Console.WriteLine("2"); + return "Method1 Overload 2"; + } + + // ---- + + public string Method2(string arg1, int arg2, decimal arg3, decimal kwarg1 = 1.1m, bool kwarg2 = false, string kwarg3 = "") + { + return "Method2 Overload 1"; + } + + public string Method2(string arg1, int arg2, decimal kwarg1 = 1.1m, bool kwarg2 = false, string kwarg3 = "") + { + return "Method2 Overload 2"; + } + + // ---- + + public string Method3(string arg1, int arg2, float arg3, float kwarg1 = 1.1f, bool kwarg2 = false, string kwarg3 = "") + { + return "Method3 Overload 1"; + } + + public string Method3(string arg1, int arg2, float kwarg1 = 1.1f, bool kwarg2 = false, string kwarg3 = "") + { + return "Method3 Overload 2"; + } + + // ---- + + public string ImplicitConversionSameArgumentCount(string symbol, int quantity, float trailingAmount, bool trailingAsPercentage, string tag = "") + { + return "ImplicitConversionSameArgumentCount 1"; + } + + public string ImplicitConversionSameArgumentCount(string symbol, decimal quantity, decimal trailingAmount, bool trailingAsPercentage, string tag = "") + { + return "ImplicitConversionSameArgumentCount 2"; + } + + public string ImplicitConversionSameArgumentCount2(string symbol, int quantity, float trailingAmount, bool trailingAsPercentage, string tag = "") + { + return "ImplicitConversionSameArgumentCount2 1"; + } + + public string ImplicitConversionSameArgumentCount2(string symbol, float quantity, float trailingAmount, bool trailingAsPercentage, string tag = "") + { + return "ImplicitConversionSameArgumentCount2 2"; + } + + public string ImplicitConversionSameArgumentCount2(string symbol, decimal quantity, float trailingAmount, bool trailingAsPercentage, string tag = "") + { + 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")] + [TestCase("Method1('abc', namedArg1=12.34, namedArg2=321)", "Method1 Overload 1")] + [TestCase("Method2(\"SPY\", 10, 123, kwarg1=1, kwarg2=True)", "Method2 Overload 1")] + [TestCase("Method2(\"SPY\", 10, 123.34, kwarg1=1.23, kwarg2=True)", "Method2 Overload 1")] + [TestCase("Method3(\"SPY\", 10, 123.34, kwarg1=1.23, kwarg2=True)", "Method3 Overload 1")] + public void SelectsRightOverloadWithNamedParameters(string methodCallCode, string expectedResult) + { + using var _ = Py.GIL(); + + dynamic module = PyModule.FromString("SelectsRightOverloadWithNamedParameters", @$" + +def call_method(instance): + return instance.{methodCallCode} +"); + + var instance = new OverloadsTestClass(); + var result = module.call_method(instance).As(); + + Assert.AreEqual(expectedResult, result); + } + + [TestCase("ImplicitConversionSameArgumentCount", "10", "ImplicitConversionSameArgumentCount 1")] + [TestCase("ImplicitConversionSameArgumentCount", "10.1", "ImplicitConversionSameArgumentCount 2")] + [TestCase("ImplicitConversionSameArgumentCount2", "10", "ImplicitConversionSameArgumentCount2 1")] + [TestCase("ImplicitConversionSameArgumentCount2", "10.1", "ImplicitConversionSameArgumentCount2 2")] + public void DisambiguatesOverloadWithSameArgumentCountAndImplicitConversion(string methodName, string quantity, string expectedResult) + { + using var _ = Py.GIL(); + + dynamic module = PyModule.FromString("DisambiguatesOverloadWithSameArgumentCountAndImplicitConversion", @$" +def call_method(instance): + return instance.{methodName}(""SPY"", {quantity}, 123.4, trailingAsPercentage=True) +"); + + var instance = new OverloadsTestClass(); + var result = module.call_method(instance).As(); + + Assert.AreEqual(expectedResult, result); + } + + public class CSharpClass + { + public string CalledMethodMessage { get; private set; } + + public void Method() + { + CalledMethodMessage = "Overload 1"; + } + + public void Method(string stringArgument, decimal decimalArgument = 1.2m) + { + CalledMethodMessage = "Overload 2"; + } + + public void Method(PyObject typeArgument, decimal decimalArgument = 1.2m) + { + CalledMethodMessage = "Overload 3"; + } + } + + [Test] + public void CallsCorrectOverloadWithoutErrors() + { + using var _ = Py.GIL(); + + var module = PyModule.FromString("CallsCorrectOverloadWithoutErrors", @" +from clr import AddReference +AddReference(""System"") +AddReference(""Python.EmbeddingTest"") +from Python.EmbeddingTest import * + +class PythonModel(TestMethodBinder.CSharpModel): + pass + +def call_method(instance): + instance.Method(PythonModel, decimalArgument=1.234) +"); + + var instance = new CSharpClass(); + using var pyInstance = instance.ToPython(); + + Assert.DoesNotThrow(() => + { + module.GetAttr("call_method").Invoke(pyInstance); + }); + + Assert.AreEqual("Overload 3", instance.CalledMethodMessage); + + Assert.IsFalse(Exceptions.ErrorOccurred()); + } + + public class CSharpClass2 + { + public string CalledMethodMessage { get; private set; } + + public void Method() + { + CalledMethodMessage = "Overload 1"; + } + + public void Method(CSharpClass csharpClassArgument, decimal decimalArgument = 1.2m, PyObject pyObjectKArgument = null) + { + CalledMethodMessage = "Overload 2"; + } + + public void Method(PyObject pyObjectArgument, decimal decimalArgument = 1.2m, object objectArgument = null) + { + CalledMethodMessage = "Overload 3"; + } // This must be matched when passing just a single argument and it's a PyObject, // event though the PyObject kwarg in the second overload has more precedence. - // But since it will not be passed, this overload must be called. - public void Method(PyObject pyObjectArgument, decimal decimalArgument = 1.2m, int intArgument = 0) - { - CalledMethodMessage = "Overload 4"; - } - } - - [Test] - public void PyObjectArgsHavePrecedenceOverOtherTypes() - { - using var _ = Py.GIL(); - - var instance = new CSharpClass2(); + // But since it will not be passed, this overload must be called. + public void Method(PyObject pyObjectArgument, decimal decimalArgument = 1.2m, int intArgument = 0) + { + CalledMethodMessage = "Overload 4"; + } + } + + [Test] + public void PyObjectArgsHavePrecedenceOverOtherTypes() + { + using var _ = Py.GIL(); + + var instance = new CSharpClass2(); using var pyInstance = instance.ToPython(); - using var pyArg = new CSharpClass().ToPython(); - - Assert.DoesNotThrow(() => + using var pyArg = new CSharpClass().ToPython(); + + Assert.DoesNotThrow(() => { // We are passing a PyObject and not using the named arguments, // that overload must be called without converting the PyObject to CSharpClass - pyInstance.InvokeMethod("Method", pyArg); - }); - - Assert.AreEqual("Overload 4", instance.CalledMethodMessage); - - Assert.IsFalse(Exceptions.ErrorOccurred()); - } - - [Test] - public void BindsConstructorToSnakeCasedArgumentsVersion([Values] bool useCamelCase, [Values] bool passOptionalArgument) - { - using var _ = Py.GIL(); - - var argument1Name = useCamelCase ? "someArgument" : "some_argument"; - var argument2Name = useCamelCase ? "anotherArgument" : "another_argument"; - var argument2Code = passOptionalArgument ? $", {argument2Name}=\"another argument value\"" : ""; - - var module = PyModule.FromString("BindsConstructorToSnakeCasedArgumentsVersion", @$" -from clr import AddReference -AddReference(""System"") -from Python.EmbeddingTest import * - -def create_instance(): - return TestMethodBinder.CSharpModel({argument1Name}=1{argument2Code}) -"); - var exception = Assert.Throws(() => module.GetAttr("create_instance").Invoke()); - var sourceException = exception.InnerException; - Assert.IsInstanceOf(sourceException); - - 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); - } - - [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) - { - return test.Month; - } - - public class CSharpModel - { - public static string MethodCalled { get; set; } - public static dynamic ProvidedArgument; - public List SomeList { get; set; } - - public CSharpModel() - { - SomeList = new List - { - new TestImplicitConversion() - }; - } - - public CSharpModel(int someArgument, string anotherArgument = "another argument default value") - { - throw new NotImplementedException($"Constructor with arguments: someArgument={someArgument}. anotherArgument=\"{anotherArgument}\""); - } - - public void TestList(List conversions) - { - if (!conversions.Any()) - { - throw new ArgumentException("We expect at least an instance"); - } - } - - public void TestEnumerable(IEnumerable conversions) - { - if (!conversions.Any()) - { - throw new ArgumentException("We expect at least an instance"); - } - } - - public bool SomeMethod() - { - return true; - } - - public virtual string OnlyClass(TestImplicitConversion data) - { - return "OnlyClass impl"; - } - - public virtual string OnlyString(string data) - { - return "OnlyString impl: " + data; - } - - public virtual string InvokeModel(string data) - { - return "string impl: " + data; - } - - public virtual string InvokeModel(TestImplicitConversion data) - { - return "TestImplicitConversion impl"; - } - - public void NumericalArgumentMethod(int value) - { - ProvidedArgument = value; - } - public void NumericalArgumentMethod(float value) - { - ProvidedArgument = value; - } - public void NumericalArgumentMethod(double value) - { - ProvidedArgument = value; - } - public void NumericalArgumentMethod(decimal value) - { - ProvidedArgument = value; - } - public void EnumerableKeyValuePair(IEnumerable> value) - { - ProvidedArgument = value; - } - public void ListKeyValuePair(List> value) - { - ProvidedArgument = value; - } - - public void MethodWithParams(decimal value, params string[] argument) - { - - } - - public void ListReadOnlyCollection(IReadOnlyCollection collection) - { - MethodCalled = "List(IReadOnlyCollection collection)"; - } - public void List(List collection) - { - MethodCalled = "List(List collection)"; - } - public void ListEnumerable(IEnumerable collection) - { - MethodCalled = "List(IEnumerable collection)"; - } - - private static void AssertErrorNotOccurred() - { - using (Py.GIL()) - { - if (Exceptions.ErrorOccurred()) - { - throw new Exception("Error occurred"); - } - } - } - - public static void MethodDateTimeAndTimeSpan(CSharpModel pepe, SomeEnu @someEnu, int integer, double? jose = null, double? pinocho = null) - { - AssertErrorNotOccurred(); - } - public static void MethodDateTimeAndTimeSpan(CSharpModel pepe, DateTime dateTime, SomeEnu someEnu, double? jose = null, double? pinocho = null) - { - AssertErrorNotOccurred(); - } - public static void MethodDateTimeAndTimeSpan(CSharpModel pepe, TimeSpan timeSpan, SomeEnu someEnu, double? jose = null, double? pinocho = null) - { - AssertErrorNotOccurred(); - } - public static void MethodDateTimeAndTimeSpan(CSharpModel pepe, Func func, SomeEnu someEnu, double? jose = null, double? pinocho = null) - { - AssertErrorNotOccurred(); - } - } - - public class TestImplicitConversion - { - public static implicit operator string(TestImplicitConversion symbol) - { - return "implicit to string"; - } - public static implicit operator TestImplicitConversion(string symbol) - { - return new TestImplicitConversion(); - } - } - - public class ErroredImplicitConversion - { - public static implicit operator string(ErroredImplicitConversion symbol) - { - throw new ArgumentException(); - } - public static implicit operator ErroredImplicitConversion(string symbol) - { - throw new ArgumentException(); - } - } - - public class GenericClassBase - where J : class - { - public int Value = 0; - - public void TestNonStaticGenericMethod(GenericClassBase test) - where T : class - { - test.Value = 1; - } - } - - // Used to test that when a generic option is available but the parameter is already typed it doesn't - // match to the wrong one. This is an example of a typed generic parameter - public static void TestGenericMethod(GenericClassBase test) - { - test.Value = 15; - } - - public static void TestGenericMethod(GenericClassBase test) - where T : class - { - test.Value = 1; - } - - // Used in test to verify non-generic is bound and used when generic option is also available - public static void TestGenericMethod(TestGenericClass3 class3) - { - class3.Value = 10; - } - - // Used in test to verify generic binding when converted PyTypes are involved (timedelta -> TimeSpan) - public static void TestGenericMethod(GenericClassBase test, TimeSpan span) - where T : class - { - test.Value = span.Hours; - } - - // Used in test to verify generic binding when defaults are used - public static void TestGenericMethodWithDefault(GenericClassBase test, int value = 25) - where T : class - { - test.Value = value; - } - - // Used in test to verify generic binding when null defaults are used - public static void TestGenericMethodWithNullDefault(GenericClassBase test, Object testObj = null) - where T : class - { - if (testObj == null) - { - test.Value = 10; - } - else - { - test.Value = 20; - } - } - - public class ReferenceClass1 - { } - - public class ReferenceClass2 - { } - - public class ReferenceClass3 - { } - - public class TestGenericClass1 : GenericClassBase - { } - - public class TestGenericClass2 : GenericClassBase - { } - - public class TestGenericClass3 : GenericClassBase - { } - - public class TestGenericClass4 : GenericClassBase - { } - - public class MultipleGenericClassBase - where T : class - where K : class - { - public int Value = 0; - } - - public static void TestMultipleGenericMethod(MultipleGenericClassBase test) - where T : class - where K : class - { - test.Value = 1; - } - - public class TestMultipleGenericClass1 : MultipleGenericClassBase - { } - - public class TestMultipleGenericClass2 : MultipleGenericClassBase - { } - - public static void TestMultipleGenericParamsMethod(GenericClassBase singleGeneric, MultipleGenericClassBase doubleGeneric) - where T : class - where K : class - { - singleGeneric.Value = 1; - doubleGeneric.Value = 1; - } - - public static void TestMultipleGenericParamsMethod2(GenericClassBase singleGeneric, MultipleGenericClassBase doubleGeneric) - where T : class - where K : class - { - singleGeneric.Value = 1; - doubleGeneric.Value = 1; - } - - public enum SomeEnu - { - A = 1, - B = 2, - } - } -} + pyInstance.InvokeMethod("Method", pyArg); + }); + + Assert.AreEqual("Overload 4", instance.CalledMethodMessage); + + Assert.IsFalse(Exceptions.ErrorOccurred()); + } + + [Test] + public void BindsConstructorToSnakeCasedArgumentsVersion([Values] bool useCamelCase, [Values] bool passOptionalArgument) + { + using var _ = Py.GIL(); + + var argument1Name = useCamelCase ? "someArgument" : "some_argument"; + var argument2Name = useCamelCase ? "anotherArgument" : "another_argument"; + var argument2Code = passOptionalArgument ? $", {argument2Name}=\"another argument value\"" : ""; + + var module = PyModule.FromString("BindsConstructorToSnakeCasedArgumentsVersion", @$" +from clr import AddReference +AddReference(""System"") +from Python.EmbeddingTest import * + +def create_instance(): + return TestMethodBinder.CSharpModel({argument1Name}=1{argument2Code}) +"); + var exception = Assert.Throws(() => module.GetAttr("create_instance").Invoke()); + var sourceException = exception.InnerException; + Assert.IsInstanceOf(sourceException); + + 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); + } + + [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) + { + return test.Month; + } + + public class CSharpModel + { + public static string MethodCalled { get; set; } + public static dynamic ProvidedArgument; + public List SomeList { get; set; } + + public CSharpModel() + { + SomeList = new List + { + new TestImplicitConversion() + }; + } + + public CSharpModel(int someArgument, string anotherArgument = "another argument default value") + { + throw new NotImplementedException($"Constructor with arguments: someArgument={someArgument}. anotherArgument=\"{anotherArgument}\""); + } + + public void TestList(List conversions) + { + if (!conversions.Any()) + { + throw new ArgumentException("We expect at least an instance"); + } + } + + public void TestEnumerable(IEnumerable conversions) + { + if (!conversions.Any()) + { + throw new ArgumentException("We expect at least an instance"); + } + } + + public bool SomeMethod() + { + return true; + } + + public virtual string OnlyClass(TestImplicitConversion data) + { + return "OnlyClass impl"; + } + + public virtual string OnlyString(string data) + { + return "OnlyString impl: " + data; + } + + public virtual string InvokeModel(string data) + { + return "string impl: " + data; + } + + public virtual string InvokeModel(TestImplicitConversion data) + { + return "TestImplicitConversion impl"; + } + + public void NumericalArgumentMethod(int value) + { + ProvidedArgument = value; + } + public void NumericalArgumentMethod(float value) + { + ProvidedArgument = value; + } + public void NumericalArgumentMethod(double value) + { + ProvidedArgument = value; + } + public void NumericalArgumentMethod(decimal value) + { + ProvidedArgument = value; + } + public void EnumerableKeyValuePair(IEnumerable> value) + { + ProvidedArgument = value; + } + public void ListKeyValuePair(List> value) + { + ProvidedArgument = value; + } + + public void MethodWithParams(decimal value, params string[] argument) + { + + } + + public void ListReadOnlyCollection(IReadOnlyCollection collection) + { + MethodCalled = "List(IReadOnlyCollection collection)"; + } + public void List(List collection) + { + MethodCalled = "List(List collection)"; + } + public void ListEnumerable(IEnumerable collection) + { + MethodCalled = "List(IEnumerable collection)"; + } + + private static void AssertErrorNotOccurred() + { + using (Py.GIL()) + { + if (Exceptions.ErrorOccurred()) + { + throw new Exception("Error occurred"); + } + } + } + + public static void MethodDateTimeAndTimeSpan(CSharpModel pepe, SomeEnu @someEnu, int integer, double? jose = null, double? pinocho = null) + { + AssertErrorNotOccurred(); + } + public static void MethodDateTimeAndTimeSpan(CSharpModel pepe, DateTime dateTime, SomeEnu someEnu, double? jose = null, double? pinocho = null) + { + AssertErrorNotOccurred(); + } + public static void MethodDateTimeAndTimeSpan(CSharpModel pepe, TimeSpan timeSpan, SomeEnu someEnu, double? jose = null, double? pinocho = null) + { + AssertErrorNotOccurred(); + } + public static void MethodDateTimeAndTimeSpan(CSharpModel pepe, Func func, SomeEnu someEnu, double? jose = null, double? pinocho = null) + { + AssertErrorNotOccurred(); + } + } + + public class TestImplicitConversion + { + public static implicit operator string(TestImplicitConversion symbol) + { + return "implicit to string"; + } + public static implicit operator TestImplicitConversion(string symbol) + { + return new TestImplicitConversion(); + } + } + + public class ErroredImplicitConversion + { + public static implicit operator string(ErroredImplicitConversion symbol) + { + throw new ArgumentException(); + } + public static implicit operator ErroredImplicitConversion(string symbol) + { + throw new ArgumentException(); + } + } + + public class GenericClassBase + where J : class + { + public int Value = 0; + + public void TestNonStaticGenericMethod(GenericClassBase test) + where T : class + { + test.Value = 1; + } + } + + // Used to test that when a generic option is available but the parameter is already typed it doesn't + // match to the wrong one. This is an example of a typed generic parameter + public static void TestGenericMethod(GenericClassBase test) + { + test.Value = 15; + } + + public static void TestGenericMethod(GenericClassBase test) + where T : class + { + test.Value = 1; + } + + // Used in test to verify non-generic is bound and used when generic option is also available + public static void TestGenericMethod(TestGenericClass3 class3) + { + class3.Value = 10; + } + + // Used in test to verify generic binding when converted PyTypes are involved (timedelta -> TimeSpan) + public static void TestGenericMethod(GenericClassBase test, TimeSpan span) + where T : class + { + test.Value = span.Hours; + } + + // Used in test to verify generic binding when defaults are used + public static void TestGenericMethodWithDefault(GenericClassBase test, int value = 25) + where T : class + { + test.Value = value; + } + + // Used in test to verify generic binding when null defaults are used + public static void TestGenericMethodWithNullDefault(GenericClassBase test, Object testObj = null) + where T : class + { + if (testObj == null) + { + test.Value = 10; + } + else + { + test.Value = 20; + } + } + + public class ReferenceClass1 + { } + + public class ReferenceClass2 + { } + + public class ReferenceClass3 + { } + + public class TestGenericClass1 : GenericClassBase + { } + + public class TestGenericClass2 : GenericClassBase + { } + + public class TestGenericClass3 : GenericClassBase + { } + + public class TestGenericClass4 : GenericClassBase + { } + + public class MultipleGenericClassBase + where T : class + where K : class + { + public int Value = 0; + } + + public static void TestMultipleGenericMethod(MultipleGenericClassBase test) + where T : class + where K : class + { + test.Value = 1; + } + + public class TestMultipleGenericClass1 : MultipleGenericClassBase + { } + + public class TestMultipleGenericClass2 : MultipleGenericClassBase + { } + + public static void TestMultipleGenericParamsMethod(GenericClassBase singleGeneric, MultipleGenericClassBase doubleGeneric) + where T : class + where K : class + { + singleGeneric.Value = 1; + doubleGeneric.Value = 1; + } + + public static void TestMultipleGenericParamsMethod2(GenericClassBase singleGeneric, MultipleGenericClassBase doubleGeneric) + where T : class + where K : class + { + singleGeneric.Value = 1; + doubleGeneric.Value = 1; + } + + public enum SomeEnu + { + A = 1, + B = 2, + } + } +} diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index bd5fe1ad7..d6503a11e 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -1,354 +1,354 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using System.Reflection; -using System.Text; - -namespace Python.Runtime -{ - /// - /// A MethodBinder encapsulates information about a (possibly overloaded) - /// managed method, and is responsible for selecting the right method given - /// a set of Python arguments. This is also used as a base class for the - /// ConstructorBinder, a minor variation used to invoke constructors. - /// - [Serializable] - internal class MethodBinder - { - [NonSerialized] - private List list; - [NonSerialized] - private static Dictionary _resolvedGenericsCache = new(); - public const bool DefaultAllowThreads = true; - public bool allow_threads = DefaultAllowThreads; - public bool init = false; - - internal MethodBinder(List list) - { - this.list = list; - } - - internal MethodBinder() - { - list = new List(); - } - - internal MethodBinder(MethodInfo mi) - { - list = new List { new MethodInformation(mi, true) }; - } - - public int Count - { - get { return list.Count; } - } - - internal void AddMethod(MethodBase m, bool isOriginal) - { - // we added a new method so we have to re sort the method list - init = false; - list.Add(new MethodInformation(m, isOriginal)); - } - - /// - /// Given a sequence of MethodInfo and a sequence of types, return the - /// MethodInfo that matches the signature represented by those types. - /// - internal static MethodBase? MatchSignature(MethodBase[] mi, Type[] tp) - { - if (tp == null) - { - return null; - } - int count = tp.Length; - foreach (MethodBase t in mi) - { - ParameterInfo[] pi = t.GetParameters(); - if (pi.Length != count) - { - continue; - } - for (var n = 0; n < pi.Length; n++) - { - if (tp[n] != pi[n].ParameterType) - { - break; - } - if (n == pi.Length - 1) - { - return t; - } - } - } - return null; - } - - /// - /// Given a sequence of MethodInfo and a sequence of type parameters, - /// return the MethodInfo that represents the matching closed generic. - /// - internal static List MatchParameters(MethodBinder binder, Type[] tp) - { - if (tp == null) - { - return null; - } - int count = tp.Length; - var result = new List(count); - foreach (var methodInformation in binder.list) - { - var t = methodInformation.MethodBase; - if (!t.IsGenericMethodDefinition) - { - continue; - } - Type[] args = t.GetGenericArguments(); - if (args.Length != count) - { - continue; - } - try - { - // MakeGenericMethod can throw ArgumentException if the type parameters do not obey the constraints. - MethodInfo method = ((MethodInfo)t).MakeGenericMethod(tp); - Exceptions.Clear(); - result.Add(new MethodInformation(method, methodInformation.IsOriginal)); - } - catch (ArgumentException e) - { - Exceptions.SetError(e); - // The error will remain set until cleared by a successful match. - } - } - return result; - } - - // Given a generic method and the argsTypes previously matched with it, - // generate the matching method - internal static MethodInfo ResolveGenericMethod(MethodInfo method, Object[] args) - { - // No need to resolve a method where generics are already assigned - if (!method.ContainsGenericParameters) - { - return method; - } - - bool shouldCache = method.DeclaringType != null; - string key = null; - - // Check our resolved generics cache first - if (shouldCache) - { - key = method.DeclaringType.AssemblyQualifiedName + method.ToString() + string.Join(",", args.Select(x => x?.GetType())); - if (_resolvedGenericsCache.TryGetValue(key, out var cachedMethod)) - { - return cachedMethod; - } - } - - // Get our matching generic types to create our method - var methodGenerics = method.GetGenericArguments().Where(x => x.IsGenericParameter).ToArray(); - var resolvedGenericsTypes = new Type[methodGenerics.Length]; - int resolvedGenerics = 0; - - var parameters = method.GetParameters(); - - // Iterate to length of ArgTypes since default args are plausible - for (int k = 0; k < args.Length; k++) - { - if (args[k] == null) - { - continue; - } - - var argType = args[k].GetType(); - var parameterType = parameters[k].ParameterType; - - // Ignore those without generic params - if (!parameterType.ContainsGenericParameters) - { - continue; - } - - // The parameters generic definition - var paramGenericDefinition = parameterType.GetGenericTypeDefinition(); - - // For the arg that matches this param index, determine the matching type for the generic - var currentType = argType; - while (currentType != null) - { - - // Check the current type for generic type definition - var genericType = currentType.IsGenericType ? currentType.GetGenericTypeDefinition() : null; - - // If the generic type matches our params generic definition, this is our match - // go ahead and match these types to this arg - if (paramGenericDefinition == genericType) - { - - // The matching generic for this method parameter - var paramGenerics = parameterType.GenericTypeArguments; - var argGenericsResolved = currentType.GenericTypeArguments; - - for (int j = 0; j < paramGenerics.Length; j++) - { - - // Get the final matching index for our resolved types array for this params generic - var index = Array.IndexOf(methodGenerics, paramGenerics[j]); - - if (resolvedGenericsTypes[index] == null) - { - // Add it, and increment our count - resolvedGenericsTypes[index] = argGenericsResolved[j]; - resolvedGenerics++; - } - else if (resolvedGenericsTypes[index] != argGenericsResolved[j]) - { - // If we have two resolved types for the same generic we have a problem - throw new ArgumentException("ResolveGenericMethod(): Generic method mismatch on argument types"); - } - } - - break; - } - - // Step up the inheritance tree - currentType = currentType.BaseType; - } - } - - try - { - if (resolvedGenerics != methodGenerics.Length) - { - throw new Exception($"ResolveGenericMethod(): Count of resolved generics {resolvedGenerics} does not match method generic count {methodGenerics.Length}."); - } - - method = method.MakeGenericMethod(resolvedGenericsTypes); - - if (shouldCache) - { - // Add to cache - _resolvedGenericsCache.Add(key, method); - } - } - catch (ArgumentException e) - { - // Will throw argument exception if improperly matched - Exceptions.SetError(e); - } - - return method; - } - - - /// - /// Given a sequence of MethodInfo and two sequences of type parameters, - /// return the MethodInfo that matches the signature and the closed generic. - /// - internal static MethodInfo MatchSignatureAndParameters(MethodBase[] mi, Type[] genericTp, Type[] sigTp) - { - if (genericTp == null || sigTp == null) - { - return null; - } - int genericCount = genericTp.Length; - int signatureCount = sigTp.Length; - foreach (MethodInfo t in mi) - { - if (!t.IsGenericMethodDefinition) - { - continue; - } - Type[] genericArgs = t.GetGenericArguments(); - if (genericArgs.Length != genericCount) - { - continue; - } - ParameterInfo[] pi = t.GetParameters(); - if (pi.Length != signatureCount) - { - continue; - } - for (var n = 0; n < pi.Length; n++) - { - if (sigTp[n] != pi[n].ParameterType) - { - break; - } - if (n == pi.Length - 1) - { - MethodInfo match = t; - if (match.IsGenericMethodDefinition) - { - // FIXME: typeArgs not used - Type[] typeArgs = match.GetGenericArguments(); - return match.MakeGenericMethod(genericTp); - } - return match; - } - } - } - return null; - } - - - /// - /// Return the array of MethodInfo for this method. The result array - /// is arranged in order of precedence (done lazily to avoid doing it - /// at all for methods that are never called). - /// - internal List GetMethods() - { - if (!init) - { - // I'm sure this could be made more efficient. - list.Sort(new MethodSorter()); - init = true; - } - return list; - } - - /// - /// Precedence algorithm largely lifted from Jython - the concerns are - /// generally the same so we'll start with this and tweak as necessary. - /// - /// - /// Based from Jython `org.python.core.ReflectedArgs.precedence` - /// See: https://github.com/jythontools/jython/blob/master/src/org/python/core/ReflectedArgs.java#L192 - /// - private static int GetPrecedence(MethodInformation methodInformation) - { - ParameterInfo[] pi = methodInformation.ParameterInfo; - var mi = methodInformation.MethodBase; - int val = mi.IsStatic ? 3000 : 0; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Reflection; +using System.Text; + +namespace Python.Runtime +{ + /// + /// A MethodBinder encapsulates information about a (possibly overloaded) + /// managed method, and is responsible for selecting the right method given + /// a set of Python arguments. This is also used as a base class for the + /// ConstructorBinder, a minor variation used to invoke constructors. + /// + [Serializable] + internal class MethodBinder + { + [NonSerialized] + private List list; + [NonSerialized] + private static Dictionary _resolvedGenericsCache = new(); + public const bool DefaultAllowThreads = true; + public bool allow_threads = DefaultAllowThreads; + public bool init = false; + + internal MethodBinder(List list) + { + this.list = list; + } + + internal MethodBinder() + { + list = new List(); + } + + internal MethodBinder(MethodInfo mi) + { + list = new List { new MethodInformation(mi, true) }; + } + + public int Count + { + get { return list.Count; } + } + + internal void AddMethod(MethodBase m, bool isOriginal) + { + // we added a new method so we have to re sort the method list + init = false; + list.Add(new MethodInformation(m, isOriginal)); + } + + /// + /// Given a sequence of MethodInfo and a sequence of types, return the + /// MethodInfo that matches the signature represented by those types. + /// + internal static MethodBase? MatchSignature(MethodBase[] mi, Type[] tp) + { + if (tp == null) + { + return null; + } + int count = tp.Length; + foreach (MethodBase t in mi) + { + ParameterInfo[] pi = t.GetParameters(); + if (pi.Length != count) + { + continue; + } + for (var n = 0; n < pi.Length; n++) + { + if (tp[n] != pi[n].ParameterType) + { + break; + } + if (n == pi.Length - 1) + { + return t; + } + } + } + return null; + } + + /// + /// Given a sequence of MethodInfo and a sequence of type parameters, + /// return the MethodInfo that represents the matching closed generic. + /// + internal static List MatchParameters(MethodBinder binder, Type[] tp) + { + if (tp == null) + { + return null; + } + int count = tp.Length; + var result = new List(count); + foreach (var methodInformation in binder.list) + { + var t = methodInformation.MethodBase; + if (!t.IsGenericMethodDefinition) + { + continue; + } + Type[] args = t.GetGenericArguments(); + if (args.Length != count) + { + continue; + } + try + { + // MakeGenericMethod can throw ArgumentException if the type parameters do not obey the constraints. + MethodInfo method = ((MethodInfo)t).MakeGenericMethod(tp); + Exceptions.Clear(); + result.Add(new MethodInformation(method, methodInformation.IsOriginal)); + } + catch (ArgumentException e) + { + Exceptions.SetError(e); + // The error will remain set until cleared by a successful match. + } + } + return result; + } + + // Given a generic method and the argsTypes previously matched with it, + // generate the matching method + internal static MethodInfo ResolveGenericMethod(MethodInfo method, Object[] args) + { + // No need to resolve a method where generics are already assigned + if (!method.ContainsGenericParameters) + { + return method; + } + + bool shouldCache = method.DeclaringType != null; + string key = null; + + // Check our resolved generics cache first + if (shouldCache) + { + key = method.DeclaringType.AssemblyQualifiedName + method.ToString() + string.Join(",", args.Select(x => x?.GetType())); + if (_resolvedGenericsCache.TryGetValue(key, out var cachedMethod)) + { + return cachedMethod; + } + } + + // Get our matching generic types to create our method + var methodGenerics = method.GetGenericArguments().Where(x => x.IsGenericParameter).ToArray(); + var resolvedGenericsTypes = new Type[methodGenerics.Length]; + int resolvedGenerics = 0; + + var parameters = method.GetParameters(); + + // Iterate to length of ArgTypes since default args are plausible + for (int k = 0; k < args.Length; k++) + { + if (args[k] == null) + { + continue; + } + + var argType = args[k].GetType(); + var parameterType = parameters[k].ParameterType; + + // Ignore those without generic params + if (!parameterType.ContainsGenericParameters) + { + continue; + } + + // The parameters generic definition + var paramGenericDefinition = parameterType.GetGenericTypeDefinition(); + + // For the arg that matches this param index, determine the matching type for the generic + var currentType = argType; + while (currentType != null) + { + + // Check the current type for generic type definition + var genericType = currentType.IsGenericType ? currentType.GetGenericTypeDefinition() : null; + + // If the generic type matches our params generic definition, this is our match + // go ahead and match these types to this arg + if (paramGenericDefinition == genericType) + { + + // The matching generic for this method parameter + var paramGenerics = parameterType.GenericTypeArguments; + var argGenericsResolved = currentType.GenericTypeArguments; + + for (int j = 0; j < paramGenerics.Length; j++) + { + + // Get the final matching index for our resolved types array for this params generic + var index = Array.IndexOf(methodGenerics, paramGenerics[j]); + + if (resolvedGenericsTypes[index] == null) + { + // Add it, and increment our count + resolvedGenericsTypes[index] = argGenericsResolved[j]; + resolvedGenerics++; + } + else if (resolvedGenericsTypes[index] != argGenericsResolved[j]) + { + // If we have two resolved types for the same generic we have a problem + throw new ArgumentException("ResolveGenericMethod(): Generic method mismatch on argument types"); + } + } + + break; + } + + // Step up the inheritance tree + currentType = currentType.BaseType; + } + } + + try + { + if (resolvedGenerics != methodGenerics.Length) + { + throw new Exception($"ResolveGenericMethod(): Count of resolved generics {resolvedGenerics} does not match method generic count {methodGenerics.Length}."); + } + + method = method.MakeGenericMethod(resolvedGenericsTypes); + + if (shouldCache) + { + // Add to cache + _resolvedGenericsCache.Add(key, method); + } + } + catch (ArgumentException e) + { + // Will throw argument exception if improperly matched + Exceptions.SetError(e); + } + + return method; + } + + + /// + /// Given a sequence of MethodInfo and two sequences of type parameters, + /// return the MethodInfo that matches the signature and the closed generic. + /// + internal static MethodInfo MatchSignatureAndParameters(MethodBase[] mi, Type[] genericTp, Type[] sigTp) + { + if (genericTp == null || sigTp == null) + { + return null; + } + int genericCount = genericTp.Length; + int signatureCount = sigTp.Length; + foreach (MethodInfo t in mi) + { + if (!t.IsGenericMethodDefinition) + { + continue; + } + Type[] genericArgs = t.GetGenericArguments(); + if (genericArgs.Length != genericCount) + { + continue; + } + ParameterInfo[] pi = t.GetParameters(); + if (pi.Length != signatureCount) + { + continue; + } + for (var n = 0; n < pi.Length; n++) + { + if (sigTp[n] != pi[n].ParameterType) + { + break; + } + if (n == pi.Length - 1) + { + MethodInfo match = t; + if (match.IsGenericMethodDefinition) + { + // FIXME: typeArgs not used + Type[] typeArgs = match.GetGenericArguments(); + return match.MakeGenericMethod(genericTp); + } + return match; + } + } + } + return null; + } + + + /// + /// Return the array of MethodInfo for this method. The result array + /// is arranged in order of precedence (done lazily to avoid doing it + /// at all for methods that are never called). + /// + internal List GetMethods() + { + if (!init) + { + // I'm sure this could be made more efficient. + list.Sort(new MethodSorter()); + init = true; + } + return list; + } + + /// + /// Precedence algorithm largely lifted from Jython - the concerns are + /// generally the same so we'll start with this and tweak as necessary. + /// + /// + /// Based from Jython `org.python.core.ReflectedArgs.precedence` + /// See: https://github.com/jythontools/jython/blob/master/src/org/python/core/ReflectedArgs.java#L192 + /// + private static int GetPrecedence(MethodInformation methodInformation) + { + ParameterInfo[] pi = methodInformation.ParameterInfo; + var mi = methodInformation.MethodBase; + int val = mi.IsStatic ? 3000 : 0; int num = pi.Length; - var isOperatorMethod = OperatorMethod.IsOperatorMethod(methodInformation.MethodBase); - - val += mi.IsGenericMethod ? 1 : 0; - for (var i = 0; i < num; i++) - { - val += ArgPrecedence(pi[i].ParameterType, isOperatorMethod); - } - - var info = mi as MethodInfo; - if (info != null) - { - val += ArgPrecedence(info.ReturnType, isOperatorMethod); - if (mi.DeclaringType == mi.ReflectedType) - { - val += methodInformation.IsOriginal ? 0 : 300000; - } - else - { - val += methodInformation.IsOriginal ? 2000 : 400000; - } - } - - return val; + var isOperatorMethod = OperatorMethod.IsOperatorMethod(methodInformation.MethodBase); + + val += mi.IsGenericMethod ? 1 : 0; + for (var i = 0; i < num; i++) + { + val += ArgPrecedence(pi[i].ParameterType, isOperatorMethod); + } + + var info = mi as MethodInfo; + if (info != null) + { + val += ArgPrecedence(info.ReturnType, isOperatorMethod); + if (mi.DeclaringType == mi.ReflectedType) + { + val += methodInformation.IsOriginal ? 0 : 300000; + } + else + { + val += methodInformation.IsOriginal ? 2000 : 400000; + } + } + + return val; } /// @@ -375,109 +375,109 @@ private static int GetMatchedArgumentsPrecedence(MethodInformation method, int m val += ArgPrecedence(info.ReturnType, isOperatorMethod); } return val; - } - - /// - /// Return a precedence value for a particular Type object. - /// - internal static int ArgPrecedence(Type t, bool isOperatorMethod) - { - Type objectType = typeof(object); - if (t == objectType) - { - return 3000; - } - - if (t.IsAssignableFrom(typeof(PyObject)) && !isOperatorMethod) - { - return -3000; - } - - if (t.IsArray) - { - Type e = t.GetElementType(); - if (e == objectType) - { - return 2500; - } - return 100 + ArgPrecedence(e, isOperatorMethod); - } - - TypeCode tc = Type.GetTypeCode(t); - // TODO: Clean up - switch (tc) - { - case TypeCode.Object: - return 1; - - // we place higher precision methods at the top - case TypeCode.Decimal: - return 2; - case TypeCode.Double: - return 3; - case TypeCode.Single: - return 4; - - case TypeCode.Int64: - return 21; - case TypeCode.Int32: - return 22; - case TypeCode.Int16: - return 23; - case TypeCode.UInt64: - return 24; - case TypeCode.UInt32: - return 25; - case TypeCode.UInt16: - return 26; - case TypeCode.Char: - return 27; - case TypeCode.Byte: - return 28; - case TypeCode.SByte: - return 29; - - case TypeCode.String: - return 30; - - case TypeCode.Boolean: - return 40; - } - - return 2000; - } - - /// - /// Bind the given Python instance and arguments to a particular method - /// overload and return a structure that contains the converted Python - /// instance, converted arguments and the correct method to call. - /// - internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw) - { - return Bind(inst, args, kw, null); - } - - internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase info) - { - // If we have KWArgs create dictionary and collect them - Dictionary kwArgDict = null; - if (kw != null) - { - 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); - for (int i = 0; i < pyKwArgsCount; ++i) - { - var keyStr = Runtime.GetManagedString(Runtime.PyList_GetItem(keylist.Borrow(), i)); - BorrowedReference value = Runtime.PyList_GetItem(valueList.Borrow(), i); - kwArgDict[keyStr!] = new PyObject(value); - } - } - var hasNamedArgs = kwArgDict != null && kwArgDict.Count > 0; - - // Fetch our methods we are going to attempt to match and bind too. - var methods = info == null ? GetMethods() + } + + /// + /// Return a precedence value for a particular Type object. + /// + internal static int ArgPrecedence(Type t, bool isOperatorMethod) + { + Type objectType = typeof(object); + if (t == objectType) + { + return 3000; + } + + if (t.IsAssignableFrom(typeof(PyObject)) && !isOperatorMethod) + { + return -3000; + } + + if (t.IsArray) + { + Type e = t.GetElementType(); + if (e == objectType) + { + return 2500; + } + return 100 + ArgPrecedence(e, isOperatorMethod); + } + + TypeCode tc = Type.GetTypeCode(t); + // TODO: Clean up + switch (tc) + { + case TypeCode.Object: + return 1; + + // we place higher precision methods at the top + case TypeCode.Decimal: + return 2; + case TypeCode.Double: + return 3; + case TypeCode.Single: + return 4; + + case TypeCode.Int64: + return 21; + case TypeCode.Int32: + return 22; + case TypeCode.Int16: + return 23; + case TypeCode.UInt64: + return 24; + case TypeCode.UInt32: + return 25; + case TypeCode.UInt16: + return 26; + case TypeCode.Char: + return 27; + case TypeCode.Byte: + return 28; + case TypeCode.SByte: + return 29; + + case TypeCode.String: + return 30; + + case TypeCode.Boolean: + return 40; + } + + return 2000; + } + + /// + /// Bind the given Python instance and arguments to a particular method + /// overload and return a structure that contains the converted Python + /// instance, converted arguments and the correct method to call. + /// + internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw) + { + return Bind(inst, args, kw, null); + } + + internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase info) + { + // If we have KWArgs create dictionary and collect them + Dictionary kwArgDict = null; + if (kw != null) + { + 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); + for (int i = 0; i < pyKwArgsCount; ++i) + { + var keyStr = Runtime.GetManagedString(Runtime.PyList_GetItem(keylist.Borrow(), i)); + BorrowedReference value = Runtime.PyList_GetItem(valueList.Borrow(), i); + kwArgDict[keyStr!] = new PyObject(value); + } + } + var hasNamedArgs = kwArgDict != null && kwArgDict.Count > 0; + + // Fetch our methods we are going to attempt to match and bind too. + var methods = info == null ? GetMethods() : new List(1) { new MethodInformation(info, true) }; if (methods.Any(m => m.MethodBase.Name.StartsWith("History"))) @@ -485,276 +485,276 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe } - int pyArgCount = (int)Runtime.PyTuple_Size(args); - var matches = new List(methods.Count); - List matchesUsingImplicitConversion = null; - - for (var i = 0; i < methods.Count; i++) - { - var methodInformation = methods[i]; - // Relevant method variables - var mi = methodInformation.MethodBase; - var pi = methodInformation.ParameterInfo; - // Avoid accessing the parameter names property unless necessary - var paramNames = hasNamedArgs ? methodInformation.ParameterNames : Array.Empty(); - - // Special case for operators - bool isOperator = OperatorMethod.IsOperatorMethod(mi); - // Binary operator methods will have 2 CLR args but only one Python arg - // (unary operators will have 1 less each), since Python operator methods are bound. - isOperator = isOperator && pyArgCount == pi.Length - 1; - bool isReverse = isOperator && OperatorMethod.IsReverse((MethodInfo)mi); // Only cast if isOperator. - if (isReverse && OperatorMethod.IsComparisonOp((MethodInfo)mi)) - continue; // Comparison operators in Python have no reverse mode. - // Preprocessing pi to remove either the first or second argument. - if (isOperator && !isReverse) - { - // The first Python arg is the right operand, while the bound instance is the left. - // We need to skip the first (left operand) CLR argument. - pi = pi.Skip(1).ToArray(); - } - else if (isOperator && isReverse) - { - // The first Python arg is the left operand. - // We need to take the first CLR argument. - pi = pi.Take(1).ToArray(); - } - - // Must be done after IsOperator section - int clrArgCount = pi.Length; - - if (CheckMethodArgumentsMatch(clrArgCount, - pyArgCount, - kwArgDict, - pi, - paramNames, - out bool paramsArray, - out ArrayList defaultArgList)) - { - var outs = 0; - var margs = new object[clrArgCount]; - - int paramsArrayIndex = paramsArray ? pi.Length - 1 : -1; // -1 indicates no paramsArray - var usedImplicitConversion = false; - var kwargsMatched = 0; - - // Conversion loop for each parameter - for (int paramIndex = 0; paramIndex < clrArgCount; paramIndex++) - { - PyObject tempPyObject = null; - BorrowedReference op = null; // Python object to be converted; not yet set - var parameter = pi[paramIndex]; // Clr parameter we are targeting - object arg; // Python -> Clr argument - - // Check positional arguments first and then check for named arguments and optional values - if (paramIndex >= pyArgCount) - { - var hasNamedParam = kwArgDict == null ? false : kwArgDict.TryGetValue(paramNames[paramIndex], out tempPyObject); - - // All positional arguments have been used: - // Check our KWargs for this parameter - if (hasNamedParam) - { - kwargsMatched++; - if (tempPyObject != null) - { - op = tempPyObject; - } - } - else if (parameter.IsOptional && !(hasNamedParam || (paramsArray && paramIndex == paramsArrayIndex))) - { - if (defaultArgList != null) - { - margs[paramIndex] = defaultArgList[paramIndex - pyArgCount]; - } - - continue; - } - } - - NewReference tempObject = default; - - // At this point, if op is IntPtr.Zero we don't have a KWArg and are not using default - if (op == null) - { - // If we have reached the paramIndex - if (paramsArrayIndex == paramIndex) - { - op = HandleParamsArray(args, paramsArrayIndex, pyArgCount, out tempObject); - } - else - { - op = Runtime.PyTuple_GetItem(args, paramIndex); - } - } - - // this logic below handles cases when multiple overloading methods - // are ambiguous, hence comparison between Python and CLR types - // is necessary - Type clrtype = null; - NewReference pyoptype = default; - if (methods.Count > 1) - { - pyoptype = Runtime.PyObject_Type(op); - Exceptions.Clear(); - if (!pyoptype.IsNull()) - { - clrtype = Converter.GetTypeByAlias(pyoptype.Borrow()); - } - pyoptype.Dispose(); - } - - - if (clrtype != null) - { - var typematch = false; - - if ((parameter.ParameterType != typeof(object)) && (parameter.ParameterType != clrtype)) - { - var pytype = Converter.GetPythonTypeByAlias(parameter.ParameterType); - pyoptype = Runtime.PyObject_Type(op); - Exceptions.Clear(); - if (!pyoptype.IsNull()) - { - if (pytype != pyoptype.Borrow()) - { - typematch = false; - } - else - { - typematch = true; - clrtype = parameter.ParameterType; - } - } - if (!typematch) - { - // this takes care of nullables - var underlyingType = Nullable.GetUnderlyingType(parameter.ParameterType); - if (underlyingType == null) - { - underlyingType = parameter.ParameterType; - } - // this takes care of enum values - TypeCode argtypecode = Type.GetTypeCode(underlyingType); - TypeCode paramtypecode = Type.GetTypeCode(clrtype); - if (argtypecode == paramtypecode) - { - typematch = true; - clrtype = parameter.ParameterType; - } - // we won't take matches using implicit conversions if there is already a match - // not using implicit conversions - else if (matches.Count == 0) - { - // accepts non-decimal numbers in decimal parameters - if (underlyingType == typeof(decimal)) - { - clrtype = parameter.ParameterType; - usedImplicitConversion |= typematch = Converter.ToManaged(op, clrtype, out arg, false); - } - if (!typematch) - { - // this takes care of implicit conversions - var opImplicit = parameter.ParameterType.GetMethod("op_Implicit", new[] { clrtype }); - if (opImplicit != null) - { - usedImplicitConversion |= typematch = opImplicit.ReturnType == parameter.ParameterType; - clrtype = parameter.ParameterType; - } - } - } - } - pyoptype.Dispose(); - if (!typematch) - { - tempObject.Dispose(); - margs = null; - break; - } - } - else - { - clrtype = parameter.ParameterType; - } - } - else - { - clrtype = parameter.ParameterType; - } - - if (parameter.IsOut || clrtype.IsByRef) - { - outs++; - } - - if (!Converter.ToManaged(op, clrtype, out arg, false)) - { - tempObject.Dispose(); - margs = null; - break; - } - tempObject.Dispose(); - - margs[paramIndex] = arg; - - } - - if (margs == null) - { - continue; - } - - if (isOperator) - { - if (inst != null) - { - if (ManagedType.GetManagedObject(inst) is CLRObject co) - { - bool isUnary = pyArgCount == 0; - // Postprocessing to extend margs. - var margsTemp = isUnary ? new object[1] : new object[2]; - // If reverse, the bound instance is the right operand. - int boundOperandIndex = isReverse ? 1 : 0; - // If reverse, the passed instance is the left operand. - int passedOperandIndex = isReverse ? 0 : 1; - margsTemp[boundOperandIndex] = co.inst; - if (!isUnary) - { - margsTemp[passedOperandIndex] = margs[0]; - } - margs = margsTemp; - } - else continue; - } - } - - var match = new MatchedMethod(kwargsMatched, margs, outs, mi); - if (usedImplicitConversion) - { - if (matchesUsingImplicitConversion == null) - { - matchesUsingImplicitConversion = new List(); - } - matchesUsingImplicitConversion.Add(match); - } - else - { - matches.Add(match); - // We don't need the matches using implicit conversion anymore, we can free the memory - matchesUsingImplicitConversion = null; - } - } - } - - if (matches.Count > 0 || (matchesUsingImplicitConversion != null && matchesUsingImplicitConversion.Count > 0)) + int pyArgCount = (int)Runtime.PyTuple_Size(args); + var matches = new List(methods.Count); + List matchesUsingImplicitConversion = null; + + for (var i = 0; i < methods.Count; i++) + { + var methodInformation = methods[i]; + // Relevant method variables + var mi = methodInformation.MethodBase; + var pi = methodInformation.ParameterInfo; + // Avoid accessing the parameter names property unless necessary + var paramNames = hasNamedArgs ? methodInformation.ParameterNames : Array.Empty(); + + // Special case for operators + bool isOperator = OperatorMethod.IsOperatorMethod(mi); + // Binary operator methods will have 2 CLR args but only one Python arg + // (unary operators will have 1 less each), since Python operator methods are bound. + isOperator = isOperator && pyArgCount == pi.Length - 1; + bool isReverse = isOperator && OperatorMethod.IsReverse((MethodInfo)mi); // Only cast if isOperator. + if (isReverse && OperatorMethod.IsComparisonOp((MethodInfo)mi)) + continue; // Comparison operators in Python have no reverse mode. + // Preprocessing pi to remove either the first or second argument. + if (isOperator && !isReverse) + { + // The first Python arg is the right operand, while the bound instance is the left. + // We need to skip the first (left operand) CLR argument. + pi = pi.Skip(1).ToArray(); + } + else if (isOperator && isReverse) + { + // The first Python arg is the left operand. + // We need to take the first CLR argument. + pi = pi.Take(1).ToArray(); + } + + // Must be done after IsOperator section + int clrArgCount = pi.Length; + + if (CheckMethodArgumentsMatch(clrArgCount, + pyArgCount, + kwArgDict, + pi, + paramNames, + out bool paramsArray, + out ArrayList defaultArgList)) + { + var outs = 0; + var margs = new object[clrArgCount]; + + int paramsArrayIndex = paramsArray ? pi.Length - 1 : -1; // -1 indicates no paramsArray + var usedImplicitConversion = false; + var kwargsMatched = 0; + + // Conversion loop for each parameter + for (int paramIndex = 0; paramIndex < clrArgCount; paramIndex++) + { + PyObject tempPyObject = null; + BorrowedReference op = null; // Python object to be converted; not yet set + var parameter = pi[paramIndex]; // Clr parameter we are targeting + object arg; // Python -> Clr argument + + // Check positional arguments first and then check for named arguments and optional values + if (paramIndex >= pyArgCount) + { + var hasNamedParam = kwArgDict == null ? false : kwArgDict.TryGetValue(paramNames[paramIndex], out tempPyObject); + + // All positional arguments have been used: + // Check our KWargs for this parameter + if (hasNamedParam) + { + kwargsMatched++; + if (tempPyObject != null) + { + op = tempPyObject; + } + } + else if (parameter.IsOptional && !(hasNamedParam || (paramsArray && paramIndex == paramsArrayIndex))) + { + if (defaultArgList != null) + { + margs[paramIndex] = defaultArgList[paramIndex - pyArgCount]; + } + + continue; + } + } + + NewReference tempObject = default; + + // At this point, if op is IntPtr.Zero we don't have a KWArg and are not using default + if (op == null) + { + // If we have reached the paramIndex + if (paramsArrayIndex == paramIndex) + { + op = HandleParamsArray(args, paramsArrayIndex, pyArgCount, out tempObject); + } + else + { + op = Runtime.PyTuple_GetItem(args, paramIndex); + } + } + + // this logic below handles cases when multiple overloading methods + // are ambiguous, hence comparison between Python and CLR types + // is necessary + Type clrtype = null; + NewReference pyoptype = default; + if (methods.Count > 1) + { + pyoptype = Runtime.PyObject_Type(op); + Exceptions.Clear(); + if (!pyoptype.IsNull()) + { + clrtype = Converter.GetTypeByAlias(pyoptype.Borrow()); + } + pyoptype.Dispose(); + } + + + if (clrtype != null) + { + var typematch = false; + + if ((parameter.ParameterType != typeof(object)) && (parameter.ParameterType != clrtype)) + { + var pytype = Converter.GetPythonTypeByAlias(parameter.ParameterType); + pyoptype = Runtime.PyObject_Type(op); + Exceptions.Clear(); + if (!pyoptype.IsNull()) + { + if (pytype != pyoptype.Borrow()) + { + typematch = false; + } + else + { + typematch = true; + clrtype = parameter.ParameterType; + } + } + if (!typematch) + { + // this takes care of nullables + var underlyingType = Nullable.GetUnderlyingType(parameter.ParameterType); + if (underlyingType == null) + { + underlyingType = parameter.ParameterType; + } + // this takes care of enum values + TypeCode argtypecode = Type.GetTypeCode(underlyingType); + TypeCode paramtypecode = Type.GetTypeCode(clrtype); + if (argtypecode == paramtypecode) + { + typematch = true; + clrtype = parameter.ParameterType; + } + // we won't take matches using implicit conversions if there is already a match + // not using implicit conversions + else if (matches.Count == 0) + { + // accepts non-decimal numbers in decimal parameters + if (underlyingType == typeof(decimal)) + { + clrtype = parameter.ParameterType; + usedImplicitConversion |= typematch = Converter.ToManaged(op, clrtype, out arg, false); + } + if (!typematch) + { + // this takes care of implicit conversions + var opImplicit = parameter.ParameterType.GetMethod("op_Implicit", new[] { clrtype }); + if (opImplicit != null) + { + usedImplicitConversion |= typematch = opImplicit.ReturnType == parameter.ParameterType; + clrtype = parameter.ParameterType; + } + } + } + } + pyoptype.Dispose(); + if (!typematch) + { + tempObject.Dispose(); + margs = null; + break; + } + } + else + { + clrtype = parameter.ParameterType; + } + } + else + { + clrtype = parameter.ParameterType; + } + + if (parameter.IsOut || clrtype.IsByRef) + { + outs++; + } + + if (!Converter.ToManaged(op, clrtype, out arg, false)) + { + tempObject.Dispose(); + margs = null; + break; + } + tempObject.Dispose(); + + margs[paramIndex] = arg; + + } + + if (margs == null) + { + continue; + } + + if (isOperator) + { + if (inst != null) + { + if (ManagedType.GetManagedObject(inst) is CLRObject co) + { + bool isUnary = pyArgCount == 0; + // Postprocessing to extend margs. + var margsTemp = isUnary ? new object[1] : new object[2]; + // If reverse, the bound instance is the right operand. + int boundOperandIndex = isReverse ? 1 : 0; + // If reverse, the passed instance is the left operand. + int passedOperandIndex = isReverse ? 0 : 1; + margsTemp[boundOperandIndex] = co.inst; + if (!isUnary) + { + margsTemp[passedOperandIndex] = margs[0]; + } + margs = margsTemp; + } + else continue; + } + } + + var match = new MatchedMethod(kwargsMatched, margs, outs, mi); + if (usedImplicitConversion) + { + if (matchesUsingImplicitConversion == null) + { + matchesUsingImplicitConversion = new List(); + } + matchesUsingImplicitConversion.Add(match); + } + else + { + matches.Add(match); + // We don't need the matches using implicit conversion anymore, we can free the memory + matchesUsingImplicitConversion = null; + } + } + } + + if (matches.Count > 0 || (matchesUsingImplicitConversion != null && matchesUsingImplicitConversion.Count > 0)) { - // We favor matches that do not use implicit conversion - var matchesTouse = matches.Count > 0 ? matches : matchesUsingImplicitConversion; - + // We favor matches that do not use implicit conversion + var matchesTouse = matches.Count > 0 ? matches : matchesUsingImplicitConversion; + // The best match would be the one with the most named arguments matched var maxKwargsMatched = matchesTouse.Max(x => x.KwargsMatched); // Don't materialize the enumerable, just enumerate twice if necessary to avoid creating a collection instance. - var bestMatches = matchesTouse.Where(x => x.KwargsMatched == maxKwargsMatched); + var bestMatches = matchesTouse.Where(x => x.KwargsMatched == maxKwargsMatched); var bestMatchesCount = bestMatches.Count(); MatchedMethod bestMatch; @@ -771,433 +771,433 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe { bestMatch = bestMatches.First(); } - - var margs = bestMatch.ManagedArgs; - var outs = bestMatch.Outs; - var mi = bestMatch.Method; - - object? target = null; - if (!mi.IsStatic && inst != null) - { - //CLRObject co = (CLRObject)ManagedType.GetManagedObject(inst); - // InvalidCastException: Unable to cast object of type - // 'Python.Runtime.ClassObject' to type 'Python.Runtime.CLRObject' - - // Sanity check: this ensures a graceful exit if someone does - // something intentionally wrong like call a non-static method - // on the class rather than on an instance of the class. - // XXX maybe better to do this before all the other rigmarole. - if (ManagedType.GetManagedObject(inst) is CLRObject co) - { - target = co.inst; - } - else - { - Exceptions.SetError(Exceptions.TypeError, "Invoked a non-static method with an invalid instance"); - return null; - } - } - - // If this match is generic we need to resolve it with our types. - // Store this generic match to be used if no others match - if (mi.IsGenericMethod) - { - mi = ResolveGenericMethod((MethodInfo)mi, margs); - } - - return new Binding(mi, target, margs, outs); - } - - return null; + + var margs = bestMatch.ManagedArgs; + var outs = bestMatch.Outs; + var mi = bestMatch.Method; + + object? target = null; + if (!mi.IsStatic && inst != null) + { + //CLRObject co = (CLRObject)ManagedType.GetManagedObject(inst); + // InvalidCastException: Unable to cast object of type + // 'Python.Runtime.ClassObject' to type 'Python.Runtime.CLRObject' + + // Sanity check: this ensures a graceful exit if someone does + // something intentionally wrong like call a non-static method + // on the class rather than on an instance of the class. + // XXX maybe better to do this before all the other rigmarole. + if (ManagedType.GetManagedObject(inst) is CLRObject co) + { + target = co.inst; + } + else + { + Exceptions.SetError(Exceptions.TypeError, "Invoked a non-static method with an invalid instance"); + return null; + } + } + + // If this match is generic we need to resolve it with our types. + // Store this generic match to be used if no others match + if (mi.IsGenericMethod) + { + mi = ResolveGenericMethod((MethodInfo)mi, margs); + } + + return new Binding(mi, target, margs, outs); + } + + return null; + } + + static BorrowedReference HandleParamsArray(BorrowedReference args, int arrayStart, int pyArgCount, out NewReference tempObject) + { + BorrowedReference op; + tempObject = default; + // for a params method, we may have a sequence or single/multiple items + // here we look to see if the item at the paramIndex is there or not + // and then if it is a sequence itself. + if ((pyArgCount - arrayStart) == 1) + { + // we only have one argument left, so we need to check it + // to see if it is a sequence or a single item + BorrowedReference item = Runtime.PyTuple_GetItem(args, arrayStart); + if (!Runtime.PyString_Check(item) && (Runtime.PySequence_Check(item) || (ManagedType.GetManagedObject(item) as CLRObject)?.inst is IEnumerable)) + { + // it's a sequence (and not a string), so we use it as the op + op = item; + } + else + { + tempObject = Runtime.PyTuple_GetSlice(args, arrayStart, pyArgCount); + op = tempObject.Borrow(); + } + } + else + { + tempObject = Runtime.PyTuple_GetSlice(args, arrayStart, pyArgCount); + op = tempObject.Borrow(); + } + return op; + } + + /// + /// This helper method will perform an initial check to determine if we found a matching + /// method based on its parameters count and type + /// + /// + /// We required both the parameters info and the parameters names to perform this check. + /// The CLR method parameters info is required to match the parameters count and type. + /// The names are required to perform an accurate match, since the method can be the snake-cased version. + /// + private bool CheckMethodArgumentsMatch(int clrArgCount, + int pyArgCount, + Dictionary kwargDict, + ParameterInfo[] parameterInfo, + string[] parameterNames, + out bool paramsArray, + out ArrayList defaultArgList) + { + var match = false; + + // Prepare our outputs + defaultArgList = null; + paramsArray = false; + if (parameterInfo.Length > 0) + { + var lastParameterInfo = parameterInfo[parameterInfo.Length - 1]; + if (lastParameterInfo.ParameterType.IsArray) + { + paramsArray = Attribute.IsDefined(lastParameterInfo, typeof(ParamArrayAttribute)); + } + } + + // First if we have anys kwargs, look at the function for matching args + if (kwargDict != null && kwargDict.Count > 0) + { + // If the method doesn't have all of these kw args, it is not a match + // Otherwise just continue on to see if it is a match + if (!kwargDict.All(x => parameterNames.Any(paramName => x.Key == paramName))) + { + return false; + } + } + + // If they have the exact same amount of args they do match + // Must check kwargs because it contains additional args + if (pyArgCount == clrArgCount && (kwargDict == null || kwargDict.Count == 0)) + { + match = true; + } + else if (pyArgCount < clrArgCount) + { + // every parameter past 'pyArgCount' must have either + // a corresponding keyword argument or a default parameter + match = true; + defaultArgList = new ArrayList(); + for (var v = pyArgCount; v < clrArgCount && match; v++) + { + if (kwargDict != null && kwargDict.ContainsKey(parameterNames[v])) + { + // we have a keyword argument for this parameter, + // no need to check for a default parameter, but put a null + // placeholder in defaultArgList + defaultArgList.Add(null); + } + else if (parameterInfo[v].IsOptional) + { + // IsOptional will be true if the parameter has a default value, + // or if the parameter has the [Optional] attribute specified. + if (parameterInfo[v].HasDefaultValue) + { + defaultArgList.Add(parameterInfo[v].DefaultValue); + } + else + { + // [OptionalAttribute] was specified for the parameter. + // See https://stackoverflow.com/questions/3416216/optionalattribute-parameters-default-value + // for rules on determining the value to pass to the parameter + var type = parameterInfo[v].ParameterType; + if (type == typeof(object)) + defaultArgList.Add(Type.Missing); + else if (type.IsValueType) + defaultArgList.Add(Activator.CreateInstance(type)); + else + defaultArgList.Add(null); + } + } + else if (!paramsArray) + { + // If there is no KWArg or Default value, then this isn't a match + match = false; + } + } + } + else if (pyArgCount > clrArgCount && clrArgCount > 0 && paramsArray) + { + // This is a `foo(params object[] bar)` style method + // We will handle the params later + match = true; + } + return match; + } + + internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw) + { + return Invoke(inst, args, kw, null, null); + } + + internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase info) + { + return Invoke(inst, args, kw, info, null); + } + + internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase info, MethodInfo[] methodinfo) + { + Binding binding = Bind(inst, args, kw, info); + object result; + IntPtr ts = IntPtr.Zero; + + if (binding == null) + { + // If we already have an exception pending, don't create a new one + if (!Exceptions.ErrorOccurred()) + { + var value = new StringBuilder("No method matches given arguments"); + if (methodinfo != null && methodinfo.Length > 0) + { + value.Append($" for {methodinfo[0].Name}"); + } + else if (list.Count > 0) + { + value.Append($" for {list[0].MethodBase.Name}"); + } + + value.Append(": "); + AppendArgumentTypes(to: value, args); + Exceptions.RaiseTypeError(value.ToString()); + } + + return default; + } + + if (allow_threads) + { + ts = PythonEngine.BeginAllowThreads(); + } + + try + { + result = binding.info.Invoke(binding.inst, BindingFlags.Default, null, binding.args, null); + } + catch (Exception e) + { + if (e.InnerException != null) + { + e = e.InnerException; + } + if (allow_threads) + { + PythonEngine.EndAllowThreads(ts); + } + Exceptions.SetError(e); + return default; + } + + if (allow_threads) + { + PythonEngine.EndAllowThreads(ts); + } + + // If there are out parameters, we return a tuple containing + // the result followed by the out parameters. If there is only + // one out parameter and the return type of the method is void, + // we return the out parameter as the result to Python (for + // code compatibility with ironpython). + + var returnType = binding.info.IsConstructor ? typeof(void) : ((MethodInfo)binding.info).ReturnType; + + if (binding.outs > 0) + { + ParameterInfo[] pi = binding.info.GetParameters(); + int c = pi.Length; + var n = 0; + + bool isVoid = returnType == typeof(void); + int tupleSize = binding.outs + (isVoid ? 0 : 1); + using var t = Runtime.PyTuple_New(tupleSize); + if (!isVoid) + { + using var v = Converter.ToPython(result, returnType); + Runtime.PyTuple_SetItem(t.Borrow(), n, v.Steal()); + n++; + } + + for (var i = 0; i < c; i++) + { + Type pt = pi[i].ParameterType; + if (pt.IsByRef) + { + using var v = Converter.ToPython(binding.args[i], pt.GetElementType()); + Runtime.PyTuple_SetItem(t.Borrow(), n, v.Steal()); + n++; + } + } + + if (binding.outs == 1 && returnType == typeof(void)) + { + BorrowedReference item = Runtime.PyTuple_GetItem(t.Borrow(), 0); + return new NewReference(item); + } + + return new NewReference(t.Borrow()); + } + + return Converter.ToPython(result, returnType); + } + + /// + /// Utility class to store the information about a + /// + [Serializable] + internal class MethodInformation + { + private ParameterInfo[] _parameterInfo; + private string[] _parametersNames; + + public MethodBase MethodBase { get; } + + public bool IsOriginal { get; set; } + + public ParameterInfo[] ParameterInfo + { + get + { + _parameterInfo ??= MethodBase.GetParameters(); + return _parameterInfo; + } + } + + public string[] ParameterNames + { + get + { + if (_parametersNames == null) + { + if (IsOriginal) + { + _parametersNames = ParameterInfo.Select(pi => pi.Name).ToArray(); + } + else + { + _parametersNames = ParameterInfo.Select(pi => pi.Name.ToSnakeCase()).ToArray(); + } + } + return _parametersNames; + } + } + + public MethodInformation(MethodBase methodBase, bool isOriginal) + { + MethodBase = methodBase; + IsOriginal = isOriginal; + } + + public override string ToString() + { + return MethodBase.ToString(); + } + } + + /// + /// Utility class to sort method info by parameter type precedence. + /// + private class MethodSorter : IComparer + { + public int Compare(MethodInformation x, MethodInformation y) + { + int p1 = GetPrecedence(x); + int p2 = GetPrecedence(y); + if (p1 < p2) + { + return -1; + } + if (p1 > p2) + { + return 1; + } + return 0; + } + } + + private readonly struct MatchedMethod + { + public int KwargsMatched { get; } + public object?[] ManagedArgs { get; } + public int Outs { get; } + public MethodBase Method { get; } + + public MatchedMethod(int kwargsMatched, object?[] margs, int outs, MethodBase mb) + { + KwargsMatched = kwargsMatched; + ManagedArgs = margs; + Outs = outs; + Method = mb; + } + } + + protected static void AppendArgumentTypes(StringBuilder to, BorrowedReference args) + { + long argCount = Runtime.PyTuple_Size(args); + to.Append("("); + for (nint argIndex = 0; argIndex < argCount; argIndex++) + { + BorrowedReference arg = Runtime.PyTuple_GetItem(args, argIndex); + if (arg != null) + { + BorrowedReference type = Runtime.PyObject_TYPE(arg); + if (type != null) + { + using var description = Runtime.PyObject_Str(type); + if (description.IsNull()) + { + Exceptions.Clear(); + to.Append(Util.BadStr); + } + else + { + to.Append(Runtime.GetManagedString(description.Borrow())); + } + } + } + + if (argIndex + 1 < argCount) + to.Append(", "); + } + to.Append(')'); } + } + + + /// + /// A Binding is a utility instance that bundles together a MethodInfo + /// representing a method to call, a (possibly null) target instance for + /// the call, and the arguments for the call (all as managed values). + /// + internal class Binding + { + public MethodBase info; + public object[] args; + public object inst; + public int outs; - static BorrowedReference HandleParamsArray(BorrowedReference args, int arrayStart, int pyArgCount, out NewReference tempObject) - { - BorrowedReference op; - tempObject = default; - // for a params method, we may have a sequence or single/multiple items - // here we look to see if the item at the paramIndex is there or not - // and then if it is a sequence itself. - if ((pyArgCount - arrayStart) == 1) - { - // we only have one argument left, so we need to check it - // to see if it is a sequence or a single item - BorrowedReference item = Runtime.PyTuple_GetItem(args, arrayStart); - if (!Runtime.PyString_Check(item) && (Runtime.PySequence_Check(item) || (ManagedType.GetManagedObject(item) as CLRObject)?.inst is IEnumerable)) - { - // it's a sequence (and not a string), so we use it as the op - op = item; - } - else - { - tempObject = Runtime.PyTuple_GetSlice(args, arrayStart, pyArgCount); - op = tempObject.Borrow(); - } - } - else - { - tempObject = Runtime.PyTuple_GetSlice(args, arrayStart, pyArgCount); - op = tempObject.Borrow(); - } - return op; - } - - /// - /// This helper method will perform an initial check to determine if we found a matching - /// method based on its parameters count and type - /// - /// - /// We required both the parameters info and the parameters names to perform this check. - /// The CLR method parameters info is required to match the parameters count and type. - /// The names are required to perform an accurate match, since the method can be the snake-cased version. - /// - private bool CheckMethodArgumentsMatch(int clrArgCount, - int pyArgCount, - Dictionary kwargDict, - ParameterInfo[] parameterInfo, - string[] parameterNames, - out bool paramsArray, - out ArrayList defaultArgList) - { - var match = false; - - // Prepare our outputs - defaultArgList = null; - paramsArray = false; - if (parameterInfo.Length > 0) - { - var lastParameterInfo = parameterInfo[parameterInfo.Length - 1]; - if (lastParameterInfo.ParameterType.IsArray) - { - paramsArray = Attribute.IsDefined(lastParameterInfo, typeof(ParamArrayAttribute)); - } - } - - // First if we have anys kwargs, look at the function for matching args - if (kwargDict != null && kwargDict.Count > 0) - { - // If the method doesn't have all of these kw args, it is not a match - // Otherwise just continue on to see if it is a match - if (!kwargDict.All(x => parameterNames.Any(paramName => x.Key == paramName))) - { - return false; - } - } - - // If they have the exact same amount of args they do match - // Must check kwargs because it contains additional args - if (pyArgCount == clrArgCount && (kwargDict == null || kwargDict.Count == 0)) - { - match = true; - } - else if (pyArgCount < clrArgCount) - { - // every parameter past 'pyArgCount' must have either - // a corresponding keyword argument or a default parameter - match = true; - defaultArgList = new ArrayList(); - for (var v = pyArgCount; v < clrArgCount && match; v++) - { - if (kwargDict != null && kwargDict.ContainsKey(parameterNames[v])) - { - // we have a keyword argument for this parameter, - // no need to check for a default parameter, but put a null - // placeholder in defaultArgList - defaultArgList.Add(null); - } - else if (parameterInfo[v].IsOptional) - { - // IsOptional will be true if the parameter has a default value, - // or if the parameter has the [Optional] attribute specified. - if (parameterInfo[v].HasDefaultValue) - { - defaultArgList.Add(parameterInfo[v].DefaultValue); - } - else - { - // [OptionalAttribute] was specified for the parameter. - // See https://stackoverflow.com/questions/3416216/optionalattribute-parameters-default-value - // for rules on determining the value to pass to the parameter - var type = parameterInfo[v].ParameterType; - if (type == typeof(object)) - defaultArgList.Add(Type.Missing); - else if (type.IsValueType) - defaultArgList.Add(Activator.CreateInstance(type)); - else - defaultArgList.Add(null); - } - } - else if (!paramsArray) - { - // If there is no KWArg or Default value, then this isn't a match - match = false; - } - } - } - else if (pyArgCount > clrArgCount && clrArgCount > 0 && paramsArray) - { - // This is a `foo(params object[] bar)` style method - // We will handle the params later - match = true; - } - return match; - } - - internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw) - { - return Invoke(inst, args, kw, null, null); - } - - internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase info) - { - return Invoke(inst, args, kw, info, null); - } - - internal virtual NewReference Invoke(BorrowedReference inst, BorrowedReference args, BorrowedReference kw, MethodBase info, MethodInfo[] methodinfo) - { - Binding binding = Bind(inst, args, kw, info); - object result; - IntPtr ts = IntPtr.Zero; - - if (binding == null) - { - // If we already have an exception pending, don't create a new one - if (!Exceptions.ErrorOccurred()) - { - var value = new StringBuilder("No method matches given arguments"); - if (methodinfo != null && methodinfo.Length > 0) - { - value.Append($" for {methodinfo[0].Name}"); - } - else if (list.Count > 0) - { - value.Append($" for {list[0].MethodBase.Name}"); - } - - value.Append(": "); - AppendArgumentTypes(to: value, args); - Exceptions.RaiseTypeError(value.ToString()); - } - - return default; - } - - if (allow_threads) - { - ts = PythonEngine.BeginAllowThreads(); - } - - try - { - result = binding.info.Invoke(binding.inst, BindingFlags.Default, null, binding.args, null); - } - catch (Exception e) - { - if (e.InnerException != null) - { - e = e.InnerException; - } - if (allow_threads) - { - PythonEngine.EndAllowThreads(ts); - } - Exceptions.SetError(e); - return default; - } - - if (allow_threads) - { - PythonEngine.EndAllowThreads(ts); - } - - // If there are out parameters, we return a tuple containing - // the result followed by the out parameters. If there is only - // one out parameter and the return type of the method is void, - // we return the out parameter as the result to Python (for - // code compatibility with ironpython). - - var returnType = binding.info.IsConstructor ? typeof(void) : ((MethodInfo)binding.info).ReturnType; - - if (binding.outs > 0) - { - ParameterInfo[] pi = binding.info.GetParameters(); - int c = pi.Length; - var n = 0; - - bool isVoid = returnType == typeof(void); - int tupleSize = binding.outs + (isVoid ? 0 : 1); - using var t = Runtime.PyTuple_New(tupleSize); - if (!isVoid) - { - using var v = Converter.ToPython(result, returnType); - Runtime.PyTuple_SetItem(t.Borrow(), n, v.Steal()); - n++; - } - - for (var i = 0; i < c; i++) - { - Type pt = pi[i].ParameterType; - if (pt.IsByRef) - { - using var v = Converter.ToPython(binding.args[i], pt.GetElementType()); - Runtime.PyTuple_SetItem(t.Borrow(), n, v.Steal()); - n++; - } - } - - if (binding.outs == 1 && returnType == typeof(void)) - { - BorrowedReference item = Runtime.PyTuple_GetItem(t.Borrow(), 0); - return new NewReference(item); - } - - return new NewReference(t.Borrow()); - } - - return Converter.ToPython(result, returnType); - } - - /// - /// Utility class to store the information about a - /// - [Serializable] - internal class MethodInformation - { - private ParameterInfo[] _parameterInfo; - private string[] _parametersNames; - - public MethodBase MethodBase { get; } - - public bool IsOriginal { get; set; } - - public ParameterInfo[] ParameterInfo - { - get - { - _parameterInfo ??= MethodBase.GetParameters(); - return _parameterInfo; - } - } - - public string[] ParameterNames - { - get - { - if (_parametersNames == null) - { - if (IsOriginal) - { - _parametersNames = ParameterInfo.Select(pi => pi.Name).ToArray(); - } - else - { - _parametersNames = ParameterInfo.Select(pi => pi.Name.ToSnakeCase()).ToArray(); - } - } - return _parametersNames; - } - } - - public MethodInformation(MethodBase methodBase, bool isOriginal) - { - MethodBase = methodBase; - IsOriginal = isOriginal; - } - - public override string ToString() - { - return MethodBase.ToString(); - } - } - - /// - /// Utility class to sort method info by parameter type precedence. - /// - private class MethodSorter : IComparer - { - public int Compare(MethodInformation x, MethodInformation y) - { - int p1 = GetPrecedence(x); - int p2 = GetPrecedence(y); - if (p1 < p2) - { - return -1; - } - if (p1 > p2) - { - return 1; - } - return 0; - } - } - - private readonly struct MatchedMethod - { - public int KwargsMatched { get; } - public object?[] ManagedArgs { get; } - public int Outs { get; } - public MethodBase Method { get; } - - public MatchedMethod(int kwargsMatched, object?[] margs, int outs, MethodBase mb) - { - KwargsMatched = kwargsMatched; - ManagedArgs = margs; - Outs = outs; - Method = mb; - } - } - - protected static void AppendArgumentTypes(StringBuilder to, BorrowedReference args) - { - long argCount = Runtime.PyTuple_Size(args); - to.Append("("); - for (nint argIndex = 0; argIndex < argCount; argIndex++) - { - BorrowedReference arg = Runtime.PyTuple_GetItem(args, argIndex); - if (arg != null) - { - BorrowedReference type = Runtime.PyObject_TYPE(arg); - if (type != null) - { - using var description = Runtime.PyObject_Str(type); - if (description.IsNull()) - { - Exceptions.Clear(); - to.Append(Util.BadStr); - } - else - { - to.Append(Runtime.GetManagedString(description.Borrow())); - } - } - } - - if (argIndex + 1 < argCount) - to.Append(", "); - } - to.Append(')'); - } - } - - - /// - /// A Binding is a utility instance that bundles together a MethodInfo - /// representing a method to call, a (possibly null) target instance for - /// the call, and the arguments for the call (all as managed values). - /// - internal class Binding - { - public MethodBase info; - public object[] args; - public object inst; - public int outs; - - internal Binding(MethodBase info, object inst, object[] args, int outs) - { - this.info = info; - this.inst = inst; - this.args = args; - this.outs = outs; - } - } -} + internal Binding(MethodBase info, object inst, object[] args, int outs) + { + this.info = info; + this.inst = inst; + this.args = args; + this.outs = outs; + } + } +} From 97d47b7a2f2b72e5620c692bfa46011d27a697e1 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 31 Oct 2024 10:05:58 -0400 Subject: [PATCH 3/8] Add more unit tests --- src/embed_tests/TestMethodBinder.cs | 14 ++++++++++++++ src/runtime/MethodBinder.cs | 7 +------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/embed_tests/TestMethodBinder.cs b/src/embed_tests/TestMethodBinder.cs index d7322135c..fa1a47db7 100644 --- a/src/embed_tests/TestMethodBinder.cs +++ b/src/embed_tests/TestMethodBinder.cs @@ -967,6 +967,20 @@ public void PyObjectArgsHavePrecedenceOverOtherTypes() pyInstance.InvokeMethod("Method", pyArg); }); + // With the first named argument + Assert.DoesNotThrow(() => + { + using var kwargs = Py.kw("decimalArgument", 1.234m); + pyInstance.InvokeMethod("Method", new[] { pyArg }, kwargs); + }); + + // Snake case version + Assert.DoesNotThrow(() => + { + using var kwargs = Py.kw("decimal_argument", 1.234m); + pyInstance.InvokeMethod("method", new[] { pyArg }, kwargs); + }); + Assert.AreEqual("Overload 4", instance.CalledMethodMessage); Assert.IsFalse(Exceptions.ErrorOccurred()); diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index d6503a11e..4767d0256 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -362,7 +362,7 @@ private static int GetMatchedArgumentsPrecedence(MethodInformation method, int m var val = 0; for (var i = 0; i < pi.Length; i++) { - if (i < matchedPositionalArgsCount || matchedKwargsNames.Contains(pi[i].Name)) + if (i < matchedPositionalArgsCount || matchedKwargsNames.Contains(method.ParameterNames[i])) { val += ArgPrecedence(pi[i].ParameterType, isOperatorMethod); } @@ -480,11 +480,6 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe var methods = info == null ? GetMethods() : new List(1) { new MethodInformation(info, true) }; - if (methods.Any(m => m.MethodBase.Name.StartsWith("History"))) - { - - } - int pyArgCount = (int)Runtime.PyTuple_Size(args); var matches = new List(methods.Count); List matchesUsingImplicitConversion = null; From 8b4c6ea3ff114c90c7227ed04ef5c7e879df58f5 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 31 Oct 2024 11:40:03 -0400 Subject: [PATCH 4/8] Minor fixes --- src/runtime/MethodBinder.cs | 89 +++++++++++++++---------------------- 1 file changed, 37 insertions(+), 52 deletions(-) diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index 4767d0256..874371308 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -2,7 +2,6 @@ using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Numerics; using System.Reflection; using System.Text; @@ -320,18 +319,40 @@ internal List GetMethods() /// See: https://github.com/jythontools/jython/blob/master/src/org/python/core/ReflectedArgs.java#L192 /// private static int GetPrecedence(MethodInformation methodInformation) + { + return GetMatchedArgumentsPrecedence(methodInformation, null, null); + } + + /// + /// Gets the precedence of a method's arguments, considering only those arguments that have been matched, + /// that is, those that are not default values. + /// + private static int GetMatchedArgumentsPrecedence(MethodInformation methodInformation, int? matchedPositionalArgsCount, IEnumerable matchedKwargsNames) { ParameterInfo[] pi = methodInformation.ParameterInfo; var mi = methodInformation.MethodBase; int val = mi.IsStatic ? 3000 : 0; - int num = pi.Length; - var isOperatorMethod = OperatorMethod.IsOperatorMethod(methodInformation.MethodBase); val += mi.IsGenericMethod ? 1 : 0; - for (var i = 0; i < num; i++) + + if (!matchedPositionalArgsCount.HasValue) + { + for (var i = 0; i < pi.Length; i++) + { + val += ArgPrecedence(pi[i].ParameterType, isOperatorMethod); + } + } + else { - val += ArgPrecedence(pi[i].ParameterType, isOperatorMethod); + matchedKwargsNames ??= Array.Empty(); + for (var i = 0; i < pi.Length; i++) + { + if (i < matchedPositionalArgsCount || matchedKwargsNames.Contains(methodInformation.ParameterNames[i])) + { + val += ArgPrecedence(pi[i].ParameterType, isOperatorMethod); + } + } } var info = mi as MethodInfo; @@ -351,32 +372,6 @@ private static int GetPrecedence(MethodInformation methodInformation) return val; } - /// - /// Gets the precedence of a method's arguments, considering only those arguments that have been matched, - /// that is, those that are not default values. - /// - private static int GetMatchedArgumentsPrecedence(MethodInformation method, int matchedPositionalArgsCount, IEnumerable matchedKwargsNames) - { - var isOperatorMethod = OperatorMethod.IsOperatorMethod(method.MethodBase); - var pi = method.ParameterInfo; - var val = 0; - for (var i = 0; i < pi.Length; i++) - { - if (i < matchedPositionalArgsCount || matchedKwargsNames.Contains(method.ParameterNames[i])) - { - val += ArgPrecedence(pi[i].ParameterType, isOperatorMethod); - } - } - - var mi = method.MethodBase; - var info = mi as MethodInfo; - if (info != null) - { - val += ArgPrecedence(info.ReturnType, isOperatorMethod); - } - return val; - } - /// /// Return a precedence value for a particular Type object. /// @@ -390,7 +385,7 @@ internal static int ArgPrecedence(Type t, bool isOperatorMethod) if (t.IsAssignableFrom(typeof(PyObject)) && !isOperatorMethod) { - return -3000; + return -1; } if (t.IsArray) @@ -746,26 +741,16 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe // We favor matches that do not use implicit conversion var matchesTouse = matches.Count > 0 ? matches : matchesUsingImplicitConversion; - // The best match would be the one with the most named arguments matched - var maxKwargsMatched = matchesTouse.Max(x => x.KwargsMatched); - // Don't materialize the enumerable, just enumerate twice if necessary to avoid creating a collection instance. - var bestMatches = matchesTouse.Where(x => x.KwargsMatched == maxKwargsMatched); - var bestMatchesCount = bestMatches.Count(); - - MatchedMethod bestMatch; - // Multiple best matches, we can still resolve the ambiguity because - // some method might take precedence if it received PyObject instances. - // So let's get the best match by the precedence of the actual passed arguments, - // without considering optional arguments without a passed value - if (bestMatchesCount > 1) - { - bestMatch = bestMatches.MinBy(x => GetMatchedArgumentsPrecedence(methods.First(m => m.MethodBase == x.Method), pyArgCount, - kwArgDict?.Keys ?? Enumerable.Empty())); - } - else - { - bestMatch = bestMatches.First(); - } + // The best match would be the one with the most named arguments matched. + // But if multiple matches have the same max number of named arguments matched, + // we solve the ambiguity by taking the one with the highest precedence but only + // considering the actual arguments passed, ignoring the optional arguments for + // which the default values were used + var bestMatch = matchesTouse + .GroupBy(x => x.KwargsMatched) + .OrderByDescending(x => x.Key) + .First() + .MinBy(x => GetMatchedArgumentsPrecedence(methods.First(m => m.MethodBase == x.Method), pyArgCount, kwArgDict?.Keys)); var margs = bestMatch.ManagedArgs; var outs = bestMatch.Outs; From 6c70561fb517e4d6d89688372e941f3cd884ec18 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 31 Oct 2024 13:17:55 -0400 Subject: [PATCH 5/8] Update version to 2.0.40 --- 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 b437fe532..ba9456e3d 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 ffb1308a4..7ab968e35 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.39")] -[assembly: AssemblyFileVersion("2.0.39")] +[assembly: AssemblyVersion("2.0.40")] +[assembly: AssemblyFileVersion("2.0.40")] diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index c579abaa5..e0d22a71e 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.39 + 2.0.40 false LICENSE https://github.com/pythonnet/pythonnet From 93fb9733d0f8a0a55631ca628db273c457342e47 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 31 Oct 2024 18:47:08 -0400 Subject: [PATCH 6/8] Minor change --- src/runtime/MethodBinder.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index 874371308..25dd76621 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -718,7 +718,7 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe } } - var match = new MatchedMethod(kwargsMatched, margs, outs, mi); + var match = new MatchedMethod(kwargsMatched, margs, outs, methodInformation); if (usedImplicitConversion) { if (matchesUsingImplicitConversion == null) @@ -750,7 +750,7 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe .GroupBy(x => x.KwargsMatched) .OrderByDescending(x => x.Key) .First() - .MinBy(x => GetMatchedArgumentsPrecedence(methods.First(m => m.MethodBase == x.Method), pyArgCount, kwArgDict?.Keys)); + .MinBy(x => GetMatchedArgumentsPrecedence(x.MethodInformation, pyArgCount, kwArgDict?.Keys)); var margs = bestMatch.ManagedArgs; var outs = bestMatch.Outs; @@ -1116,14 +1116,15 @@ private readonly struct MatchedMethod public int KwargsMatched { get; } public object?[] ManagedArgs { get; } public int Outs { get; } - public MethodBase Method { get; } + public MethodInformation MethodInformation { get; } + public MethodBase Method => MethodInformation.MethodBase; - public MatchedMethod(int kwargsMatched, object?[] margs, int outs, MethodBase mb) + public MatchedMethod(int kwargsMatched, object?[] margs, int outs, MethodInformation methodInformation) { KwargsMatched = kwargsMatched; ManagedArgs = margs; Outs = outs; - Method = mb; + MethodInformation = methodInformation; } } From 0acc2db68d1a9f0243f0d81ebab4336fe3f416ab Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 1 Nov 2024 10:06:19 -0400 Subject: [PATCH 7/8] Improve unit test --- src/embed_tests/TestMethodBinder.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/embed_tests/TestMethodBinder.cs b/src/embed_tests/TestMethodBinder.cs index fa1a47db7..d2fd8b7a2 100644 --- a/src/embed_tests/TestMethodBinder.cs +++ b/src/embed_tests/TestMethodBinder.cs @@ -925,7 +925,12 @@ def call_method(instance): public class CSharpClass2 { - public string CalledMethodMessage { get; private set; } + public string CalledMethodMessage { get; private set; } = string.Empty; + + public void Clear() + { + CalledMethodMessage = string.Empty; + } public void Method() { @@ -967,6 +972,10 @@ public void PyObjectArgsHavePrecedenceOverOtherTypes() pyInstance.InvokeMethod("Method", pyArg); }); + Assert.AreEqual("Overload 4", instance.CalledMethodMessage); + Assert.IsFalse(Exceptions.ErrorOccurred()); + instance.Clear(); + // With the first named argument Assert.DoesNotThrow(() => { @@ -974,6 +983,10 @@ public void PyObjectArgsHavePrecedenceOverOtherTypes() pyInstance.InvokeMethod("Method", new[] { pyArg }, kwargs); }); + Assert.AreEqual("Overload 4", instance.CalledMethodMessage); + Assert.IsFalse(Exceptions.ErrorOccurred()); + instance.Clear(); + // Snake case version Assert.DoesNotThrow(() => { @@ -982,7 +995,6 @@ public void PyObjectArgsHavePrecedenceOverOtherTypes() }); Assert.AreEqual("Overload 4", instance.CalledMethodMessage); - Assert.IsFalse(Exceptions.ErrorOccurred()); } From e26db13eb80f16720800c9b2105b233d904e800b Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 1 Nov 2024 10:21:42 -0400 Subject: [PATCH 8/8] Add unit test --- src/embed_tests/TestMethodBinder.cs | 52 ++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/embed_tests/TestMethodBinder.cs b/src/embed_tests/TestMethodBinder.cs index d2fd8b7a2..7f4c58d7e 100644 --- a/src/embed_tests/TestMethodBinder.cs +++ b/src/embed_tests/TestMethodBinder.cs @@ -937,7 +937,7 @@ public void Method() CalledMethodMessage = "Overload 1"; } - public void Method(CSharpClass csharpClassArgument, decimal decimalArgument = 1.2m, PyObject pyObjectKArgument = null) + public void Method(CSharpClass csharpClassArgument, decimal decimalArgument = 1.2m, PyObject pyObjectKwArgument = null) { CalledMethodMessage = "Overload 2"; } @@ -998,6 +998,56 @@ public void PyObjectArgsHavePrecedenceOverOtherTypes() Assert.IsFalse(Exceptions.ErrorOccurred()); } + [Test] + public void OtherTypesHavePrecedenceOverPyObjectArgsIfMoreArgsAreMatched() + { + using var _ = Py.GIL(); + + var instance = new CSharpClass2(); + using var pyInstance = instance.ToPython(); + using var pyArg = new CSharpClass().ToPython(); + + Assert.DoesNotThrow(() => + { + using var kwargs = Py.kw("pyObjectKwArgument", new CSharpClass2()); + pyInstance.InvokeMethod("Method", new[] { pyArg }, kwargs); + }); + + Assert.AreEqual("Overload 2", instance.CalledMethodMessage); + Assert.IsFalse(Exceptions.ErrorOccurred()); + instance.Clear(); + + Assert.DoesNotThrow(() => + { + using var kwargs = Py.kw("py_object_kw_argument", new CSharpClass2()); + pyInstance.InvokeMethod("method", new[] { pyArg }, kwargs); + }); + + Assert.AreEqual("Overload 2", instance.CalledMethodMessage); + Assert.IsFalse(Exceptions.ErrorOccurred()); + instance.Clear(); + + Assert.DoesNotThrow(() => + { + using var kwargs = Py.kw("objectArgument", "somestring"); + pyInstance.InvokeMethod("Method", new[] { pyArg }, kwargs); + }); + + Assert.AreEqual("Overload 3", instance.CalledMethodMessage); + Assert.IsFalse(Exceptions.ErrorOccurred()); + instance.Clear(); + + Assert.DoesNotThrow(() => + { + using var kwargs = Py.kw("object_argument", "somestring"); + pyInstance.InvokeMethod("method", new[] { pyArg }, kwargs); + }); + + Assert.AreEqual("Overload 3", instance.CalledMethodMessage); + Assert.IsFalse(Exceptions.ErrorOccurred()); + instance.Clear(); + } + [Test] public void BindsConstructorToSnakeCasedArgumentsVersion([Values] bool useCamelCase, [Values] bool passOptionalArgument) {