From 4fc58714415ab245e955c3d0cafd7df98921a7d8 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 30 Oct 2024 18:50:32 -0400 Subject: [PATCH 01/24] 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 02/24] 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 03/24] 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 04/24] 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 05/24] 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 06/24] 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 07/24] 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 08/24] 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) { From 360948f55878a3121c8b95cbf75a782937658ff6 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Mon, 2 Dec 2024 15:55:03 -0400 Subject: [PATCH 09/24] Bug dynamic class throwing on hasattr (#96) * Throw AttributeError in tp_getattro for dynamic classes Python api documentation indicates it should throw AttributeError * Bump version to 2.0.41 --- src/embed_tests/TestPropertyAccess.cs | 56 +++++++++++++++++++ src/perf_tests/Python.PerformanceTests.csproj | 4 +- src/runtime/Properties/AssemblyInfo.cs | 4 +- src/runtime/Python.Runtime.csproj | 2 +- src/runtime/Types/DynamicClassObject.cs | 7 ++- 5 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/embed_tests/TestPropertyAccess.cs b/src/embed_tests/TestPropertyAccess.cs index e10dfadf6..54acc08f0 100644 --- a/src/embed_tests/TestPropertyAccess.cs +++ b/src/embed_tests/TestPropertyAccess.cs @@ -1410,6 +1410,62 @@ def CallDynamicMethodCatchingExceptions(self, fixture, defaultValue): } } + public class ThrowingDynamicFixture : DynamicFixture + { + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + if (!base.TryGetMember(binder, out result)) + { + throw new InvalidOperationException("Member not found"); + } + return true; + } + } + + [Test] + public void TestHasAttrShouldNotThrowIfAttributeIsNotPresentForDynamicClassObjects() + { + using var _ = Py.GIL(); + + dynamic module = PyModule.FromString("TestHasAttrShouldNotThrowIfAttributeIsNotPresentForDynamicClassObjects", @" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") +AddReference(""System"") + +from Python.EmbeddingTest import TestPropertyAccess + +class TestDynamicClass(TestPropertyAccess.ThrowingDynamicFixture): + def __init__(self): + self.test_attribute = 11; + +def has_attribute(obj, attribute): + return hasattr(obj, attribute) +"); + + dynamic fixture = module.GetAttr("TestDynamicClass")(); + dynamic hasAttribute = module.GetAttr("has_attribute"); + + var hasAttributeResult = false; + Assert.DoesNotThrow(() => + { + hasAttributeResult = hasAttribute(fixture, "test_attribute"); + }); + Assert.IsTrue(hasAttributeResult); + + var attribute = 0; + Assert.DoesNotThrow(() => + { + attribute = fixture.test_attribute.As(); + }); + Assert.AreEqual(11, attribute); + + Assert.DoesNotThrow(() => + { + hasAttributeResult = hasAttribute(fixture, "non_existent_attribute"); + }); + Assert.IsFalse(hasAttributeResult); + } + public interface IModel { void InvokeModel(); diff --git a/src/perf_tests/Python.PerformanceTests.csproj b/src/perf_tests/Python.PerformanceTests.csproj index ba9456e3d..7d6192974 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 7ab968e35..448265145 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.40")] -[assembly: AssemblyFileVersion("2.0.40")] +[assembly: AssemblyVersion("2.0.41")] +[assembly: AssemblyFileVersion("2.0.41")] diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index e0d22a71e..a3fd340be 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.40 + 2.0.41 false LICENSE https://github.com/pythonnet/pythonnet diff --git a/src/runtime/Types/DynamicClassObject.cs b/src/runtime/Types/DynamicClassObject.cs index 2aa4b935a..94e94b568 100644 --- a/src/runtime/Types/DynamicClassObject.cs +++ b/src/runtime/Types/DynamicClassObject.cs @@ -88,7 +88,12 @@ public static NewReference tp_getattro(BorrowedReference ob, BorrowedReference k catch (Exception exception) { Exceptions.Clear(); - Exceptions.SetError(exception); + // tp_getattro should call PyObject_GenericGetAttr (which we already did) + // which must throw AttributeError if the attribute is not found (see https://docs.python.org/3/c-api/object.html#c.PyObject_GenericGetAttr) + // So if we are throwing anything, it must be AttributeError. + // e.g hasattr uses this method to check if the attribute exists. If we throw anything other than AttributeError, + // hasattr will throw instead of catching and returning False. + Exceptions.SetError(Exceptions.AttributeError, exception.Message); } } From 78551dffa2d1aa6ad0b8c3d7efe8e337c0e952ab Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 3 Dec 2024 14:38:05 -0400 Subject: [PATCH 10/24] Throw AttributeError in tp_setattro for dynamic classes (#97) * Throw AttributeError in tp_getattro for dynamic classes Python api documentation indicates it should throw AttributeError on failure * Cleanup --- src/embed_tests/TestPropertyAccess.cs | 45 ++++++++++++++++++++++++- src/runtime/Types/DynamicClassObject.cs | 8 +++-- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/embed_tests/TestPropertyAccess.cs b/src/embed_tests/TestPropertyAccess.cs index 54acc08f0..8dba383d6 100644 --- a/src/embed_tests/TestPropertyAccess.cs +++ b/src/embed_tests/TestPropertyAccess.cs @@ -1420,6 +1420,16 @@ public override bool TryGetMember(GetMemberBinder binder, out object result) } return true; } + + public override bool TrySetMember(SetMemberBinder binder, object value) + { + if (value is PyObject pyValue && PyString.IsStringType(pyValue)) + { + throw new InvalidOperationException("Cannot set string value"); + } + + return base.TrySetMember(binder, value); + } } [Test] @@ -1430,7 +1440,6 @@ public void TestHasAttrShouldNotThrowIfAttributeIsNotPresentForDynamicClassObjec dynamic module = PyModule.FromString("TestHasAttrShouldNotThrowIfAttributeIsNotPresentForDynamicClassObjects", @" from clr import AddReference AddReference(""Python.EmbeddingTest"") -AddReference(""System"") from Python.EmbeddingTest import TestPropertyAccess @@ -1466,6 +1475,40 @@ def has_attribute(obj, attribute): Assert.IsFalse(hasAttributeResult); } + [Test] + public void TestSetAttrShouldThrowPythonExceptionOnFailure() + { + using var _ = Py.GIL(); + + dynamic module = PyModule.FromString("TestHasAttrShouldNotThrowIfAttributeIsNotPresentForDynamicClassObjects", @" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import TestPropertyAccess + +class TestDynamicClass(TestPropertyAccess.ThrowingDynamicFixture): + pass + +def set_attribute(obj): + obj.int_attribute = 11 + +def set_string_attribute(obj): + obj.string_attribute = 'string' +"); + + dynamic fixture = module.GetAttr("TestDynamicClass")(); + + dynamic setAttribute = module.GetAttr("set_attribute"); + Assert.DoesNotThrow(() => setAttribute(fixture)); + + dynamic setStringAttribute = module.GetAttr("set_string_attribute"); + var exception = Assert.Throws(() => setStringAttribute(fixture)); + Assert.AreEqual("Cannot set string value", exception.Message); + + using var expectedExceptionType = new PyType(Exceptions.AttributeError); + Assert.AreEqual(expectedExceptionType, exception.Type); + } + public interface IModel { void InvokeModel(); diff --git a/src/runtime/Types/DynamicClassObject.cs b/src/runtime/Types/DynamicClassObject.cs index 94e94b568..cb6fd5650 100644 --- a/src/runtime/Types/DynamicClassObject.cs +++ b/src/runtime/Types/DynamicClassObject.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Dynamic; -using System.Reflection; using System.Runtime.CompilerServices; using RuntimeBinder = Microsoft.CSharp.RuntimeBinder; @@ -94,6 +92,7 @@ public static NewReference tp_getattro(BorrowedReference ob, BorrowedReference k // e.g hasattr uses this method to check if the attribute exists. If we throw anything other than AttributeError, // hasattr will throw instead of catching and returning False. Exceptions.SetError(Exceptions.AttributeError, exception.Message); + return default; } } @@ -120,7 +119,10 @@ public static int tp_setattro(BorrowedReference ob, BorrowedReference key, Borro // Catch C# exceptions and raise them as Python exceptions. catch (Exception exception) { - Exceptions.SetError(exception); + // tp_setattro should call PyObject_GenericSetAttr (which we already did) + // which must throw AttributeError on failure and return -1 (see https://docs.python.org/3/c-api/object.html#c.PyObject_GenericSetAttr) + Exceptions.SetError(Exceptions.AttributeError, exception.Message); + return -1; } return 0; From dff82a19038af18a3bf3bbc35afa2ecef26d1f81 Mon Sep 17 00:00:00 2001 From: Martin-Molinero Date: Fri, 20 Dec 2024 16:57:15 -0300 Subject: [PATCH 11/24] dotnet 9 (#98) * dotnet 9 * Bump version to 2.0.42 * Fix compiler warnings --- Directory.Build.props | 1 - src/console/Console.csproj | 2 +- src/embed_tests/Python.EmbeddingTest.csproj | 2 +- .../StateSerialization/MethodSerialization.cs | 3 ++- src/perf_tests/Python.PerformanceTests.csproj | 6 +++--- .../Python.PythonTestsRunner.csproj | 2 +- src/runtime/MethodBinder.cs | 8 +++----- src/runtime/Native/NewReference.cs | 8 ++++---- src/runtime/Native/StolenReference.cs | 2 +- src/runtime/Properties/AssemblyInfo.cs | 4 ++-- src/runtime/Python.Runtime.csproj | 4 ++-- src/runtime/Runtime.cs | 17 ++++------------- src/runtime/StateSerialization/RuntimeData.cs | 3 ++- src/testing/Python.Test.csproj | 2 +- 14 files changed, 27 insertions(+), 37 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 6716f29df..d724e41e7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,6 @@ Copyright (c) 2006-2021 The Contributors of the Python.NET Project pythonnet Python.NET - 10.0 false diff --git a/src/console/Console.csproj b/src/console/Console.csproj index 5ca5192e3..edd9054ef 100644 --- a/src/console/Console.csproj +++ b/src/console/Console.csproj @@ -1,6 +1,6 @@ - net6.0 + net9.0 Exe nPython Python.Runtime diff --git a/src/embed_tests/Python.EmbeddingTest.csproj b/src/embed_tests/Python.EmbeddingTest.csproj index 84dcb3fe2..f50311141 100644 --- a/src/embed_tests/Python.EmbeddingTest.csproj +++ b/src/embed_tests/Python.EmbeddingTest.csproj @@ -1,7 +1,7 @@ - net6.0 + net9.0 ..\pythonnet.snk true diff --git a/src/embed_tests/StateSerialization/MethodSerialization.cs b/src/embed_tests/StateSerialization/MethodSerialization.cs index 80b7a08ee..21a6cfa52 100644 --- a/src/embed_tests/StateSerialization/MethodSerialization.cs +++ b/src/embed_tests/StateSerialization/MethodSerialization.cs @@ -1,4 +1,4 @@ -using System.IO; +/*using System.IO; using System.Reflection; using NUnit.Framework; @@ -44,3 +44,4 @@ public class MethodTestHost public MethodTestHost(int _) { } public void Generic(T item, T[] array, ref T @ref) { } } +*/ diff --git a/src/perf_tests/Python.PerformanceTests.csproj b/src/perf_tests/Python.PerformanceTests.csproj index 7d6192974..540e18b66 100644 --- a/src/perf_tests/Python.PerformanceTests.csproj +++ b/src/perf_tests/Python.PerformanceTests.csproj @@ -1,7 +1,7 @@ - net6.0 + net9.0 false @@ -13,7 +13,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + compile @@ -25,7 +25,7 @@ - + diff --git a/src/python_tests_runner/Python.PythonTestsRunner.csproj b/src/python_tests_runner/Python.PythonTestsRunner.csproj index 04b8ef252..16e563ff6 100644 --- a/src/python_tests_runner/Python.PythonTestsRunner.csproj +++ b/src/python_tests_runner/Python.PythonTestsRunner.csproj @@ -1,7 +1,7 @@ - net6.0 + net9.0 diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index 25dd76621..8c8bac65d 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -793,7 +793,6 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe 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 @@ -806,20 +805,19 @@ static BorrowedReference HandleParamsArray(BorrowedReference args, int arrayStar 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; + return item; } else { tempObject = Runtime.PyTuple_GetSlice(args, arrayStart, pyArgCount); - op = tempObject.Borrow(); + return tempObject.Borrow(); } } else { tempObject = Runtime.PyTuple_GetSlice(args, arrayStart, pyArgCount); - op = tempObject.Borrow(); + return tempObject.Borrow(); } - return op; } /// diff --git a/src/runtime/Native/NewReference.cs b/src/runtime/Native/NewReference.cs index 00e01d75f..456503b41 100644 --- a/src/runtime/Native/NewReference.cs +++ b/src/runtime/Native/NewReference.cs @@ -15,7 +15,7 @@ ref struct NewReference /// Creates a pointing to the same object [DebuggerHidden] - public NewReference(BorrowedReference reference, bool canBeNull = false) + public NewReference(scoped BorrowedReference reference, bool canBeNull = false) { var address = canBeNull ? reference.DangerousGetAddressOrNull() @@ -157,15 +157,15 @@ public static bool IsNull(this in NewReference reference) [Pure] [DebuggerHidden] - public static BorrowedReference BorrowNullable(this in NewReference reference) + public static BorrowedReference BorrowNullable(this scoped in NewReference reference) => new(NewReference.DangerousGetAddressOrNull(reference)); [Pure] [DebuggerHidden] - public static BorrowedReference Borrow(this in NewReference reference) + public static BorrowedReference Borrow(this scoped in NewReference reference) => reference.IsNull() ? throw new NullReferenceException() : reference.BorrowNullable(); [Pure] [DebuggerHidden] - public static BorrowedReference BorrowOrThrow(this in NewReference reference) + public static BorrowedReference BorrowOrThrow(this scoped in NewReference reference) => reference.IsNull() ? throw PythonException.ThrowLastAsClrException() : reference.BorrowNullable(); } } diff --git a/src/runtime/Native/StolenReference.cs b/src/runtime/Native/StolenReference.cs index 49304c1fd..14c3a6995 100644 --- a/src/runtime/Native/StolenReference.cs +++ b/src/runtime/Native/StolenReference.cs @@ -28,7 +28,7 @@ public static StolenReference Take(ref IntPtr ptr) } [MethodImpl(MethodImplOptions.AggressiveInlining)] [DebuggerHidden] - public static StolenReference TakeNullable(ref IntPtr ptr) + public static StolenReference TakeNullable(scoped ref IntPtr ptr) { var stolenAddr = ptr; ptr = IntPtr.Zero; diff --git a/src/runtime/Properties/AssemblyInfo.cs b/src/runtime/Properties/AssemblyInfo.cs index 448265145..126b2f62e 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.41")] -[assembly: AssemblyFileVersion("2.0.41")] +[assembly: AssemblyVersion("2.0.42")] +[assembly: AssemblyFileVersion("2.0.42")] diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index a3fd340be..4ab951154 100644 --- a/src/runtime/Python.Runtime.csproj +++ b/src/runtime/Python.Runtime.csproj @@ -1,11 +1,11 @@ - net6.0 + net9.0 AnyCPU Python.Runtime Python.Runtime QuantConnect.pythonnet - 2.0.41 + 2.0.42 false LICENSE https://github.com/pythonnet/pythonnet diff --git a/src/runtime/Runtime.cs b/src/runtime/Runtime.cs index a4a6acb05..7febdbcb2 100644 --- a/src/runtime/Runtime.cs +++ b/src/runtime/Runtime.cs @@ -157,15 +157,8 @@ internal static void Initialize(bool initSigs = false) // Initialize modules that depend on the runtime class. AssemblyManager.Initialize(); OperatorMethod.Initialize(); - if (RuntimeData.HasStashData()) - { - RuntimeData.RestoreRuntimeData(); - } - else - { - PyCLRMetaType = MetaType.Initialize(); - ImportHook.Initialize(); - } + PyCLRMetaType = MetaType.Initialize(); + ImportHook.Initialize(); Exceptions.Initialize(); // Need to add the runtime directory to sys.path so that we @@ -269,8 +262,6 @@ internal static void Shutdown() { // avoid saving dead objects TryCollectingGarbage(runs: 3); - - RuntimeData.Stash(); } AssemblyManager.Shutdown(); @@ -832,7 +823,7 @@ public static int Py_Main(int argc, string[] argv) internal static IntPtr Py_GetBuildInfo() => Delegates.Py_GetBuildInfo(); - const PyCompilerFlags Utf8String = PyCompilerFlags.IGNORE_COOKIE | PyCompilerFlags.SOURCE_IS_UTF8; + private static readonly PyCompilerFlags Utf8String = PyCompilerFlags.IGNORE_COOKIE | PyCompilerFlags.SOURCE_IS_UTF8; internal static int PyRun_SimpleString(string code) { @@ -1715,7 +1706,7 @@ internal static bool PyType_IsSameAsOrSubtype(BorrowedReference type, BorrowedRe internal static NewReference PyType_GenericAlloc(BorrowedReference type, nint n) => Delegates.PyType_GenericAlloc(type, n); internal static IntPtr PyType_GetSlot(BorrowedReference type, TypeSlotID slot) => Delegates.PyType_GetSlot(type, slot); - internal static NewReference PyType_FromSpecWithBases(in NativeTypeSpec spec, BorrowedReference bases) => Delegates.PyType_FromSpecWithBases(in spec, bases); + internal static NewReference PyType_FromSpecWithBases(scoped in NativeTypeSpec spec, BorrowedReference bases) => Delegates.PyType_FromSpecWithBases(in spec, bases); /// /// Finalize a type object. This should be called on all type objects to finish their initialization. This function is responsible for adding inherited slots from a type�s base class. Return 0 on success, or return -1 and sets an exception on error. diff --git a/src/runtime/StateSerialization/RuntimeData.cs b/src/runtime/StateSerialization/RuntimeData.cs index a60796a87..20d9e2e8a 100644 --- a/src/runtime/StateSerialization/RuntimeData.cs +++ b/src/runtime/StateSerialization/RuntimeData.cs @@ -1,4 +1,4 @@ -using System; +/*using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -210,3 +210,4 @@ internal static IFormatter CreateFormatter() } } } +*/ diff --git a/src/testing/Python.Test.csproj b/src/testing/Python.Test.csproj index 24a8f72c4..7f688f0ba 100644 --- a/src/testing/Python.Test.csproj +++ b/src/testing/Python.Test.csproj @@ -1,6 +1,6 @@ - net6.0 + net9.0 true true ..\pythonnet.snk From 30f32b97363e40920934157acc7857318c8b847a Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 1 May 2025 10:38:37 -0400 Subject: [PATCH 12/24] Support pythonic manipulation of managed enums (#101) * Support pythonic manipulation of managed enums. Add support for 'len' method, 'in' operator and iteration of enum types. * Minor fixes and unit tests * Bump version to 2.0.43 --- src/embed_tests/ClassManagerTests.cs | 80 +++++++++++++++++++ src/perf_tests/Python.PerformanceTests.csproj | 4 +- src/runtime/Properties/AssemblyInfo.cs | 4 +- src/runtime/Python.Runtime.csproj | 2 +- src/runtime/Types/MetaType.cs | 74 +++++++++++++++++ 5 files changed, 159 insertions(+), 5 deletions(-) diff --git a/src/embed_tests/ClassManagerTests.cs b/src/embed_tests/ClassManagerTests.cs index 0db0d282f..15da61e3b 100644 --- a/src/embed_tests/ClassManagerTests.cs +++ b/src/embed_tests/ClassManagerTests.cs @@ -1003,6 +1003,86 @@ def call(instance): } #endregion + + public enum TestEnum + { + FirstEnumValue, + SecondEnumValue, + ThirdEnumValue + } + + [Test] + public void EnumPythonOperationsCanBePerformedOnManagedEnum() + { + using (Py.GIL()) + { + var module = PyModule.FromString("EnumPythonOperationsCanBePerformedOnManagedEnum", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +def get_enum_values(): + return [x for x in ClassManagerTests.TestEnum] + +def count_enum_values(): + return len(ClassManagerTests.TestEnum) + +def is_enum_value_defined(value): + return value in ClassManagerTests.TestEnum + "); + + using var pyEnumValues = module.InvokeMethod("get_enum_values"); + var enumValues = pyEnumValues.As>(); + + var expectedEnumValues = Enum.GetValues(); + CollectionAssert.AreEquivalent(expectedEnumValues, enumValues); + + using var pyEnumCount = module.InvokeMethod("count_enum_values"); + var enumCount = pyEnumCount.As(); + Assert.AreEqual(expectedEnumValues.Length, enumCount); + + var validEnumValues = expectedEnumValues + .SelectMany(x => new object[] { x, (int)x, Enum.GetName(x.GetType(), x) }) + .Select(x => (x, true)); + var invalidEnumValues = new object[] { 5, "INVALID_ENUM_VALUE" }.Select(x => (x, false)); + + foreach (var (enumValue, isValid) in validEnumValues.Concat(invalidEnumValues)) + { + using var pyEnumValue = enumValue.ToPython(); + using var pyIsDefined = module.InvokeMethod("is_enum_value_defined", pyEnumValue); + var isDefined = pyIsDefined.As(); + Assert.AreEqual(isValid, isDefined, $"Failed for {enumValue} ({enumValue.GetType()})"); + } + } + } + + [Test] + public void EnumInterableOperationsNotSupportedForManagedNonEnumTypes() + { + using (Py.GIL()) + { + var module = PyModule.FromString("EnumInterableOperationsNotSupportedForManagedNonEnumTypes", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +def get_enum_values(): + return [x for x in ClassManagerTests] + +def count_enum_values(): + return len(ClassManagerTests) + +def is_enum_value_defined(): + return 1 in ClassManagerTests + "); + + Assert.Throws(() => module.InvokeMethod("get_enum_values")); + Assert.Throws(() => module.InvokeMethod("count_enum_values")); + Assert.Throws(() => module.InvokeMethod("is_enum_value_defined")); + } + } } public class NestedTestParent diff --git a/src/perf_tests/Python.PerformanceTests.csproj b/src/perf_tests/Python.PerformanceTests.csproj index 540e18b66..99f447f56 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 126b2f62e..c8a43c43a 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.42")] -[assembly: AssemblyFileVersion("2.0.42")] +[assembly: AssemblyVersion("2.0.43")] +[assembly: AssemblyFileVersion("2.0.43")] diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index 4ab951154..f1f77f9d7 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.42 + 2.0.43 false LICENSE https://github.com/pythonnet/pythonnet diff --git a/src/runtime/Types/MetaType.cs b/src/runtime/Types/MetaType.cs index 1543711f6..bfaced5f6 100644 --- a/src/runtime/Types/MetaType.cs +++ b/src/runtime/Types/MetaType.cs @@ -359,5 +359,79 @@ public static NewReference __subclasscheck__(BorrowedReference tp, BorrowedRefer { return DoInstanceCheck(tp, args, true); } + + /// + /// Standard iteration support Enums. This allows natural interation + /// over the available values an Enum defines. + /// + public static NewReference tp_iter(BorrowedReference tp) + { + if (!TryGetEnumType(tp, out var type)) + { + return default; + } + var values = Enum.GetValues(type); + return new Iterator(values.GetEnumerator(), type).Alloc(); + } + + /// + /// Implements __len__ for Enum types. + /// + public static int mp_length(BorrowedReference tp) + { + if (!TryGetEnumType(tp, out var type)) + { + return -1; + } + return Enum.GetValues(type).Length; + } + + /// + /// Implements __contains__ for Enum types. + /// + public static int sq_contains(BorrowedReference tp, BorrowedReference v) + { + if (!TryGetEnumType(tp, out var type)) + { + return -1; + } + + if (!Converter.ToManaged(v, type, out var enumValue, false) && + !Converter.ToManaged(v, typeof(int), out enumValue, false) && + !Converter.ToManaged(v, typeof(string), out enumValue, false)) + { + Exceptions.SetError(Exceptions.TypeError, + $"invalid parameter type for sq_contains: should be {Converter.GetTypeByAlias(v)}, found {type}"); + return -1; + } + + return Enum.IsDefined(type, enumValue) ? 1 : 0; + } + + private static bool TryGetEnumType(BorrowedReference tp, out Type type) + { + type = null; + var cb = GetManagedObject(tp) as ClassBase; + if (cb == null) + { + Exceptions.SetError(Exceptions.TypeError, "invalid object"); + return false; + } + + if (!cb.type.Valid) + { + Exceptions.SetError(Exceptions.TypeError, "invalid type"); + return false; + } + + if (!cb.type.Value.IsEnum) + { + Exceptions.SetError(Exceptions.TypeError, "uniterable type"); + return false; + } + + type = cb.type.Value; + return true; + } } } From 60e9e86317a43cd39db7457e830520235312cba9 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 1 May 2025 10:38:45 -0400 Subject: [PATCH 13/24] Support py list conversion to IReadOnlyList (#100) --- src/embed_tests/TestConverter.cs | 15 +++++++++++++++ src/runtime/Converter.cs | 3 ++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/embed_tests/TestConverter.cs b/src/embed_tests/TestConverter.cs index 88809e7f7..889f27f17 100644 --- a/src/embed_tests/TestConverter.cs +++ b/src/embed_tests/TestConverter.cs @@ -78,6 +78,21 @@ public void ReadOnlyCollection() Assert.AreEqual(typeof(int), ((IReadOnlyCollection) result).ToList()[1]); } + [Test] + public void ReadOnlyList() + { + var array = new List { typeof(decimal), typeof(int) }; + var py = array.ToPython(); + object result; + var converted = Converter.ToManaged(py, typeof(IReadOnlyList), out result, false); + + Assert.IsTrue(converted); + Assert.AreEqual(typeof(List), result.GetType()); + Assert.AreEqual(2, ((IReadOnlyList)result).Count); + Assert.AreEqual(typeof(decimal), ((IReadOnlyList)result).ToList()[0]); + Assert.AreEqual(typeof(int), ((IReadOnlyList)result).ToList()[1]); + } + [Test] public void ConvertPyListToArray() { diff --git a/src/runtime/Converter.cs b/src/runtime/Converter.cs index 047f7a03a..19fb1c883 100644 --- a/src/runtime/Converter.cs +++ b/src/runtime/Converter.cs @@ -421,7 +421,8 @@ internal static bool ToManagedValue(BorrowedReference value, Type obType, if (typeDefinition == typeof(List<>) || typeDefinition == typeof(IList<>) || typeDefinition == typeof(IEnumerable<>) - || typeDefinition == typeof(IReadOnlyCollection<>)) + || typeDefinition == typeof(IReadOnlyCollection<>) + || typeDefinition == typeof(IReadOnlyList<>)) { return ToList(value, obType, out result, setError); } From e303acfa57cf4385c6c291942d6b806cb17bb38a Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 1 May 2025 10:48:11 -0400 Subject: [PATCH 14/24] Add container methods to `IDictionary` (#99) * Add __len__ and __contains__ to IDictionary that defines ContainsKey * Replace DictionaryObject with LookUpObject --- src/embed_tests/ClassManagerTests.cs | 159 ++++++++++++++++++ src/runtime/ClassManager.cs | 14 +- src/runtime/Types/DynamicClassLookUpObject.cs | 34 ++++ .../Types/KeyValuePairEnumerableObject.cs | 66 +------- src/runtime/Types/LookUpObject.cs | 121 +++++++++++++ 5 files changed, 329 insertions(+), 65 deletions(-) create mode 100644 src/runtime/Types/DynamicClassLookUpObject.cs create mode 100644 src/runtime/Types/LookUpObject.cs diff --git a/src/embed_tests/ClassManagerTests.cs b/src/embed_tests/ClassManagerTests.cs index 15da61e3b..dcdf66edb 100644 --- a/src/embed_tests/ClassManagerTests.cs +++ b/src/embed_tests/ClassManagerTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -1083,6 +1084,164 @@ def is_enum_value_defined(): Assert.Throws(() => module.InvokeMethod("is_enum_value_defined")); } } + + private static TestCaseData[] IDictionaryContainsTestCases => + [ + new(typeof(TestDictionary)), + new(typeof(Dictionary)), + new(typeof(TestKeyValueContainer)), + new(typeof(DynamicClassDictionary)), + ]; + + [TestCaseSource(nameof(IDictionaryContainsTestCases))] + public void IDictionaryContainsMethodIsBound(Type dictType) + { + using var _ = Py.GIL(); + + var module = PyModule.FromString("IDictionaryContainsMethodIsBound", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +def contains(dictionary, key): + return key in dictionary +"); + + using var contains = module.GetAttr("contains"); + + var dictionary = Convert.ChangeType(Activator.CreateInstance(dictType), dictType); + var key1 = "key1"; + (dictionary as dynamic).Add(key1, "value1"); + + using var pyDictionary = dictionary.ToPython(); + using var pyKey1 = key1.ToPython(); + + var result = contains.Invoke(pyDictionary, pyKey1).As(); + Assert.IsTrue(result); + + using var pyKey2 = "key2".ToPython(); + result = contains.Invoke(pyDictionary, pyKey2).As(); + Assert.IsFalse(result); + } + + [TestCaseSource(nameof(IDictionaryContainsTestCases))] + public void CanCheckIfNoneIsInDictionary(Type dictType) + { + using var _ = Py.GIL(); + + var module = PyModule.FromString("CanCheckIfNoneIsInDictionary", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +def contains(dictionary, key): + return key in dictionary +"); + + using var contains = module.GetAttr("contains"); + + var dictionary = Convert.ChangeType(Activator.CreateInstance(dictType), dictType); + (dictionary as dynamic).Add("key1", "value1"); + + using var pyDictionary = dictionary.ToPython(); + + var result = false; + Assert.DoesNotThrow(() => result = contains.Invoke(pyDictionary, PyObject.None).As()); + Assert.IsFalse(result); + } + + public class TestDictionary : IDictionary + { + private readonly Dictionary _data = new(); + + public object this[object key] { get => ((IDictionary)_data)[key]; set => ((IDictionary)_data)[key] = value; } + + public bool IsFixedSize => ((IDictionary)_data).IsFixedSize; + + public bool IsReadOnly => ((IDictionary)_data).IsReadOnly; + + public ICollection Keys => ((IDictionary)_data).Keys; + + public ICollection Values => ((IDictionary)_data).Values; + + public int Count => ((ICollection)_data).Count; + + public bool IsSynchronized => ((ICollection)_data).IsSynchronized; + + public object SyncRoot => ((ICollection)_data).SyncRoot; + + public void Add(object key, object value) + { + ((IDictionary)_data).Add(key, value); + } + + public void Clear() + { + ((IDictionary)_data).Clear(); + } + + public bool Contains(object key) + { + return ((IDictionary)_data).Contains(key); + } + + public void CopyTo(Array array, int index) + { + ((ICollection)_data).CopyTo(array, index); + } + + public IDictionaryEnumerator GetEnumerator() + { + return ((IDictionary)_data).GetEnumerator(); + } + + public void Remove(object key) + { + ((IDictionary)_data).Remove(key); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)_data).GetEnumerator(); + } + + public bool ContainsKey(TKey key) + { + return Contains(key); + } + } + + public class TestKeyValueContainer + where TKey: class + where TValue: class + { + private readonly Dictionary _data = new(); + public int Count => _data.Count; + public bool ContainsKey(TKey key) + { + return _data.ContainsKey(key); + } + public void Add(TKey key, TValue value) + { + _data.Add(key, value); + } + } + + public class DynamicClassDictionary : TestPropertyAccess.DynamicFixture + { + private readonly Dictionary _data = new(); + public int Count => _data.Count; + public bool ContainsKey(TKey key) + { + return _data.ContainsKey(key); + } + public void Add(TKey key, TValue value) + { + _data.Add(key, value); + } + } } public class NestedTestParent diff --git a/src/runtime/ClassManager.cs b/src/runtime/ClassManager.cs index 58f80ce30..bf852112c 100644 --- a/src/runtime/ClassManager.cs +++ b/src/runtime/ClassManager.cs @@ -205,7 +205,19 @@ internal static ClassBase CreateClass(Type type) else if (typeof(IDynamicMetaObjectProvider).IsAssignableFrom(type)) { - impl = new DynamicClassObject(type); + if (type.IsLookUp()) + { + impl = new DynamicClassLookUpObject(type); + } + else + { + impl = new DynamicClassObject(type); + } + } + + else if (type.IsLookUp()) + { + impl = new LookUpObject(type); } else diff --git a/src/runtime/Types/DynamicClassLookUpObject.cs b/src/runtime/Types/DynamicClassLookUpObject.cs new file mode 100644 index 000000000..2c570fe20 --- /dev/null +++ b/src/runtime/Types/DynamicClassLookUpObject.cs @@ -0,0 +1,34 @@ +using System; + +namespace Python.Runtime +{ + /// + /// Implements a Python type for managed DynamicClass objects that support look up (dictionaries), + /// that is, they implement ContainsKey(). + /// This type is essentially the same as a ClassObject, except that it provides + /// sequence semantics to support natural dictionary usage (__contains__ and __len__) + /// from Python. + /// + internal class DynamicClassLookUpObject : DynamicClassObject + { + internal DynamicClassLookUpObject(Type tp) : base(tp) + { + } + + /// + /// Implements __len__ for dictionary types. + /// + public static int mp_length(BorrowedReference ob) + { + return LookUpObject.mp_length(ob); + } + + /// + /// Implements __contains__ for dictionary types. + /// + public static int sq_contains(BorrowedReference ob, BorrowedReference v) + { + return LookUpObject.sq_contains(ob, v); + } + } +} diff --git a/src/runtime/Types/KeyValuePairEnumerableObject.cs b/src/runtime/Types/KeyValuePairEnumerableObject.cs index 95a0180e1..04c3f66f9 100644 --- a/src/runtime/Types/KeyValuePairEnumerableObject.cs +++ b/src/runtime/Types/KeyValuePairEnumerableObject.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Reflection; namespace Python.Runtime { @@ -10,75 +9,14 @@ namespace Python.Runtime /// sequence semantics to support natural dictionary usage (__contains__ and __len__) /// from Python. /// - internal class KeyValuePairEnumerableObject : ClassObject + internal class KeyValuePairEnumerableObject : LookUpObject { - [NonSerialized] - private static Dictionary, MethodInfo> methodsByType = new Dictionary, MethodInfo>(); - private static List requiredMethods = new List { "Count", "ContainsKey" }; - - internal static bool VerifyMethodRequirements(Type type) - { - foreach (var requiredMethod in requiredMethods) - { - var method = type.GetMethod(requiredMethod); - if (method == null) - { - method = type.GetMethod($"get_{requiredMethod}"); - if (method == null) - { - return false; - } - } - - var key = Tuple.Create(type, requiredMethod); - methodsByType.Add(key, method); - } - - return true; - } - internal KeyValuePairEnumerableObject(Type tp) : base(tp) { } internal override bool CanSubclass() => false; - - /// - /// Implements __len__ for dictionary types. - /// - public static int mp_length(BorrowedReference ob) - { - var obj = (CLRObject)GetManagedObject(ob); - var self = obj.inst; - - var key = Tuple.Create(self.GetType(), "Count"); - var methodInfo = methodsByType[key]; - - return (int)methodInfo.Invoke(self, null); - } - - /// - /// Implements __contains__ for dictionary types. - /// - public static int sq_contains(BorrowedReference ob, BorrowedReference v) - { - var obj = (CLRObject)GetManagedObject(ob); - var self = obj.inst; - - var key = Tuple.Create(self.GetType(), "ContainsKey"); - var methodInfo = methodsByType[key]; - - var parameters = methodInfo.GetParameters(); - object arg; - if (!Converter.ToManaged(v, parameters[0].ParameterType, out arg, false)) - { - Exceptions.SetError(Exceptions.TypeError, - $"invalid parameter type for sq_contains: should be {Converter.GetTypeByAlias(v)}, found {parameters[0].ParameterType}"); - } - - return (bool)methodInfo.Invoke(self, new[] { arg }) ? 1 : 0; - } } public static class KeyValuePairEnumerableObjectExtension @@ -102,7 +40,7 @@ public static bool IsKeyValuePairEnumerable(this Type type) a.GetGenericTypeDefinition() == keyValuePairType && a.GetGenericArguments().Length == 2) { - return KeyValuePairEnumerableObject.VerifyMethodRequirements(type); + return LookUpObject.VerifyMethodRequirements(type); } } } diff --git a/src/runtime/Types/LookUpObject.cs b/src/runtime/Types/LookUpObject.cs new file mode 100644 index 000000000..04520132c --- /dev/null +++ b/src/runtime/Types/LookUpObject.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Python.Runtime +{ + /// + /// Implements a Python type for managed objects that support look up (dictionaries), + /// that is, they implement ContainsKey(). + /// This type is essentially the same as a ClassObject, except that it provides + /// sequence semantics to support natural dictionary usage (__contains__ and __len__) + /// from Python. + /// + internal class LookUpObject : ClassObject + { + [NonSerialized] + private static Dictionary, MethodInfo> methodsByType = new Dictionary, MethodInfo>(); + private static List<(string, int)> requiredMethods = new (){ ("Count", 0), ("ContainsKey", 1) }; + + private static MethodInfo GetRequiredMethod(MethodInfo[] methods, string methodName, int parametersCount) + { + return methods.FirstOrDefault(m => m.Name == methodName && m.GetParameters().Length == parametersCount); + } + + internal static bool VerifyMethodRequirements(Type type) + { + var methods = type.GetMethods(); + + foreach (var (requiredMethod, parametersCount) in requiredMethods) + { + var method = GetRequiredMethod(methods, requiredMethod, parametersCount); + if (method == null) + { + var getterName = $"get_{requiredMethod}"; + method = GetRequiredMethod(methods, getterName, parametersCount); + if (method == null) + { + return false; + } + } + + var key = Tuple.Create(type, requiredMethod); + methodsByType.Add(key, method); + } + + return true; + } + + internal LookUpObject(Type tp) : base(tp) + { + } + + /// + /// Implements __len__ for dictionary types. + /// + public static int mp_length(BorrowedReference ob) + { + return LookUpObjectExtensions.Length(ob, methodsByType); + } + + /// + /// Implements __contains__ for dictionary types. + /// + public static int sq_contains(BorrowedReference ob, BorrowedReference v) + { + return LookUpObjectExtensions.Contains(ob, v, methodsByType); + } + } + + internal static class LookUpObjectExtensions + { + internal static bool IsLookUp(this Type type) + { + return LookUpObject.VerifyMethodRequirements(type); + } + + /// + /// Implements __len__ for dictionary types. + /// + internal static int Length(BorrowedReference ob, Dictionary, MethodInfo> methodsByType) + { + var obj = (CLRObject)ManagedType.GetManagedObject(ob); + var self = obj.inst; + + var key = Tuple.Create(self.GetType(), "Count"); + var methodInfo = methodsByType[key]; + + return (int)methodInfo.Invoke(self, null); + } + + /// + /// Implements __contains__ for dictionary types. + /// + internal static int Contains(BorrowedReference ob, BorrowedReference v, Dictionary, MethodInfo> methodsByType) + { + var obj = (CLRObject)ManagedType.GetManagedObject(ob); + var self = obj.inst; + + var key = Tuple.Create(self.GetType(), "ContainsKey"); + var methodInfo = methodsByType[key]; + + var parameters = methodInfo.GetParameters(); + object arg; + if (!Converter.ToManaged(v, parameters[0].ParameterType, out arg, false)) + { + Exceptions.SetError(Exceptions.TypeError, + $"invalid parameter type for sq_contains: should be {Converter.GetTypeByAlias(v)}, found {parameters[0].ParameterType}"); + } + + // If the argument is None, we return false. Python allows using None as key, + // but C# doesn't and will throw, so we shortcut here + if (arg == null) + { + return 0; + } + + return (bool)methodInfo.Invoke(self, new[] { arg }) ? 1 : 0; + } + } +} From 68a2183e07835d1c2e22a82aa863166801d37eea Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 2 May 2025 09:07:37 -0400 Subject: [PATCH 15/24] Add __bool__ for MetaType (#102) * Add __bool__ for MetaType * Bump version to 2.0.44 * Minor fix --- src/embed_tests/ClassManagerTests.cs | 27 +++++++++++++++++++ src/perf_tests/Python.PerformanceTests.csproj | 4 +-- src/runtime/Native/ITypeOffsets.cs | 1 + src/runtime/Native/TypeOffset.cs | 1 + src/runtime/Properties/AssemblyInfo.cs | 4 +-- src/runtime/Python.Runtime.csproj | 2 +- src/runtime/Types/MetaType.cs | 10 +++++++ 7 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/embed_tests/ClassManagerTests.cs b/src/embed_tests/ClassManagerTests.cs index dcdf66edb..2fd38f272 100644 --- a/src/embed_tests/ClassManagerTests.cs +++ b/src/embed_tests/ClassManagerTests.cs @@ -1085,6 +1085,33 @@ def is_enum_value_defined(): } } + [Test] + public void TruthinessCanBeCheckedForTypes() + { + using (Py.GIL()) + { + var module = PyModule.FromString("TruthinessCanBeCheckedForTypes", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +def throw_if_falsy(): + if not ClassManagerTests: + raise Exception(""ClassManagerTests is falsy"") + +def throw_if_not_truthy(): + if ClassManagerTests: + return + raise Exception(""ClassManagerTests is not truthy"") +"); + + // Types are always truthy + Assert.DoesNotThrow(() => module.InvokeMethod("throw_if_falsy")); + Assert.DoesNotThrow(() => module.InvokeMethod("throw_if_not_truthy")); + } + } + private static TestCaseData[] IDictionaryContainsTestCases => [ new(typeof(TestDictionary)), diff --git a/src/perf_tests/Python.PerformanceTests.csproj b/src/perf_tests/Python.PerformanceTests.csproj index 99f447f56..ee239ff12 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/Native/ITypeOffsets.cs b/src/runtime/Native/ITypeOffsets.cs index 2c4fdf59a..fb65e76f8 100644 --- a/src/runtime/Native/ITypeOffsets.cs +++ b/src/runtime/Native/ITypeOffsets.cs @@ -30,6 +30,7 @@ interface ITypeOffsets int nb_invert { get; } int nb_inplace_add { get; } int nb_inplace_subtract { get; } + int nb_bool { get; } int ob_size { get; } int ob_type { get; } int qualname { get; } diff --git a/src/runtime/Native/TypeOffset.cs b/src/runtime/Native/TypeOffset.cs index a1bae8253..0a85b05d2 100644 --- a/src/runtime/Native/TypeOffset.cs +++ b/src/runtime/Native/TypeOffset.cs @@ -37,6 +37,7 @@ static partial class TypeOffset internal static int nb_invert { get; private set; } internal static int nb_inplace_add { get; private set; } internal static int nb_inplace_subtract { get; private set; } + internal static int nb_bool { get; private set; } internal static int ob_size { get; private set; } internal static int ob_type { get; private set; } internal static int qualname { get; private set; } diff --git a/src/runtime/Properties/AssemblyInfo.cs b/src/runtime/Properties/AssemblyInfo.cs index c8a43c43a..c3e7c304f 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.43")] -[assembly: AssemblyFileVersion("2.0.43")] +[assembly: AssemblyVersion("2.0.44")] +[assembly: AssemblyFileVersion("2.0.44")] diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index f1f77f9d7..9b870ed44 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.43 + 2.0.44 false LICENSE https://github.com/pythonnet/pythonnet diff --git a/src/runtime/Types/MetaType.cs b/src/runtime/Types/MetaType.cs index bfaced5f6..9a66240d3 100644 --- a/src/runtime/Types/MetaType.cs +++ b/src/runtime/Types/MetaType.cs @@ -386,6 +386,16 @@ public static int mp_length(BorrowedReference tp) return Enum.GetValues(type).Length; } + /// + /// Implements __bool__ for types, so that Python uses this instead of __len__ as default. + /// For types, this is always "true" + /// + public static int nb_bool(BorrowedReference tp) + { + var cb = GetManagedObject(tp) as ClassBase; + return cb == null || !cb.type.Valid ? 0 : 1; + } + /// /// Implements __contains__ for Enum types. /// From 5f36aa7aa73079f68591a3e56ade054df805868c Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 31 Jul 2025 11:59:23 -0400 Subject: [PATCH 16/24] C# enums to work as proper enums in Python (#103) * Make C# enums work as proper enums in Python Avoid converting C# enums to long in Python * Minor fixes in substraction and division operators * Bump version to 2.0.45 * Support enum comparison to other enum types Compare based on the underlying int value * Use single cached reference for C# enum values in Python Make C# enums work as singletons in Python so that the `is` identity comparison operator works for C# enums as well. * Minor fix * More tests and cleanup * Reduce enum operators overloads * Fix comparison to null/None * Minor change --- src/embed_tests/EnumTests.cs | 628 ++++++++++++++++++ src/embed_tests/TestMethodBinder.cs | 34 + src/perf_tests/Python.PerformanceTests.csproj | 4 +- src/runtime/Converter.cs | 17 + src/runtime/MethodBinder.cs | 11 + src/runtime/Properties/AssemblyInfo.cs | 4 +- src/runtime/Python.Runtime.csproj | 2 +- src/runtime/Util/OpsHelper.cs | 498 +++++++++++++- 8 files changed, 1191 insertions(+), 7 deletions(-) create mode 100644 src/embed_tests/EnumTests.cs diff --git a/src/embed_tests/EnumTests.cs b/src/embed_tests/EnumTests.cs new file mode 100644 index 000000000..f8f1789d2 --- /dev/null +++ b/src/embed_tests/EnumTests.cs @@ -0,0 +1,628 @@ +using System; +using System.Collections.Generic; + +using NUnit.Framework; + +using Python.Runtime; + +namespace Python.EmbeddingTest +{ + public class EnumTests + { + private static VerticalDirection[] VerticalDirectionEnumValues = Enum.GetValues(); + private static HorizontalDirection[] HorizontalDirectionEnumValues = Enum.GetValues(); + + [OneTimeSetUp] + public void SetUp() + { + PythonEngine.Initialize(); + } + + [OneTimeTearDown] + public void Dispose() + { + PythonEngine.Shutdown(); + } + + public enum VerticalDirection + { + Down = -2, + Flat = 0, + Up = 2, + } + + public enum HorizontalDirection + { + Left = -2, + Flat = 0, + Right = 2, + } + + [Test] + public void CSharpEnumsBehaveAsEnumsInPython() + { + using var _ = Py.GIL(); + using var module = PyModule.FromString("CSharpEnumsBehaveAsEnumsInPython", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +def enum_is_right_type(enum_value={nameof(EnumTests)}.{nameof(VerticalDirection)}.{nameof(VerticalDirection.Up)}): + return isinstance(enum_value, {nameof(EnumTests)}.{nameof(VerticalDirection)}) +"); + + Assert.IsTrue(module.InvokeMethod("enum_is_right_type").As()); + + // Also test passing the enum value from C# to Python + using var pyEnumValue = VerticalDirection.Up.ToPython(); + Assert.IsTrue(module.InvokeMethod("enum_is_right_type", pyEnumValue).As()); + } + + private PyModule GetTestOperatorsModule(string @operator, VerticalDirection operand1, double operand2) + { + var operand1Str = $"{nameof(EnumTests)}.{nameof(VerticalDirection)}.{operand1}"; + return PyModule.FromString("GetTestOperatorsModule", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +def operation1(): + return {operand1Str} {@operator} {operand2} + +def operation2(): + return {operand2} {@operator} {operand1Str} +"); + } + + [TestCase("*", VerticalDirection.Down, 2, -4, -4)] + [TestCase("/", VerticalDirection.Down, 2, -1, -1)] + [TestCase("+", VerticalDirection.Down, 2, 0, 0)] + [TestCase("-", VerticalDirection.Down, 2, -4, 4)] + [TestCase("*", VerticalDirection.Flat, 2, 0, 0)] + [TestCase("/", VerticalDirection.Flat, 2, 0, 0)] + [TestCase("+", VerticalDirection.Flat, 2, 2, 2)] + [TestCase("-", VerticalDirection.Flat, 2, -2, 2)] + [TestCase("*", VerticalDirection.Up, 2, 4, 4)] + [TestCase("/", VerticalDirection.Up, 2, 1, 1)] + [TestCase("+", VerticalDirection.Up, 2, 4, 4)] + [TestCase("-", VerticalDirection.Up, 2, 0, 0)] + [TestCase("*", VerticalDirection.Down, -2, 4, 4)] + [TestCase("/", VerticalDirection.Down, -2, 1, 1)] + [TestCase("+", VerticalDirection.Down, -2, -4, -4)] + [TestCase("-", VerticalDirection.Down, -2, 0, 0)] + [TestCase("*", VerticalDirection.Flat, -2, 0, 0)] + [TestCase("/", VerticalDirection.Flat, -2, 0, 0)] + [TestCase("+", VerticalDirection.Flat, -2, -2, -2)] + [TestCase("-", VerticalDirection.Flat, -2, 2, -2)] + [TestCase("*", VerticalDirection.Up, -2, -4, -4)] + [TestCase("/", VerticalDirection.Up, -2, -1, -1)] + [TestCase("+", VerticalDirection.Up, -2, 0, 0)] + [TestCase("-", VerticalDirection.Up, -2, 4, -4)] + public void ArithmeticOperatorsWorkWithoutExplicitCast(string @operator, VerticalDirection operand1, double operand2, double expectedResult, double invertedOperationExpectedResult) + { + using var _ = Py.GIL(); + using var module = GetTestOperatorsModule(@operator, operand1, operand2); + + Assert.AreEqual(expectedResult, module.InvokeMethod("operation1").As()); + + if (Convert.ToInt64(operand1) != 0 || @operator != "/") + { + Assert.AreEqual(invertedOperationExpectedResult, module.InvokeMethod("operation2").As()); + } + } + + [TestCase("==", VerticalDirection.Down, -2, true)] + [TestCase("==", VerticalDirection.Down, 0, false)] + [TestCase("==", VerticalDirection.Down, 2, false)] + [TestCase("==", VerticalDirection.Flat, -2, false)] + [TestCase("==", VerticalDirection.Flat, 0, true)] + [TestCase("==", VerticalDirection.Flat, 2, false)] + [TestCase("==", VerticalDirection.Up, -2, false)] + [TestCase("==", VerticalDirection.Up, 0, false)] + [TestCase("==", VerticalDirection.Up, 2, true)] + [TestCase("!=", VerticalDirection.Down, -2, false)] + [TestCase("!=", VerticalDirection.Down, 0, true)] + [TestCase("!=", VerticalDirection.Down, 2, true)] + [TestCase("!=", VerticalDirection.Flat, -2, true)] + [TestCase("!=", VerticalDirection.Flat, 0, false)] + [TestCase("!=", VerticalDirection.Flat, 2, true)] + [TestCase("!=", VerticalDirection.Up, -2, true)] + [TestCase("!=", VerticalDirection.Up, 0, true)] + [TestCase("!=", VerticalDirection.Up, 2, false)] + [TestCase("<", VerticalDirection.Down, -3, false)] + [TestCase("<", VerticalDirection.Down, -2, false)] + [TestCase("<", VerticalDirection.Down, 0, true)] + [TestCase("<", VerticalDirection.Down, 2, true)] + [TestCase("<", VerticalDirection.Flat, -2, false)] + [TestCase("<", VerticalDirection.Flat, 0, false)] + [TestCase("<", VerticalDirection.Flat, 2, true)] + [TestCase("<", VerticalDirection.Up, -2, false)] + [TestCase("<", VerticalDirection.Up, 0, false)] + [TestCase("<", VerticalDirection.Up, 2, false)] + [TestCase("<", VerticalDirection.Up, 3, true)] + [TestCase("<=", VerticalDirection.Down, -3, false)] + [TestCase("<=", VerticalDirection.Down, -2, true)] + [TestCase("<=", VerticalDirection.Down, 0, true)] + [TestCase("<=", VerticalDirection.Down, 2, true)] + [TestCase("<=", VerticalDirection.Flat, -2, false)] + [TestCase("<=", VerticalDirection.Flat, 0, true)] + [TestCase("<=", VerticalDirection.Flat, 2, true)] + [TestCase("<=", VerticalDirection.Up, -2, false)] + [TestCase("<=", VerticalDirection.Up, 0, false)] + [TestCase("<=", VerticalDirection.Up, 2, true)] + [TestCase("<=", VerticalDirection.Up, 3, true)] + [TestCase(">", VerticalDirection.Down, -3, true)] + [TestCase(">", VerticalDirection.Down, -2, false)] + [TestCase(">", VerticalDirection.Down, 0, false)] + [TestCase(">", VerticalDirection.Down, 2, false)] + [TestCase(">", VerticalDirection.Flat, -2, true)] + [TestCase(">", VerticalDirection.Flat, 0, false)] + [TestCase(">", VerticalDirection.Flat, 2, false)] + [TestCase(">", VerticalDirection.Up, -2, true)] + [TestCase(">", VerticalDirection.Up, 0, true)] + [TestCase(">", VerticalDirection.Up, 2, false)] + [TestCase(">", VerticalDirection.Up, 3, false)] + [TestCase(">=", VerticalDirection.Down, -3, true)] + [TestCase(">=", VerticalDirection.Down, -2, true)] + [TestCase(">=", VerticalDirection.Down, 0, false)] + [TestCase(">=", VerticalDirection.Down, 2, false)] + [TestCase(">=", VerticalDirection.Flat, -2, true)] + [TestCase(">=", VerticalDirection.Flat, 0, true)] + [TestCase(">=", VerticalDirection.Flat, 2, false)] + [TestCase(">=", VerticalDirection.Up, -2, true)] + [TestCase(">=", VerticalDirection.Up, 0, true)] + [TestCase(">=", VerticalDirection.Up, 2, true)] + [TestCase(">=", VerticalDirection.Up, 3, false)] + public void IntComparisonOperatorsWorkWithoutExplicitCast(string @operator, VerticalDirection operand1, int operand2, bool expectedResult) + { + using var _ = Py.GIL(); + using var module = GetTestOperatorsModule(@operator, operand1, operand2); + + Assert.AreEqual(expectedResult, module.InvokeMethod("operation1").As()); + + var invertedOperationExpectedResult = (@operator.StartsWith('<') || @operator.StartsWith('>')) && Convert.ToInt64(operand1) != operand2 + ? !expectedResult + : expectedResult; + Assert.AreEqual(invertedOperationExpectedResult, module.InvokeMethod("operation2").As()); + } + + [TestCase("==", VerticalDirection.Down, -2.0, true)] + [TestCase("==", VerticalDirection.Down, -2.00001, false)] + [TestCase("==", VerticalDirection.Down, -1.99999, false)] + [TestCase("==", VerticalDirection.Down, 0.0, false)] + [TestCase("==", VerticalDirection.Down, 2.0, false)] + [TestCase("==", VerticalDirection.Flat, -2.0, false)] + [TestCase("==", VerticalDirection.Flat, 0.0, true)] + [TestCase("==", VerticalDirection.Flat, 0.00001, false)] + [TestCase("==", VerticalDirection.Flat, -0.00001, false)] + [TestCase("==", VerticalDirection.Flat, 2.0, false)] + [TestCase("==", VerticalDirection.Up, -2.0, false)] + [TestCase("==", VerticalDirection.Up, 0.0, false)] + [TestCase("==", VerticalDirection.Up, 2.0, true)] + [TestCase("==", VerticalDirection.Up, 2.00001, false)] + [TestCase("==", VerticalDirection.Up, 1.99999, false)] + [TestCase("!=", VerticalDirection.Down, -2.0, false)] + [TestCase("!=", VerticalDirection.Down, -2.00001, true)] + [TestCase("!=", VerticalDirection.Down, -1.99999, true)] + [TestCase("!=", VerticalDirection.Down, 0.0, true)] + [TestCase("!=", VerticalDirection.Down, 2.0, true)] + [TestCase("!=", VerticalDirection.Flat, -2.0, true)] + [TestCase("!=", VerticalDirection.Flat, 0.0, false)] + [TestCase("!=", VerticalDirection.Flat, 0.00001, true)] + [TestCase("!=", VerticalDirection.Flat, -0.00001, true)] + [TestCase("!=", VerticalDirection.Flat, 2.0, true)] + [TestCase("!=", VerticalDirection.Up, -2.0, true)] + [TestCase("!=", VerticalDirection.Up, 0.0, true)] + [TestCase("!=", VerticalDirection.Up, 2.0, false)] + [TestCase("!=", VerticalDirection.Up, 2.00001, true)] + [TestCase("!=", VerticalDirection.Up, 1.99999, true)] + [TestCase("<", VerticalDirection.Down, -3.0, false)] + [TestCase("<", VerticalDirection.Down, -2.00001, false)] + [TestCase("<", VerticalDirection.Down, -2.0, false)] + [TestCase("<", VerticalDirection.Down, -1.99999, true)] + [TestCase("<", VerticalDirection.Down, 0.0, true)] + [TestCase("<", VerticalDirection.Down, 2.0, true)] + [TestCase("<", VerticalDirection.Flat, -2.0, false)] + [TestCase("<", VerticalDirection.Flat, -0.00001, false)] + [TestCase("<", VerticalDirection.Flat, 0.0, false)] + [TestCase("<", VerticalDirection.Flat, 0.00001, true)] + [TestCase("<", VerticalDirection.Flat, 2.0, true)] + [TestCase("<", VerticalDirection.Up, -2.0, false)] + [TestCase("<", VerticalDirection.Up, 0.0, false)] + [TestCase("<", VerticalDirection.Up, 1.99999, false)] + [TestCase("<", VerticalDirection.Up, 2.0, false)] + [TestCase("<", VerticalDirection.Up, 2.00001, true)] + [TestCase("<", VerticalDirection.Up, 3.0, true)] + [TestCase("<=", VerticalDirection.Down, -3.0, false)] + [TestCase("<=", VerticalDirection.Down, -2.00001, false)] + [TestCase("<=", VerticalDirection.Down, -2.0, true)] + [TestCase("<=", VerticalDirection.Down, -1.99999, true)] + [TestCase("<=", VerticalDirection.Down, 0.0, true)] + [TestCase("<=", VerticalDirection.Down, 2.0, true)] + [TestCase("<=", VerticalDirection.Flat, -2.0, false)] + [TestCase("<=", VerticalDirection.Flat, -0.00001, false)] + [TestCase("<=", VerticalDirection.Flat, 0.0, true)] + [TestCase("<=", VerticalDirection.Flat, 0.00001, true)] + [TestCase("<=", VerticalDirection.Flat, 2.0, true)] + [TestCase("<=", VerticalDirection.Up, -2.0, false)] + [TestCase("<=", VerticalDirection.Up, 0.0, false)] + [TestCase("<=", VerticalDirection.Up, 1.99999, false)] + [TestCase("<=", VerticalDirection.Up, 2.0, true)] + [TestCase("<=", VerticalDirection.Up, 2.00001, true)] + [TestCase("<=", VerticalDirection.Up, 3.0, true)] + [TestCase(">", VerticalDirection.Down, -3.0, true)] + [TestCase(">", VerticalDirection.Down, -2.00001, true)] + [TestCase(">", VerticalDirection.Down, -2.0, false)] + [TestCase(">", VerticalDirection.Down, -1.99999, false)] + [TestCase(">", VerticalDirection.Down, 0.0, false)] + [TestCase(">", VerticalDirection.Down, 2.0, false)] + [TestCase(">", VerticalDirection.Flat, -2.0, true)] + [TestCase(">", VerticalDirection.Flat, -0.00001, true)] + [TestCase(">", VerticalDirection.Flat, 0.0, false)] + [TestCase(">", VerticalDirection.Flat, 0.00001, false)] + [TestCase(">", VerticalDirection.Flat, 2.0, false)] + [TestCase(">", VerticalDirection.Up, -2.0, true)] + [TestCase(">", VerticalDirection.Up, 0.0, true)] + [TestCase(">", VerticalDirection.Up, 1.99999, true)] + [TestCase(">", VerticalDirection.Up, 2.0, false)] + [TestCase(">", VerticalDirection.Up, 2.00001, false)] + [TestCase(">", VerticalDirection.Up, 3.0, false)] + [TestCase(">=", VerticalDirection.Down, -3.0, true)] + [TestCase(">=", VerticalDirection.Down, -2.00001, true)] + [TestCase(">=", VerticalDirection.Down, -2.0, true)] + [TestCase(">=", VerticalDirection.Down, -1.99999, false)] + [TestCase(">=", VerticalDirection.Down, 0.0, false)] + [TestCase(">=", VerticalDirection.Down, 2.0, false)] + [TestCase(">=", VerticalDirection.Flat, -2.0, true)] + [TestCase(">=", VerticalDirection.Flat, -0.00001, true)] + [TestCase(">=", VerticalDirection.Flat, 0.0, true)] + [TestCase(">=", VerticalDirection.Flat, 0.00001, false)] + [TestCase(">=", VerticalDirection.Flat, 2.0, false)] + [TestCase(">=", VerticalDirection.Up, -2.0, true)] + [TestCase(">=", VerticalDirection.Up, 0.0, true)] + [TestCase(">=", VerticalDirection.Up, 1.99999, true)] + [TestCase(">=", VerticalDirection.Up, 2.0, true)] + [TestCase(">=", VerticalDirection.Up, 2.00001, false)] + [TestCase(">=", VerticalDirection.Up, 3.0, false)] + public void FloatComparisonOperatorsWorkWithoutExplicitCast(string @operator, VerticalDirection operand1, double operand2, bool expectedResult) + { + using var _ = Py.GIL(); + using var module = GetTestOperatorsModule(@operator, operand1, operand2); + + Assert.AreEqual(expectedResult, module.InvokeMethod("operation1").As()); + + var invertedOperationExpectedResult = (@operator.StartsWith('<') || @operator.StartsWith('>')) && Convert.ToInt64(operand1) != operand2 + ? !expectedResult + : expectedResult; + Assert.AreEqual(invertedOperationExpectedResult, module.InvokeMethod("operation2").As()); + } + + public static IEnumerable SameEnumTypeComparisonOperatorsTestCases + { + get + { + var operators = new[] { "==", "!=", "<", "<=", ">", ">=" }; + + foreach (var enumValue in VerticalDirectionEnumValues) + { + foreach (var enumValue2 in VerticalDirectionEnumValues) + { + yield return new TestCaseData("==", enumValue, enumValue2, enumValue == enumValue2); + yield return new TestCaseData("!=", enumValue, enumValue2, enumValue != enumValue2); + yield return new TestCaseData("<", enumValue, enumValue2, enumValue < enumValue2); + yield return new TestCaseData("<=", enumValue, enumValue2, enumValue <= enumValue2); + yield return new TestCaseData(">", enumValue, enumValue2, enumValue > enumValue2); + yield return new TestCaseData(">=", enumValue, enumValue2, enumValue >= enumValue2); + } + } + } + } + + [TestCaseSource(nameof(SameEnumTypeComparisonOperatorsTestCases))] + public void SameEnumTypeComparisonOperatorsWorkWithoutExplicitCast(string @operator, VerticalDirection operand1, VerticalDirection operand2, bool expectedResult) + { + using var _ = Py.GIL(); + using var module = PyModule.FromString("SameEnumTypeComparisonOperatorsWorkWithoutExplicitCast", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +def operation(): + return {nameof(EnumTests)}.{nameof(VerticalDirection)}.{operand1} {@operator} {nameof(EnumTests)}.{nameof(VerticalDirection)}.{operand2} +"); + + Assert.AreEqual(expectedResult, module.InvokeMethod("operation").As()); + } + + [TestCase("==", VerticalDirection.Down, "Down", true)] + [TestCase("==", VerticalDirection.Down, "Flat", false)] + [TestCase("==", VerticalDirection.Down, "Up", false)] + [TestCase("==", VerticalDirection.Flat, "Down", false)] + [TestCase("==", VerticalDirection.Flat, "Flat", true)] + [TestCase("==", VerticalDirection.Flat, "Up", false)] + [TestCase("==", VerticalDirection.Up, "Down", false)] + [TestCase("==", VerticalDirection.Up, "Flat", false)] + [TestCase("==", VerticalDirection.Up, "Up", true)] + [TestCase("!=", VerticalDirection.Down, "Down", false)] + [TestCase("!=", VerticalDirection.Down, "Flat", true)] + [TestCase("!=", VerticalDirection.Down, "Up", true)] + [TestCase("!=", VerticalDirection.Flat, "Down", true)] + [TestCase("!=", VerticalDirection.Flat, "Flat", false)] + [TestCase("!=", VerticalDirection.Flat, "Up", true)] + [TestCase("!=", VerticalDirection.Up, "Down", true)] + [TestCase("!=", VerticalDirection.Up, "Flat", true)] + [TestCase("!=", VerticalDirection.Up, "Up", false)] + public void EnumComparisonOperatorsWorkWithString(string @operator, VerticalDirection operand1, string operand2, bool expectedResult) + { + using var _ = Py.GIL(); + using var module = PyModule.FromString("EnumComparisonOperatorsWorkWithString", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +def operation1(): + return {nameof(EnumTests)}.{nameof(VerticalDirection)}.{operand1} {@operator} ""{operand2}"" + +def operation2(): + return ""{operand2}"" {@operator} {nameof(EnumTests)}.{nameof(VerticalDirection)}.{operand1} +"); + + Assert.AreEqual(expectedResult, module.InvokeMethod("operation1").As()); + Assert.AreEqual(expectedResult, module.InvokeMethod("operation2").As()); + } + + public static IEnumerable OtherEnumsComparisonOperatorsTestCases + { + get + { + var operators = new[] { "==", "!=", "<", "<=", ">", ">=" }; + + foreach (var enumValue in VerticalDirectionEnumValues) + { + foreach (var enum2Value in HorizontalDirectionEnumValues) + { + var intEnumValue = Convert.ToInt64(enumValue); + var intEnum2Value = Convert.ToInt64(enum2Value); + + yield return new TestCaseData("==", enumValue, enum2Value, intEnumValue == intEnum2Value, intEnum2Value == intEnumValue); + yield return new TestCaseData("!=", enumValue, enum2Value, intEnumValue != intEnum2Value, intEnum2Value != intEnumValue); + yield return new TestCaseData("<", enumValue, enum2Value, intEnumValue < intEnum2Value, intEnum2Value < intEnumValue); + yield return new TestCaseData("<=", enumValue, enum2Value, intEnumValue <= intEnum2Value, intEnum2Value <= intEnumValue); + yield return new TestCaseData(">", enumValue, enum2Value, intEnumValue > intEnum2Value, intEnum2Value > intEnumValue); + yield return new TestCaseData(">=", enumValue, enum2Value, intEnumValue >= intEnum2Value, intEnum2Value >= intEnumValue); + } + } + } + } + + [TestCaseSource(nameof(OtherEnumsComparisonOperatorsTestCases))] + public void OtherEnumsComparisonOperatorsWorkWithoutExplicitCast(string @operator, VerticalDirection operand1, HorizontalDirection operand2, bool expectedResult, bool invertedOperationExpectedResult) + { + using var _ = Py.GIL(); + using var module = PyModule.FromString("OtherEnumsComparisonOperatorsWorkWithoutExplicitCast", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +def operation1(): + return {nameof(EnumTests)}.{nameof(VerticalDirection)}.{operand1} {@operator} {nameof(EnumTests)}.{nameof(HorizontalDirection)}.{operand2} + +def operation2(): + return {nameof(EnumTests)}.{nameof(HorizontalDirection)}.{operand2} {@operator} {nameof(EnumTests)}.{nameof(VerticalDirection)}.{operand1} +"); + + Assert.AreEqual(expectedResult, module.InvokeMethod("operation1").As()); + Assert.AreEqual(invertedOperationExpectedResult, module.InvokeMethod("operation2").As()); + } + + private static IEnumerable IdentityComparisonTestCases + { + get + { + foreach (var enumValue1 in VerticalDirectionEnumValues) + { + foreach (var enumValue2 in VerticalDirectionEnumValues) + { + if (enumValue2 != enumValue1) + { + yield return new TestCaseData(enumValue1, enumValue2); + } + } + } + } + } + + [TestCaseSource(nameof(IdentityComparisonTestCases))] + public void CSharpEnumsAreSingletonsInPthonAndIdentityComparisonWorks(VerticalDirection enumValue1, VerticalDirection enumValue2) + { + var enumValue1Str = $"{nameof(EnumTests)}.{nameof(VerticalDirection)}.{enumValue1}"; + var enumValue2Str = $"{nameof(EnumTests)}.{nameof(VerticalDirection)}.{enumValue2}"; + + using var _ = Py.GIL(); + using var module = PyModule.FromString("CSharpEnumsAreSingletonsInPthonAndIdentityComparisonWorks", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +def are_same1(): + return {enumValue1Str} is {enumValue1Str} + +def are_same2(): + enum_value = {enumValue1Str} + return enum_value is {enumValue1Str} + +def are_same3(): + enum_value = {enumValue1Str} + return {enumValue1Str} is enum_value + +def are_same4(): + enum_value1 = {enumValue1Str} + enum_value2 = {enumValue1Str} + return enum_value1 is enum_value2 + +def are_not_same1(): + return {enumValue1Str} is not {enumValue2Str} + +def are_not_same2(): + enum_value = {enumValue1Str} + return enum_value is not {enumValue2Str} + +def are_not_same3(): + enum_value = {enumValue2Str} + return {enumValue1Str} is not enum_value + +def are_not_same4(): + enum_value1 = {enumValue1Str} + enum_value2 = {enumValue2Str} + return enum_value1 is not enum_value2 + + +"); + + Assert.IsTrue(module.InvokeMethod("are_same1").As()); + Assert.IsTrue(module.InvokeMethod("are_same2").As()); + Assert.IsTrue(module.InvokeMethod("are_same3").As()); + Assert.IsTrue(module.InvokeMethod("are_same4").As()); + + Assert.IsTrue(module.InvokeMethod("are_not_same1").As()); + Assert.IsTrue(module.InvokeMethod("are_not_same2").As()); + Assert.IsTrue(module.InvokeMethod("are_not_same3").As()); + Assert.IsTrue(module.InvokeMethod("are_not_same4").As()); + } + + [Test] + public void IdentityComparisonBetweenDifferentEnumTypesIsNeverTrue( + [ValueSource(nameof(VerticalDirectionEnumValues))] VerticalDirection enumValue1, + [ValueSource(nameof(HorizontalDirectionEnumValues))] HorizontalDirection enumValue2) + { + var enumValue1Str = $"{nameof(EnumTests)}.{nameof(VerticalDirection)}.{enumValue1}"; + var enumValue2Str = $"{nameof(EnumTests)}.{nameof(HorizontalDirection)}.{enumValue2}"; + + using var _ = Py.GIL(); + using var module = PyModule.FromString("IdentityComparisonBetweenDifferentEnumTypesIsNeverTrue", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +enum_value1 = {enumValue1Str} +enum_value2 = {enumValue2Str} + +def are_same1(): + return {enumValue1Str} is {enumValue2Str} + +def are_same2(): + return enum_value1 is {enumValue2Str} + +def are_same3(): + return {enumValue2Str} is enum_value1 + +def are_same4(): + return enum_value2 is {enumValue1Str} + +def are_same5(): + return {enumValue1Str} is enum_value2 + +def are_same6(): + return enum_value1 is enum_value2 + +def are_same7(): + return enum_value2 is enum_value1 +"); + + Assert.IsFalse(module.InvokeMethod("are_same1").As()); + Assert.IsFalse(module.InvokeMethod("are_same2").As()); + Assert.IsFalse(module.InvokeMethod("are_same3").As()); + Assert.IsFalse(module.InvokeMethod("are_same4").As()); + Assert.IsFalse(module.InvokeMethod("are_same5").As()); + Assert.IsFalse(module.InvokeMethod("are_same6").As()); + Assert.IsFalse(module.InvokeMethod("are_same7").As()); + } + + private PyModule GetCSharpObjectsComparisonTestModule(string @operator) + { + return PyModule.FromString("GetCSharpObjectsComparisonTestModule", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +enum_value = {nameof(EnumTests)}.{nameof(VerticalDirection)}.{VerticalDirection.Up} + +def compare_with_none1(): + return enum_value {@operator} None + +def compare_with_none2(): + return None {@operator} enum_value + +def compare_with_csharp_object1(csharp_object): + return enum_value {@operator} csharp_object + +def compare_with_csharp_object2(csharp_object): + return csharp_object {@operator} enum_value +"); + } + + [TestCase("==", false)] + [TestCase("!=", true)] + public void EqualityComparisonWithNull(string @operator, bool expectedResult) + { + using var _ = Py.GIL(); + using var module = GetCSharpObjectsComparisonTestModule(@operator); + + Assert.AreEqual(expectedResult, module.InvokeMethod("compare_with_none1").As()); + Assert.AreEqual(expectedResult, module.InvokeMethod("compare_with_none2").As()); + + using var pyNull = ((TestClass)null).ToPython(); + Assert.AreEqual(expectedResult, module.InvokeMethod("compare_with_csharp_object1", pyNull).As()); + Assert.AreEqual(expectedResult, module.InvokeMethod("compare_with_csharp_object2", pyNull).As()); + } + + [TestCase("==", false)] + [TestCase("!=", true)] + public void EqualityOperatorsWithNonEnumObjects(string @operator, bool expectedResult) + { + using var _ = Py.GIL(); + using var module = GetCSharpObjectsComparisonTestModule(@operator); + + using var pyCSharpObject = new TestClass().ToPython(); + Assert.AreEqual(expectedResult, module.InvokeMethod("compare_with_csharp_object1", pyCSharpObject).As()); + Assert.AreEqual(expectedResult, module.InvokeMethod("compare_with_csharp_object2", pyCSharpObject).As()); + } + + [Test] + public void ThrowsOnObjectComparisonOperators([Values("<", "<=", ">", ">=")] string @operator) + { + using var _ = Py.GIL(); + using var module = GetCSharpObjectsComparisonTestModule(@operator); + + using var pyCSharpObject = new TestClass().ToPython(); + Assert.Throws(() => module.InvokeMethod("compare_with_csharp_object1", pyCSharpObject)); + Assert.Throws(() => module.InvokeMethod("compare_with_csharp_object2", pyCSharpObject)); + } + + [Test] + public void ThrowsOnNullComparisonOperators([Values("<", "<=", ">", ">=")] string @operator) + { + using var _ = Py.GIL(); + using var module = GetCSharpObjectsComparisonTestModule(@operator); + + Assert.Throws(() => module.InvokeMethod("compare_with_none1").As()); + Assert.Throws(() => module.InvokeMethod("compare_with_none2").As()); + + using var pyNull = ((TestClass)null).ToPython(); + Assert.Throws(() => module.InvokeMethod("compare_with_csharp_object1", pyNull)); + Assert.Throws(() => module.InvokeMethod("compare_with_csharp_object2", pyNull)); + } + + public class TestClass + { + } + } +} diff --git a/src/embed_tests/TestMethodBinder.cs b/src/embed_tests/TestMethodBinder.cs index 7f4c58d7e..0b3f6497c 100644 --- a/src/embed_tests/TestMethodBinder.cs +++ b/src/embed_tests/TestMethodBinder.cs @@ -815,6 +815,20 @@ public string VariableArgumentsMethod(params PyObject[] paramsParams) return "VariableArgumentsMethod(PyObject[])"; } + // ---- + + public string MethodWithEnumParam(SomeEnu enumValue, string symbol) + { + return $"MethodWithEnumParam With Enum"; + } + + public string MethodWithEnumParam(PyObject pyObject, string symbol) + { + return $"MethodWithEnumParam With PyObject"; + } + + // ---- + public string ConstructorMessage { get; set; } public OverloadsTestClass(params CSharpModel[] paramsParams) @@ -1117,6 +1131,26 @@ def get_instance(): Assert.AreEqual("OverloadsTestClass(PyObject[])", instance.GetAttr("ConstructorMessage").As()); } + [Test] + public void EnumHasPrecedenceOverPyObject() + { + using var _ = Py.GIL(); + + var module = PyModule.FromString("EnumHasPrecedenceOverPyObject", @$" +from clr import AddReference +AddReference(""System"") +from Python.EmbeddingTest import * + +class PythonModel(TestMethodBinder.CSharpModel): + pass + +def call_method(): + return TestMethodBinder.OverloadsTestClass().MethodWithEnumParam(TestMethodBinder.SomeEnu.A, ""Some string"") +"); + + var result = module.GetAttr("call_method").Invoke(); + Assert.AreEqual("MethodWithEnumParam With Enum", result.As()); + } // Used to test that we match this function with Py DateTime & Date Objects public static int GetMonth(DateTime test) diff --git a/src/perf_tests/Python.PerformanceTests.csproj b/src/perf_tests/Python.PerformanceTests.csproj index ee239ff12..aa3a04adb 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/Converter.cs b/src/runtime/Converter.cs index 19fb1c883..fc6437bc1 100644 --- a/src/runtime/Converter.cs +++ b/src/runtime/Converter.cs @@ -18,6 +18,13 @@ namespace Python.Runtime [SuppressUnmanagedCodeSecurity] internal class Converter { + /// + /// We use a cache of the enum values references so that we treat them as singletons in Python. + /// We just try to mimic Python enums behavior, since Python enum values are singletons, + /// so the `is` identity comparison operator works for C# enums as well. + /// + + private static readonly Dictionary _enumCache = new(); private Converter() { } @@ -226,6 +233,16 @@ internal static NewReference ToPython(object? value, Type type) return resultlist.NewReferenceOrNull(); } + if (type.IsEnum) + { + if (!_enumCache.TryGetValue(value, out var cachedValue)) + { + _enumCache[value] = cachedValue = CLRObject.GetReference(value, type).MoveToPyObject(); + } + + return cachedValue.NewReferenceOrNull(); + } + // it the type is a python subclass of a managed type then return the // underlying python object rather than construct a new wrapper object. var pyderived = value as IPythonDerivedType; diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index 8c8bac65d..42fe0ba91 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -383,6 +383,17 @@ internal static int ArgPrecedence(Type t, bool isOperatorMethod) return 3000; } + if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + // Nullable is a special case, we treat it as the underlying type + return ArgPrecedence(Nullable.GetUnderlyingType(t), isOperatorMethod); + } + + if (t.IsEnum) + { + return -2; + } + if (t.IsAssignableFrom(typeof(PyObject)) && !isOperatorMethod) { return -1; diff --git a/src/runtime/Properties/AssemblyInfo.cs b/src/runtime/Properties/AssemblyInfo.cs index c3e7c304f..6941d1ac1 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.44")] -[assembly: AssemblyFileVersion("2.0.44")] +[assembly: AssemblyVersion("2.0.45")] +[assembly: AssemblyFileVersion("2.0.45")] diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index 9b870ed44..035bc6214 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.44 + 2.0.45 false LICENSE https://github.com/pythonnet/pythonnet diff --git a/src/runtime/Util/OpsHelper.cs b/src/runtime/Util/OpsHelper.cs index ab623f3de..89ce79e20 100644 --- a/src/runtime/Util/OpsHelper.cs +++ b/src/runtime/Util/OpsHelper.cs @@ -1,6 +1,7 @@ using System; using System.Linq.Expressions; using System.Reflection; +using System.Runtime.CompilerServices; using static Python.Runtime.OpsHelper; @@ -35,7 +36,7 @@ public static Expression EnumUnderlyingValue(Expression enumValue) } [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - internal class OpsAttribute: Attribute { } + internal class OpsAttribute : Attribute { } [Ops] internal static class FlagEnumOps where T : Enum @@ -78,12 +79,505 @@ static Func UnaryOp(Func op) [Ops] internal static class EnumOps where T : Enum { + private static bool IsUnsigned = typeof(T).GetEnumUnderlyingType() == typeof(UInt64); + [ForbidPythonThreads] #pragma warning disable IDE1006 // Naming Styles - must match Python public static PyInt __int__(T value) #pragma warning restore IDE1006 // Naming Styles - => typeof(T).GetEnumUnderlyingType() == typeof(UInt64) + => IsUnsigned ? new PyInt(Convert.ToUInt64(value)) : new PyInt(Convert.ToInt64(value)); + + #region Arithmetic operators + + public static double op_Addition(T a, double b) + { + if (IsUnsigned) + { + return Convert.ToUInt64(a) + b; + } + return Convert.ToInt64(a) + b; + } + + public static double op_Addition(double a, T b) + { + return op_Addition(b, a); + } + + public static double op_Subtraction(T a, double b) + { + if (IsUnsigned) + { + return Convert.ToUInt64(a) - b; + } + return Convert.ToInt64(a) - b; + } + + public static double op_Subtraction(double a, T b) + { + if (IsUnsigned) + { + return a - Convert.ToUInt64(b); + } + return a - Convert.ToInt64(b); + } + + public static double op_Multiply(T a, double b) + { + if (IsUnsigned) + { + return Convert.ToUInt64(a) * b; + } + return Convert.ToInt64(a) * b; + } + + public static double op_Multiply(double a, T b) + { + return op_Multiply(b, a); + } + + public static double op_Division(T a, double b) + { + if (IsUnsigned) + { + return Convert.ToUInt64(a) / b; + } + return Convert.ToInt64(a) / b; + } + + public static double op_Division(double a, T b) + { + if (IsUnsigned) + { + return a / Convert.ToUInt64(b); + } + return a / Convert.ToInt64(b); + } + + #endregion + + #region Int comparison operators + + public static bool op_Equality(T a, long b) + { + if (IsUnsigned) + { + var uvalue = Convert.ToUInt64(a); + return b >= 0 && ((ulong)b) == uvalue; + } + return Convert.ToInt64(a) == b; + } + + public static bool op_Equality(T a, ulong b) + { + if (IsUnsigned) + { + var uvalue = Convert.ToUInt64(a); + return b == uvalue; + } + var ivalue = Convert.ToInt64(a); + return ivalue >= 0 && ((ulong)ivalue) == b; + } + + public static bool op_Equality(long a, T b) + { + return op_Equality(b, a); + } + + public static bool op_Equality(ulong a, T b) + { + return op_Equality(b, a); + } + + public static bool op_Inequality(T a, long b) + { + return !op_Equality(a, b); + } + + public static bool op_Inequality(T a, ulong b) + { + return !op_Equality(a, b); + } + + public static bool op_Inequality(long a, T b) + { + return !op_Equality(b, a); + } + + public static bool op_Inequality(ulong a, T b) + { + return !op_Equality(b, a); + } + + public static bool op_LessThan(T a, long b) + { + if (IsUnsigned) + { + var uvalue = Convert.ToUInt64(a); + return b >= 0 && ((ulong)b) > uvalue; + } + return Convert.ToInt64(a) < b; + } + + public static bool op_LessThan(T a, ulong b) + { + if (IsUnsigned) + { + var uvalue = Convert.ToUInt64(a); + return b > uvalue; + } + var ivalue = Convert.ToInt64(a); + return ivalue >= 0 && ((ulong)ivalue) < b; + } + + public static bool op_LessThan(long a, T b) + { + return op_GreaterThan(b, a); + } + + public static bool op_LessThan(ulong a, T b) + { + return op_GreaterThan(b, a); + } + + public static bool op_GreaterThan(T a, long b) + { + if (IsUnsigned) + { + var uvalue = Convert.ToUInt64(a); + return b >= 0 && ((ulong)b) < uvalue; + } + return Convert.ToInt64(a) > b; + } + + public static bool op_GreaterThan(T a, ulong b) + { + if (IsUnsigned) + { + var uvalue = Convert.ToUInt64(a); + return b < uvalue; + } + var ivalue = Convert.ToInt64(a); + return ivalue >= 0 && ((ulong)ivalue) > b; + } + + public static bool op_GreaterThan(long a, T b) + { + return op_LessThan(b, a); + } + + public static bool op_GreaterThan(ulong a, T b) + { + return op_LessThan(b, a); + } + + public static bool op_LessThanOrEqual(T a, long b) + { + if (IsUnsigned) + { + var uvalue = Convert.ToUInt64(a); + return b >= 0 && ((ulong)b) >= uvalue; + } + return Convert.ToInt64(a) <= b; + } + + public static bool op_LessThanOrEqual(T a, ulong b) + { + if (IsUnsigned) + { + var uvalue = Convert.ToUInt64(a); + return b >= uvalue; + } + var ivalue = Convert.ToInt64(a); + return ivalue >= 0 && ((ulong)ivalue) <= b; + } + + public static bool op_LessThanOrEqual(long a, T b) + { + return op_GreaterThanOrEqual(b, a); + } + + public static bool op_LessThanOrEqual(ulong a, T b) + { + return op_GreaterThanOrEqual(b, a); + } + + public static bool op_GreaterThanOrEqual(T a, long b) + { + if (IsUnsigned) + { + var uvalue = Convert.ToUInt64(a); + return b >= 0 && ((ulong)b) <= uvalue; + } + return Convert.ToInt64(a) >= b; + } + + public static bool op_GreaterThanOrEqual(T a, ulong b) + { + if (IsUnsigned) + { + var uvalue = Convert.ToUInt64(a); + return b <= uvalue; + } + var ivalue = Convert.ToInt64(a); + return ivalue >= 0 && ((ulong)ivalue) >= b; + } + + public static bool op_GreaterThanOrEqual(long a, T b) + { + return op_LessThanOrEqual(b, a); + } + + public static bool op_GreaterThanOrEqual(ulong a, T b) + { + return op_LessThanOrEqual(b, a); + } + + #endregion + + #region Double comparison operators + + public static bool op_Equality(T a, double b) + { + if (IsUnsigned) + { + return Convert.ToUInt64(a) == b; + } + return Convert.ToInt64(a) == b; + } + + public static bool op_Equality(double a, T b) + { + return op_Equality(b, a); + } + + public static bool op_Inequality(T a, double b) + { + return !op_Equality(a, b); + } + + public static bool op_Inequality(double a, T b) + { + return !op_Equality(b, a); + } + + public static bool op_LessThan(T a, double b) + { + if (IsUnsigned) + { + return Convert.ToUInt64(a) < b; + } + return Convert.ToInt64(a) < b; + } + + public static bool op_LessThan(double a, T b) + { + return op_GreaterThan(b, a); + } + + public static bool op_GreaterThan(T a, double b) + { + if (IsUnsigned) + { + return Convert.ToUInt64(a) > b; + } + return Convert.ToInt64(a) > b; + } + + public static bool op_GreaterThan(double a, T b) + { + return op_LessThan(b, a); + } + + public static bool op_LessThanOrEqual(T a, double b) + { + if (IsUnsigned) + { + return Convert.ToUInt64(a) <= b; + } + return Convert.ToInt64(a) <= b; + } + + public static bool op_LessThanOrEqual(double a, T b) + { + return op_GreaterThanOrEqual(b, a); + } + + public static bool op_GreaterThanOrEqual(T a, double b) + { + if (IsUnsigned) + { + return Convert.ToUInt64(a) >= b; + } + return Convert.ToInt64(a) >= b; + } + + public static bool op_GreaterThanOrEqual(double a, T b) + { + return op_LessThanOrEqual(b, a); + } + + #endregion + + #region String comparison operators + public static bool op_Equality(T a, string b) + { + return a.ToString().Equals(b, StringComparison.InvariantCultureIgnoreCase); + } + public static bool op_Equality(string a, T b) + { + return op_Equality(b, a); + } + + public static bool op_Inequality(T a, string b) + { + return !op_Equality(a, b); + } + + public static bool op_Inequality(string a, T b) + { + return !op_Equality(b, a); + } + + #endregion + + #region Enum comparison operators + + public static bool op_Equality(T a, Enum b) + { + if (b == null) + { + return false; + } + + if (b.GetType().GetEnumUnderlyingType() == typeof(UInt64)) + { + return op_Equality(a, Convert.ToUInt64(b)); + } + return op_Equality(a, Convert.ToInt64(b)); + } + + public static bool op_Equality(Enum a, T b) + { + return op_Equality(b, a); + } + + public static bool op_Inequality(T a, Enum b) + { + return !op_Equality(a, b); + } + + public static bool op_Inequality(Enum a, T b) + { + return !op_Equality(b, a); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ThrowOnNull(object obj, string @operator) + { + if (obj == null) + { + using (Py.GIL()) + { + Exceptions.RaiseTypeError($"'{@operator}' not supported between instances of '{typeof(T).Name}' and null/None"); + PythonException.ThrowLastAsClrException(); + } + } + } + + public static bool op_LessThan(T a, Enum b) + { + ThrowOnNull(b, "<"); + + if (b.GetType().GetEnumUnderlyingType() == typeof(UInt64)) + { + return op_LessThan(a, Convert.ToUInt64(b)); + } + return op_LessThan(a, Convert.ToInt64(b)); + } + + public static bool op_LessThan(Enum a, T b) + { + ThrowOnNull(a, "<"); + return op_GreaterThan(b, a); + } + + public static bool op_GreaterThan(T a, Enum b) + { + ThrowOnNull(b, ">"); + + if (b.GetType().GetEnumUnderlyingType() == typeof(UInt64)) + { + return op_GreaterThan(a, Convert.ToUInt64(b)); + } + return op_GreaterThan(a, Convert.ToInt64(b)); + } + + public static bool op_GreaterThan(Enum a, T b) + { + ThrowOnNull(a, ">"); + return op_LessThan(b, a); + } + + public static bool op_LessThanOrEqual(T a, Enum b) + { + ThrowOnNull(b, "<="); + + if (b.GetType().GetEnumUnderlyingType() == typeof(UInt64)) + { + return op_LessThanOrEqual(a, Convert.ToUInt64(b)); + } + return op_LessThanOrEqual(a, Convert.ToInt64(b)); + } + + public static bool op_LessThanOrEqual(Enum a, T b) + { + ThrowOnNull(a, "<="); + return op_GreaterThanOrEqual(b, a); + } + + public static bool op_GreaterThanOrEqual(T a, Enum b) + { + ThrowOnNull(b, ">="); + + if (b.GetType().GetEnumUnderlyingType() == typeof(UInt64)) + { + return op_GreaterThanOrEqual(a, Convert.ToUInt64(b)); + } + return op_GreaterThanOrEqual(a, Convert.ToInt64(b)); + } + + public static bool op_GreaterThanOrEqual(Enum a, T b) + { + ThrowOnNull(a, ">="); + return op_LessThanOrEqual(b, a); + } + + #endregion + + #region Object equality operators + + public static bool op_Equality(T a, object b) + { + return false; + } + + public static bool op_Equality(object a, T b) + { + return false; + } + + public static bool op_Inequality(T a, object b) + { + return true; + } + + public static bool op_Inequality(object a, T b) + { + return true; + } + + #endregion } } From 1d09c9847c4d3d53d858d8ad62357596d79310eb Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 6 Aug 2025 17:33:59 -0400 Subject: [PATCH 17/24] Python functions conversion to managed delegates (#104) * Convert Python functions to managed delegates * Bump version to 2.0.46 * Support managed delegates wrapped in PyObjects Add more unit tests * Cleanup * Fix enums precedence in MethodBinder --- src/embed_tests/TestMethodBinder.cs | 350 +++++++++++++++++- src/perf_tests/Python.PerformanceTests.csproj | 4 +- src/runtime/Converter.cs | 72 +++- src/runtime/MethodBinder.cs | 20 +- src/runtime/Properties/AssemblyInfo.cs | 4 +- src/runtime/Python.Runtime.csproj | 2 +- src/runtime/PythonTypes/PyObject.cs | 57 ++- 7 files changed, 493 insertions(+), 16 deletions(-) diff --git a/src/embed_tests/TestMethodBinder.cs b/src/embed_tests/TestMethodBinder.cs index 0b3f6497c..2e20870f3 100644 --- a/src/embed_tests/TestMethodBinder.cs +++ b/src/embed_tests/TestMethodBinder.cs @@ -67,7 +67,7 @@ def TestEnumerable(self): public static dynamic Numpy; [OneTimeSetUp] - public void SetUp() + public void OneTimeSetUp() { PythonEngine.Initialize(); using var _ = Py.GIL(); @@ -89,6 +89,15 @@ public void Dispose() PythonEngine.Shutdown(); } + [SetUp] + public void SetUp() + { + CSharpModel.LastDelegateCalled = null; + CSharpModel.LastFuncCalled = null; + CSharpModel.MethodCalled = null; + CSharpModel.ProvidedArgument = null; + } + [Test] public void MethodCalledList() { @@ -1152,6 +1161,247 @@ def call_method(): Assert.AreEqual("MethodWithEnumParam With Enum", result.As()); } + [TestCase("call_method_with_func1", "MethodWithFunc1", "func1")] + [TestCase("call_method_with_func2", "MethodWithFunc2", "func2")] + [TestCase("call_method_with_func3", "MethodWithFunc3", "func3")] + [TestCase("call_method_with_func1_lambda", "MethodWithFunc1", "func1")] + [TestCase("call_method_with_func2_lambda", "MethodWithFunc2", "func2")] + [TestCase("call_method_with_func3_lambda", "MethodWithFunc3", "func3")] + public void BindsPythonToCSharpFuncDelegates(string pythonFuncToCall, string expectedCSharpMethodCalled, string expectedPythonFuncCalled) + { + using var _ = Py.GIL(); + + var module = PyModule.FromString("BindsPythonToCSharpFuncDelegates", @$" +from clr import AddReference +AddReference(""System"") +from Python.EmbeddingTest import * + +from System import Func + +class PythonModel: + last_delegate_called = None + +def func1(): + PythonModel.last_delegate_called = 'func1' + return TestMethodBinder.CSharpModel(); + +def func2(model): + if model is None or not isinstance(model, TestMethodBinder.CSharpModel): + raise TypeError(""model must be of type CSharpModel"") + PythonModel.last_delegate_called = 'func2' + return model + +def func3(model1, model2): + if model1 is None or model2 is None or not isinstance(model1, TestMethodBinder.CSharpModel) or not isinstance(model2, TestMethodBinder.CSharpModel): + raise TypeError(""model1 and model2 must be of type CSharpModel"") + PythonModel.last_delegate_called = 'func3' + return model1 + +def call_method_with_func1(): + return TestMethodBinder.CSharpModel.MethodWithFunc1(func1) + +def call_method_with_func2(): + return TestMethodBinder.CSharpModel.MethodWithFunc2(func2) + +def call_method_with_func3(): + return TestMethodBinder.CSharpModel.MethodWithFunc3(func3) + +def call_method_with_func1_lambda(): + return TestMethodBinder.CSharpModel.MethodWithFunc1(lambda: func1()) + +def call_method_with_func2_lambda(): + return TestMethodBinder.CSharpModel.MethodWithFunc2(lambda model: func2(model)) + +def call_method_with_func3_lambda(): + return TestMethodBinder.CSharpModel.MethodWithFunc3(lambda model1, model2: func3(model1, model2)) +"); + + CSharpModel managedResult = null; + Assert.DoesNotThrow(() => + { + using var result = module.GetAttr(pythonFuncToCall).Invoke(); + managedResult = result.As(); + }); + + Assert.IsNotNull(managedResult); + Assert.AreEqual(expectedCSharpMethodCalled, CSharpModel.LastDelegateCalled); + + using var pythonModel = module.GetAttr("PythonModel"); + using var lastDelegateCalled = pythonModel.GetAttr("last_delegate_called"); + Assert.AreEqual(expectedPythonFuncCalled, lastDelegateCalled.As()); + } + + [TestCase("call_method_with_action1", "MethodWithAction1", "action1")] + [TestCase("call_method_with_action2", "MethodWithAction2", "action2")] + [TestCase("call_method_with_action3", "MethodWithAction3", "action3")] + [TestCase("call_method_with_action1_lambda", "MethodWithAction1", "action1")] + [TestCase("call_method_with_action2_lambda", "MethodWithAction2", "action2")] + [TestCase("call_method_with_action3_lambda", "MethodWithAction3", "action3")] + public void BindsPythonToCSharpActionDelegates(string pythonFuncToCall, string expectedCSharpMethodCalled, string expectedPythonFuncCalled) + { + using var _ = Py.GIL(); + + var module = PyModule.FromString("BindsPythonToCSharpActionDelegates", @$" +from clr import AddReference +AddReference(""System"") +from Python.EmbeddingTest import * + +from System import Func + +class PythonModel: + last_delegate_called = None + +def action1(): + PythonModel.last_delegate_called = 'action1' + pass + +def action2(model): + if model is None or not isinstance(model, TestMethodBinder.CSharpModel): + raise TypeError(""model must be of type CSharpModel"") + PythonModel.last_delegate_called = 'action2' + pass + +def action3(model1, model2): + if model1 is None or model2 is None or not isinstance(model1, TestMethodBinder.CSharpModel) or not isinstance(model2, TestMethodBinder.CSharpModel): + raise TypeError(""model1 and model2 must be of type CSharpModel"") + PythonModel.last_delegate_called = 'action3' + pass + +def call_method_with_action1(): + return TestMethodBinder.CSharpModel.MethodWithAction1(action1) + +def call_method_with_action2(): + return TestMethodBinder.CSharpModel.MethodWithAction2(action2) + +def call_method_with_action3(): + return TestMethodBinder.CSharpModel.MethodWithAction3(action3) + +def call_method_with_action1_lambda(): + return TestMethodBinder.CSharpModel.MethodWithAction1(lambda: action1()) + +def call_method_with_action2_lambda(): + return TestMethodBinder.CSharpModel.MethodWithAction2(lambda model: action2(model)) + +def call_method_with_action3_lambda(): + return TestMethodBinder.CSharpModel.MethodWithAction3(lambda model1, model2: action3(model1, model2)) +"); + + Assert.DoesNotThrow(() => + { + using var result = module.GetAttr(pythonFuncToCall).Invoke(); + }); + + Assert.AreEqual(expectedCSharpMethodCalled, CSharpModel.LastDelegateCalled); + + using var pythonModel = module.GetAttr("PythonModel"); + using var lastDelegateCalled = pythonModel.GetAttr("last_delegate_called"); + Assert.AreEqual(expectedPythonFuncCalled, lastDelegateCalled.As()); + } + + [TestCase("call_method_with_func1", "MethodWithFunc1", "TestFunc1")] + [TestCase("call_method_with_func2", "MethodWithFunc2", "TestFunc2")] + [TestCase("call_method_with_func3", "MethodWithFunc3", "TestFunc3")] + public void BindsCSharpFuncFromPythonToCSharpFuncDelegates(string pythonFuncToCall, string expectedMethodCalled, string expectedInnerMethodCalled) + { + using var _ = Py.GIL(); + + var module = PyModule.FromString("BindsCSharpFuncFromPythonToCSharpFuncDelegates", @$" +from clr import AddReference +AddReference(""System"") +from Python.EmbeddingTest import * + +def call_method_with_func1(): + return TestMethodBinder.CSharpModel.MethodWithFunc1(TestMethodBinder.CSharpModel.TestFunc1) + +def call_method_with_func2(): + return TestMethodBinder.CSharpModel.MethodWithFunc2(TestMethodBinder.CSharpModel.TestFunc2) + +def call_method_with_func3(): + return TestMethodBinder.CSharpModel.MethodWithFunc3(TestMethodBinder.CSharpModel.TestFunc3) +"); + + CSharpModel managedResult = null; + Assert.DoesNotThrow(() => + { + using var result = module.GetAttr(pythonFuncToCall).Invoke(); + managedResult = result.As(); + }); + Assert.IsNotNull(managedResult); + Assert.AreEqual(expectedMethodCalled, CSharpModel.LastDelegateCalled); + Assert.AreEqual(expectedInnerMethodCalled, CSharpModel.LastFuncCalled); + } + + [TestCase("call_method_with_action1", "MethodWithAction1", "TestAction1")] + [TestCase("call_method_with_action2", "MethodWithAction2", "TestAction2")] + [TestCase("call_method_with_action3", "MethodWithAction3", "TestAction3")] + public void BindsCSharpActionFromPythonToCSharpActionDelegates(string pythonFuncToCall, string expectedMethodCalled, string expectedInnerMethodCalled) + { + using var _ = Py.GIL(); + + var module = PyModule.FromString("BindsCSharpActionFromPythonToCSharpActionDelegates", @$" +from clr import AddReference +AddReference(""System"") +from Python.EmbeddingTest import * + +def call_method_with_action1(): + return TestMethodBinder.CSharpModel.MethodWithAction1(TestMethodBinder.CSharpModel.TestAction1) + +def call_method_with_action2(): + return TestMethodBinder.CSharpModel.MethodWithAction2(TestMethodBinder.CSharpModel.TestAction2) + +def call_method_with_action3(): + return TestMethodBinder.CSharpModel.MethodWithAction3(TestMethodBinder.CSharpModel.TestAction3) +"); + + Assert.DoesNotThrow(() => + { + using var result = module.GetAttr(pythonFuncToCall).Invoke(); + }); + Assert.AreEqual(expectedMethodCalled, CSharpModel.LastDelegateCalled); + Assert.AreEqual(expectedInnerMethodCalled, CSharpModel.LastFuncCalled); + } + + [Test] + public void NumericArgumentsTakePrecedenceOverEnums() + { + using var _ = Py.GIL(); + + var module = PyModule.FromString("NumericArgumentsTakePrecedenceOverEnums", @$" +from clr import AddReference +AddReference(""System"") +from Python.EmbeddingTest import * +from System import DayOfWeek + +def call_method_with_int(): + TestMethodBinder.CSharpModel().NumericalArgumentMethod(1) + +def call_method_with_float(): + TestMethodBinder.CSharpModel().NumericalArgumentMethod(0.1) + +def call_method_with_numpy_float(): + TestMethodBinder.CSharpModel().NumericalArgumentMethod(TestMethodBinder.Numpy.float64(0.1)) + +def call_method_with_enum(): + TestMethodBinder.CSharpModel().NumericalArgumentMethod(DayOfWeek.MONDAY) +"); + + module.GetAttr("call_method_with_int").Invoke(); + Assert.AreEqual(typeof(int), CSharpModel.ProvidedArgument.GetType()); + Assert.AreEqual(1, CSharpModel.ProvidedArgument); + + module.GetAttr("call_method_with_float").Invoke(); + Assert.AreEqual(typeof(double), CSharpModel.ProvidedArgument.GetType()); + Assert.AreEqual(0.1d, CSharpModel.ProvidedArgument); + + module.GetAttr("call_method_with_numpy_float").Invoke(); + Assert.AreEqual(typeof(decimal), CSharpModel.ProvidedArgument.GetType()); + Assert.AreEqual(0.1m, CSharpModel.ProvidedArgument); + + module.GetAttr("call_method_with_enum").Invoke(); + Assert.AreEqual(typeof(DayOfWeek), CSharpModel.ProvidedArgument.GetType()); + Assert.AreEqual(DayOfWeek.Monday, CSharpModel.ProvidedArgument); + } + // Used to test that we match this function with Py DateTime & Date Objects public static int GetMonth(DateTime test) { @@ -1234,6 +1484,10 @@ public void NumericalArgumentMethod(decimal value) { ProvidedArgument = value; } + public void NumericalArgumentMethod(DayOfWeek value) + { + ProvidedArgument = value; + } public void EnumerableKeyValuePair(IEnumerable> value) { ProvidedArgument = value; @@ -1288,6 +1542,100 @@ public static void MethodDateTimeAndTimeSpan(CSharpModel pepe, Func func) + { + AssertErrorNotOccurred(); + LastDelegateCalled = "MethodWithFunc1"; + return func(); + } + + public static CSharpModel MethodWithFunc2(Func func) + { + AssertErrorNotOccurred(); + LastDelegateCalled = "MethodWithFunc2"; + return func(new CSharpModel()); + } + + public static CSharpModel MethodWithFunc3(Func func) + { + AssertErrorNotOccurred(); + LastDelegateCalled = "MethodWithFunc3"; + return func(new CSharpModel(), new CSharpModel()); + } + + public static void MethodWithAction1(Action action) + { + AssertErrorNotOccurred(); + LastDelegateCalled = "MethodWithAction1"; + action(); + } + + public static void MethodWithAction2(Action action) + { + AssertErrorNotOccurred(); + LastDelegateCalled = "MethodWithAction2"; + action(new CSharpModel()); + } + + public static void MethodWithAction3(Action action) + { + AssertErrorNotOccurred(); + LastDelegateCalled = "MethodWithAction3"; + action(new CSharpModel(), new CSharpModel()); + } + + public static CSharpModel TestFunc1() + { + LastFuncCalled = "TestFunc1"; + return new CSharpModel(); + } + + public static CSharpModel TestFunc2(CSharpModel model) + { + if (model == null) + { + throw new ArgumentNullException(nameof(model)); + } + LastFuncCalled = "TestFunc2"; + return model; + } + + public static CSharpModel TestFunc3(CSharpModel model1, CSharpModel model2) + { + if (model1 == null || model2 == null) + { + throw new ArgumentNullException(model1 == null ? nameof(model1) : nameof(model2)); + } + LastFuncCalled = "TestFunc3"; + return model1; + } + + public static void TestAction1() + { + LastFuncCalled = "TestAction1"; + } + + public static void TestAction2(CSharpModel model) + { + if (model == null) + { + throw new ArgumentNullException(nameof(model)); + } + LastFuncCalled = "TestAction2"; + } + + public static void TestAction3(CSharpModel model1, CSharpModel model2) + { + if (model1 == null || model2 == null) + { + throw new ArgumentNullException(model1 == null ? nameof(model1) : nameof(model2)); + } + LastFuncCalled = "TestAction3"; + } } public class TestImplicitConversion diff --git a/src/perf_tests/Python.PerformanceTests.csproj b/src/perf_tests/Python.PerformanceTests.csproj index aa3a04adb..4ee604bf2 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/Converter.cs b/src/runtime/Converter.cs index fc6437bc1..be5501828 100644 --- a/src/runtime/Converter.cs +++ b/src/runtime/Converter.cs @@ -9,6 +9,7 @@ using System.Text; using Python.Runtime.Native; +using System.Linq; namespace Python.Runtime { @@ -505,8 +506,11 @@ internal static bool ToManagedValue(BorrowedReference value, Type obType, result = cb.type.Value; return true; } - // shouldn't happen - return false; + // Method bindings will be handled below along with actual Python callables + if (mt is not MethodBinding) + { + return false; + } } if (value == Runtime.PyNone && !obType.IsValueType) @@ -545,6 +549,11 @@ internal static bool ToManagedValue(BorrowedReference value, Type obType, return ToEnum(value, obType, out result, setError, out usedImplicit); } + if (TryConvertToDelegate(value, obType, out result)) + { + return true; + } + // Conversion to 'Object' is done based on some reasonable default // conversions (Python string -> managed string, Python int -> Int32 etc.). if (obType == objectType) @@ -722,6 +731,65 @@ internal static bool ToManagedExplicit(BorrowedReference value, Type obType, return ToPrimitive(explicitlyCoerced.Borrow(), obType, out result, false, out var _); } + /// + /// Tries to convert the given Python object into a managed delegate + /// + /// Python object to be converted + /// The wanted delegate type + /// Managed delegate + /// True if successful conversion + internal static bool TryConvertToDelegate(BorrowedReference pyValue, Type delegateType, out object result) + { + result = null; + + if (!typeof(MulticastDelegate).IsAssignableFrom(delegateType) || Runtime.PyCallable_Check(pyValue) == 0) + { + return false; + } + + if (pyValue.IsNull) + { + return true; + } + + var code = string.Empty; + var types = delegateType.GetGenericArguments(); + + using var locals = new PyDict(); + try + { + using var pyCallable = new PyObject(pyValue); + locals.SetItem("pyCallable", pyCallable); + + if (types.Length > 0) + { + code = string.Join(',', types.Select((type, i) => + { + var t = $"t{i}"; + locals.SetItem(t, type.ToPython()); + return t; + })); + var name = delegateType.Name.Substring(0, delegateType.Name.IndexOf('`')); + code = $"from System import {name}; delegate = {name}[{code}](pyCallable)"; + } + else + { + var name = delegateType.Name; + code = $"from System import {name}; delegate = {name}(pyCallable)"; + } + + PythonEngine.Exec(code, null, locals); + result = locals.GetItem("delegate").AsManagedObject(delegateType); + + return true; + } + catch + { + } + + return false; + } + /// Determine if the comparing class is a subclass of a generic type private static bool IsSubclassOfRawGeneric(Type generic, Type comparingClass) { diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index 42fe0ba91..d567ced0c 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -389,14 +389,24 @@ internal static int ArgPrecedence(Type t, bool isOperatorMethod) return ArgPrecedence(Nullable.GetUnderlyingType(t), isOperatorMethod); } + // Enums precedence is higher tan PyObject but lower than numbers. + // PyObject precedence is higher and objects. + // Strings precedence is higher than objects. + // So we have: + // - String: 50 + // - Object: 40 + // - PyObject: 39 + // - Enum: 38 + // - Numbers: 2 -> 29 + if (t.IsEnum) { - return -2; + return 38; } if (t.IsAssignableFrom(typeof(PyObject)) && !isOperatorMethod) { - return -1; + return 39; } if (t.IsArray) @@ -414,7 +424,7 @@ internal static int ArgPrecedence(Type t, bool isOperatorMethod) switch (tc) { case TypeCode.Object: - return 1; + return 40; // we place higher precision methods at the top case TypeCode.Decimal: @@ -444,10 +454,10 @@ internal static int ArgPrecedence(Type t, bool isOperatorMethod) return 29; case TypeCode.String: - return 30; + return 50; case TypeCode.Boolean: - return 40; + return 60; } return 2000; diff --git a/src/runtime/Properties/AssemblyInfo.cs b/src/runtime/Properties/AssemblyInfo.cs index 6941d1ac1..a7c2c1a3d 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.45")] -[assembly: AssemblyFileVersion("2.0.45")] +[assembly: AssemblyVersion("2.0.46")] +[assembly: AssemblyFileVersion("2.0.46")] diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index 035bc6214..558466d26 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.45 + 2.0.46 false LICENSE https://github.com/pythonnet/pythonnet diff --git a/src/runtime/PythonTypes/PyObject.cs b/src/runtime/PythonTypes/PyObject.cs index e0a17bed5..96472ce25 100644 --- a/src/runtime/PythonTypes/PyObject.cs +++ b/src/runtime/PythonTypes/PyObject.cs @@ -25,7 +25,7 @@ public partial class PyObject : DynamicObject, IDisposable, ISerializable /// Trace stack for PyObject's construction /// public StackTrace Traceback { get; } = new StackTrace(1); -#endif +#endif protected internal IntPtr rawPtr = IntPtr.Zero; internal readonly int run = Runtime.GetRun(); @@ -163,7 +163,7 @@ public static PyObject FromManagedObject(object ob) /// public object? AsManagedObject(Type t) { - if (!Converter.ToManaged(obj, t, out var result, true)) + if (!TryAsManagedObject(t, out var result)) { throw new InvalidCastException("cannot convert object to target type", PythonException.FetchCurrentOrNull(out _)); @@ -177,6 +177,57 @@ public static PyObject FromManagedObject(object ob) /// public T As() => (T)this.AsManagedObject(typeof(T))!; + /// + /// Tries to convert the Python object to a managed object of the specified type. + /// + public bool TryAsManagedObject(Type t, out object? result) + { + return Converter.ToManaged(obj, t, out result, true); + } + + /// + /// Tries to convert the Python object to a managed object of the specified type. + /// + public bool TryAs(out T result) + { + if (TryAsManagedObject(typeof(T), out var obj)) + { + if (obj is T t) + { + result = t; + return true; + } + } + + result = default!; + return false; + } + + /// + /// Return a managed object of the given type, based on the + /// value of the Python object. + /// + /// + /// This method will act in a safe way by acquiring the GIL. + /// + public T SafeAs() + { + using var _ = Py.GIL(); + return As(); + } + + /// + /// Tries to convert the Python object to a managed object of the specified type. + /// + /// + /// This method will act in a safe way by acquiring the GIL. + /// + public bool TrySafeAs(out T result) + { + using var _ = Py.GIL(); + return TryAs(out result); + } + internal bool IsDisposed => rawPtr == IntPtr.Zero; void CheckDisposed() @@ -235,7 +286,7 @@ public void Dispose() { GC.SuppressFinalize(this); Dispose(true); - + } internal StolenReference Steal() From 1aa5b8b6b2664849ddc0cd1dec58e7f2ef8899d9 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 14 Aug 2025 11:14:12 -0400 Subject: [PATCH 18/24] Refactor enums comparison operators for performance improvements (#105) * Refactor enums comparison operators for performance improvements * Minor change * Minor change * Minor improvements * Minor change * Cleanup * Update version to 2.0.47 --- src/perf_tests/Python.PerformanceTests.csproj | 4 +- src/runtime/ClassManager.cs | 5 + src/runtime/Properties/AssemblyInfo.cs | 4 +- src/runtime/Python.Runtime.csproj | 2 +- src/runtime/Types/ClassBase.cs | 85 ++-- src/runtime/Types/DelegateObject.cs | 2 +- src/runtime/Types/EnumObject.cs | 218 +++++++++ src/runtime/Util/OpsHelper.cs | 423 ------------------ 8 files changed, 277 insertions(+), 466 deletions(-) create mode 100644 src/runtime/Types/EnumObject.cs diff --git a/src/perf_tests/Python.PerformanceTests.csproj b/src/perf_tests/Python.PerformanceTests.csproj index 4ee604bf2..8ea99d9b2 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/ClassManager.cs b/src/runtime/ClassManager.cs index bf852112c..b88a6a6b6 100644 --- a/src/runtime/ClassManager.cs +++ b/src/runtime/ClassManager.cs @@ -220,6 +220,11 @@ internal static ClassBase CreateClass(Type type) impl = new LookUpObject(type); } + else if (type.IsEnum) + { + impl = new EnumObject(type); + } + else { impl = new ClassObject(type); diff --git a/src/runtime/Properties/AssemblyInfo.cs b/src/runtime/Properties/AssemblyInfo.cs index a7c2c1a3d..4bada6682 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.46")] -[assembly: AssemblyFileVersion("2.0.46")] +[assembly: AssemblyVersion("2.0.47")] +[assembly: AssemblyFileVersion("2.0.47")] diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index 558466d26..b60b36e6b 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.46 + 2.0.47 false LICENSE https://github.com/pythonnet/pythonnet diff --git a/src/runtime/Types/ClassBase.cs b/src/runtime/Types/ClassBase.cs index ded315952..590c870b5 100644 --- a/src/runtime/Types/ClassBase.cs +++ b/src/runtime/Types/ClassBase.cs @@ -156,42 +156,7 @@ public static NewReference tp_richcompare(BorrowedReference ob, BorrowedReferenc try { int cmp = co1Comp.CompareTo(co2Inst); - - BorrowedReference pyCmp; - if (cmp < 0) - { - if (op == Runtime.Py_LT || op == Runtime.Py_LE) - { - pyCmp = Runtime.PyTrue; - } - else - { - pyCmp = Runtime.PyFalse; - } - } - else if (cmp == 0) - { - if (op == Runtime.Py_LE || op == Runtime.Py_GE) - { - pyCmp = Runtime.PyTrue; - } - else - { - pyCmp = Runtime.PyFalse; - } - } - else - { - if (op == Runtime.Py_GE || op == Runtime.Py_GT) - { - pyCmp = Runtime.PyTrue; - } - else - { - pyCmp = Runtime.PyFalse; - } - } - return new NewReference(pyCmp); + return new NewReference(GetComparisonResult(op, cmp)); } catch (ArgumentException e) { @@ -202,7 +167,53 @@ public static NewReference tp_richcompare(BorrowedReference ob, BorrowedReferenc } } - private static bool TryGetSecondCompareOperandInstance(BorrowedReference left, BorrowedReference right, out CLRObject co1, out object co2Inst) + /// + /// Get the result of a comparison operation based on the operator and the comparison result. + /// + /// + /// This method is used to determine the result of a comparison operation, excluding equality and inequality. + /// + protected static BorrowedReference GetComparisonResult(int op, int comparisonResult) + { + BorrowedReference pyCmp; + if (comparisonResult < 0) + { + if (op == Runtime.Py_LT || op == Runtime.Py_LE) + { + pyCmp = Runtime.PyTrue; + } + else + { + pyCmp = Runtime.PyFalse; + } + } + else if (comparisonResult == 0) + { + if (op == Runtime.Py_LE || op == Runtime.Py_GE) + { + pyCmp = Runtime.PyTrue; + } + else + { + pyCmp = Runtime.PyFalse; + } + } + else + { + if (op == Runtime.Py_GE || op == Runtime.Py_GT) + { + pyCmp = Runtime.PyTrue; + } + else + { + pyCmp = Runtime.PyFalse; + } + } + + return pyCmp; + } + + protected static bool TryGetSecondCompareOperandInstance(BorrowedReference left, BorrowedReference right, out CLRObject co1, out object co2Inst) { co2Inst = null; diff --git a/src/runtime/Types/DelegateObject.cs b/src/runtime/Types/DelegateObject.cs index 43a75aba7..a469e6a52 100644 --- a/src/runtime/Types/DelegateObject.cs +++ b/src/runtime/Types/DelegateObject.cs @@ -103,7 +103,7 @@ public static NewReference tp_call(BorrowedReference ob, BorrowedReference args, /// /// Implements __cmp__ for reflected delegate types. /// - public new static NewReference tp_richcompare(BorrowedReference ob, BorrowedReference other, int op) + public static NewReference tp_richcompare(BorrowedReference ob, BorrowedReference other, int op) { if (op != Runtime.Py_EQ && op != Runtime.Py_NE) { diff --git a/src/runtime/Types/EnumObject.cs b/src/runtime/Types/EnumObject.cs new file mode 100644 index 000000000..8c146ff50 --- /dev/null +++ b/src/runtime/Types/EnumObject.cs @@ -0,0 +1,218 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Python.Runtime +{ + /// + /// Managed class that provides the implementation for reflected enum types. + /// + [Serializable] + internal class EnumObject : ClassBase + { + internal EnumObject(Type type) : base(type) + { + } + + /// + /// Standard comparison implementation for instances of enum types. + /// + public static NewReference tp_richcompare(BorrowedReference ob, BorrowedReference other, int op) + { + object rightInstance; + CLRObject leftClrObject; + int comparisonResult; + + switch (op) + { + case Runtime.Py_EQ: + case Runtime.Py_NE: + var pytrue = Runtime.PyTrue; + var pyfalse = Runtime.PyFalse; + + // swap true and false for NE + if (op != Runtime.Py_EQ) + { + pytrue = Runtime.PyFalse; + pyfalse = Runtime.PyTrue; + } + + if (ob == other) + { + return new NewReference(pytrue); + } + + if (!TryGetSecondCompareOperandInstance(ob, other, out leftClrObject, out rightInstance)) + { + return new NewReference(pyfalse); + } + + if (rightInstance != null && + TryCompare(leftClrObject.inst as Enum, rightInstance, out comparisonResult) && + comparisonResult == 0) + { + return new NewReference(pytrue); + } + else + { + return new NewReference(pyfalse); + } + + case Runtime.Py_LT: + case Runtime.Py_LE: + case Runtime.Py_GT: + case Runtime.Py_GE: + if (!TryGetSecondCompareOperandInstance(ob, other, out leftClrObject, out rightInstance)) + { + return Exceptions.RaiseTypeError("Cannot get managed object"); + } + + if (rightInstance == null) + { + return Exceptions.RaiseTypeError($"Cannot compare {leftClrObject.inst.GetType()} to None"); + } + + try + { + if (!TryCompare(leftClrObject.inst as Enum, rightInstance, out comparisonResult)) + { + return Exceptions.RaiseTypeError($"Cannot compare {leftClrObject.inst.GetType()} with {rightInstance.GetType()}"); + } + + return new NewReference(GetComparisonResult(op, comparisonResult)); + } + catch (ArgumentException e) + { + return Exceptions.RaiseTypeError(e.Message); + } + + default: + return new NewReference(Runtime.PyNotImplemented); + } + } + + /// + /// Tries comparing the give enum to the right operand by converting it to the appropriate type if possible + /// + /// True if the right operand was converted to a supported type and the comparison was performed successfully + private static bool TryCompare(Enum left, object right, out int result) + { + result = int.MinValue; + var conversionSuccessful = true; + var leftType = left.GetType(); + var rightType = right.GetType(); + + // Same enum comparison: + if (leftType == rightType) + { + result = left.CompareTo(right); + } + // Comparison with other enum types + else if (rightType.IsEnum) + { + var leftIsUnsigned = leftType.GetEnumUnderlyingType() == typeof(UInt64); + result = Compare(left, right as Enum, leftIsUnsigned); + } + else if (right is string rightString) + { + result = left.ToString().CompareTo(rightString); + } + else + { + var leftIsUnsigned = leftType.GetEnumUnderlyingType() == typeof(UInt64); + switch (right) + { + case double rightDouble: + result = Compare(left, rightDouble, leftIsUnsigned); + break; + case long rightLong: + result = Compare(left, rightLong, leftIsUnsigned); + break; + case ulong rightULong: + result = Compare(left, rightULong, leftIsUnsigned); + break; + case int rightInt: + result = Compare(left, (long)rightInt, leftIsUnsigned); + break; + case uint rightUInt: + result = Compare(left, (ulong)rightUInt, leftIsUnsigned); + break; + default: + conversionSuccessful = false; + break; + } + } + + return conversionSuccessful; + } + + #region Comparison against integers + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Compare(long a, ulong b) + { + if (a < 0) return -1; + return ((ulong)a).CompareTo(b); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Compare(Enum a, long b, bool isUnsigned) + { + + if (isUnsigned) + { + return -Compare(b, Convert.ToUInt64(a)); + } + return Convert.ToInt64(a).CompareTo(b); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Compare(Enum a, ulong b, bool inUnsigned) + { + if (inUnsigned) + { + return Convert.ToUInt64(a).CompareTo(b); + } + return Compare(Convert.ToInt64(a), b); + } + + #endregion + + #region Comparison against doubles + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Compare(Enum a, double b, bool isUnsigned) + { + if (isUnsigned) + { + var uIntA = Convert.ToUInt64(a); + if (uIntA < b) return -1; + if (uIntA > b) return 1; + return 0; + } + + var intA = Convert.ToInt64(a); + if (intA < b) return -1; + if (intA > b) return 1; + return 0; + } + + #endregion + + #region Comparison against other enum types + + /// + /// We support comparing enums of different types by comparing their underlying values. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Compare(Enum a, Enum b, bool isUnsigned) + { + if (b.GetType().GetEnumUnderlyingType() == typeof(UInt64)) + { + return Compare(a, Convert.ToUInt64(b), isUnsigned); + } + return Compare(a, Convert.ToInt64(b), isUnsigned); + } + + #endregion + } +} diff --git a/src/runtime/Util/OpsHelper.cs b/src/runtime/Util/OpsHelper.cs index 89ce79e20..135a67163 100644 --- a/src/runtime/Util/OpsHelper.cs +++ b/src/runtime/Util/OpsHelper.cs @@ -156,428 +156,5 @@ public static double op_Division(double a, T b) } #endregion - - #region Int comparison operators - - public static bool op_Equality(T a, long b) - { - if (IsUnsigned) - { - var uvalue = Convert.ToUInt64(a); - return b >= 0 && ((ulong)b) == uvalue; - } - return Convert.ToInt64(a) == b; - } - - public static bool op_Equality(T a, ulong b) - { - if (IsUnsigned) - { - var uvalue = Convert.ToUInt64(a); - return b == uvalue; - } - var ivalue = Convert.ToInt64(a); - return ivalue >= 0 && ((ulong)ivalue) == b; - } - - public static bool op_Equality(long a, T b) - { - return op_Equality(b, a); - } - - public static bool op_Equality(ulong a, T b) - { - return op_Equality(b, a); - } - - public static bool op_Inequality(T a, long b) - { - return !op_Equality(a, b); - } - - public static bool op_Inequality(T a, ulong b) - { - return !op_Equality(a, b); - } - - public static bool op_Inequality(long a, T b) - { - return !op_Equality(b, a); - } - - public static bool op_Inequality(ulong a, T b) - { - return !op_Equality(b, a); - } - - public static bool op_LessThan(T a, long b) - { - if (IsUnsigned) - { - var uvalue = Convert.ToUInt64(a); - return b >= 0 && ((ulong)b) > uvalue; - } - return Convert.ToInt64(a) < b; - } - - public static bool op_LessThan(T a, ulong b) - { - if (IsUnsigned) - { - var uvalue = Convert.ToUInt64(a); - return b > uvalue; - } - var ivalue = Convert.ToInt64(a); - return ivalue >= 0 && ((ulong)ivalue) < b; - } - - public static bool op_LessThan(long a, T b) - { - return op_GreaterThan(b, a); - } - - public static bool op_LessThan(ulong a, T b) - { - return op_GreaterThan(b, a); - } - - public static bool op_GreaterThan(T a, long b) - { - if (IsUnsigned) - { - var uvalue = Convert.ToUInt64(a); - return b >= 0 && ((ulong)b) < uvalue; - } - return Convert.ToInt64(a) > b; - } - - public static bool op_GreaterThan(T a, ulong b) - { - if (IsUnsigned) - { - var uvalue = Convert.ToUInt64(a); - return b < uvalue; - } - var ivalue = Convert.ToInt64(a); - return ivalue >= 0 && ((ulong)ivalue) > b; - } - - public static bool op_GreaterThan(long a, T b) - { - return op_LessThan(b, a); - } - - public static bool op_GreaterThan(ulong a, T b) - { - return op_LessThan(b, a); - } - - public static bool op_LessThanOrEqual(T a, long b) - { - if (IsUnsigned) - { - var uvalue = Convert.ToUInt64(a); - return b >= 0 && ((ulong)b) >= uvalue; - } - return Convert.ToInt64(a) <= b; - } - - public static bool op_LessThanOrEqual(T a, ulong b) - { - if (IsUnsigned) - { - var uvalue = Convert.ToUInt64(a); - return b >= uvalue; - } - var ivalue = Convert.ToInt64(a); - return ivalue >= 0 && ((ulong)ivalue) <= b; - } - - public static bool op_LessThanOrEqual(long a, T b) - { - return op_GreaterThanOrEqual(b, a); - } - - public static bool op_LessThanOrEqual(ulong a, T b) - { - return op_GreaterThanOrEqual(b, a); - } - - public static bool op_GreaterThanOrEqual(T a, long b) - { - if (IsUnsigned) - { - var uvalue = Convert.ToUInt64(a); - return b >= 0 && ((ulong)b) <= uvalue; - } - return Convert.ToInt64(a) >= b; - } - - public static bool op_GreaterThanOrEqual(T a, ulong b) - { - if (IsUnsigned) - { - var uvalue = Convert.ToUInt64(a); - return b <= uvalue; - } - var ivalue = Convert.ToInt64(a); - return ivalue >= 0 && ((ulong)ivalue) >= b; - } - - public static bool op_GreaterThanOrEqual(long a, T b) - { - return op_LessThanOrEqual(b, a); - } - - public static bool op_GreaterThanOrEqual(ulong a, T b) - { - return op_LessThanOrEqual(b, a); - } - - #endregion - - #region Double comparison operators - - public static bool op_Equality(T a, double b) - { - if (IsUnsigned) - { - return Convert.ToUInt64(a) == b; - } - return Convert.ToInt64(a) == b; - } - - public static bool op_Equality(double a, T b) - { - return op_Equality(b, a); - } - - public static bool op_Inequality(T a, double b) - { - return !op_Equality(a, b); - } - - public static bool op_Inequality(double a, T b) - { - return !op_Equality(b, a); - } - - public static bool op_LessThan(T a, double b) - { - if (IsUnsigned) - { - return Convert.ToUInt64(a) < b; - } - return Convert.ToInt64(a) < b; - } - - public static bool op_LessThan(double a, T b) - { - return op_GreaterThan(b, a); - } - - public static bool op_GreaterThan(T a, double b) - { - if (IsUnsigned) - { - return Convert.ToUInt64(a) > b; - } - return Convert.ToInt64(a) > b; - } - - public static bool op_GreaterThan(double a, T b) - { - return op_LessThan(b, a); - } - - public static bool op_LessThanOrEqual(T a, double b) - { - if (IsUnsigned) - { - return Convert.ToUInt64(a) <= b; - } - return Convert.ToInt64(a) <= b; - } - - public static bool op_LessThanOrEqual(double a, T b) - { - return op_GreaterThanOrEqual(b, a); - } - - public static bool op_GreaterThanOrEqual(T a, double b) - { - if (IsUnsigned) - { - return Convert.ToUInt64(a) >= b; - } - return Convert.ToInt64(a) >= b; - } - - public static bool op_GreaterThanOrEqual(double a, T b) - { - return op_LessThanOrEqual(b, a); - } - - #endregion - - #region String comparison operators - public static bool op_Equality(T a, string b) - { - return a.ToString().Equals(b, StringComparison.InvariantCultureIgnoreCase); - } - public static bool op_Equality(string a, T b) - { - return op_Equality(b, a); - } - - public static bool op_Inequality(T a, string b) - { - return !op_Equality(a, b); - } - - public static bool op_Inequality(string a, T b) - { - return !op_Equality(b, a); - } - - #endregion - - #region Enum comparison operators - - public static bool op_Equality(T a, Enum b) - { - if (b == null) - { - return false; - } - - if (b.GetType().GetEnumUnderlyingType() == typeof(UInt64)) - { - return op_Equality(a, Convert.ToUInt64(b)); - } - return op_Equality(a, Convert.ToInt64(b)); - } - - public static bool op_Equality(Enum a, T b) - { - return op_Equality(b, a); - } - - public static bool op_Inequality(T a, Enum b) - { - return !op_Equality(a, b); - } - - public static bool op_Inequality(Enum a, T b) - { - return !op_Equality(b, a); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void ThrowOnNull(object obj, string @operator) - { - if (obj == null) - { - using (Py.GIL()) - { - Exceptions.RaiseTypeError($"'{@operator}' not supported between instances of '{typeof(T).Name}' and null/None"); - PythonException.ThrowLastAsClrException(); - } - } - } - - public static bool op_LessThan(T a, Enum b) - { - ThrowOnNull(b, "<"); - - if (b.GetType().GetEnumUnderlyingType() == typeof(UInt64)) - { - return op_LessThan(a, Convert.ToUInt64(b)); - } - return op_LessThan(a, Convert.ToInt64(b)); - } - - public static bool op_LessThan(Enum a, T b) - { - ThrowOnNull(a, "<"); - return op_GreaterThan(b, a); - } - - public static bool op_GreaterThan(T a, Enum b) - { - ThrowOnNull(b, ">"); - - if (b.GetType().GetEnumUnderlyingType() == typeof(UInt64)) - { - return op_GreaterThan(a, Convert.ToUInt64(b)); - } - return op_GreaterThan(a, Convert.ToInt64(b)); - } - - public static bool op_GreaterThan(Enum a, T b) - { - ThrowOnNull(a, ">"); - return op_LessThan(b, a); - } - - public static bool op_LessThanOrEqual(T a, Enum b) - { - ThrowOnNull(b, "<="); - - if (b.GetType().GetEnumUnderlyingType() == typeof(UInt64)) - { - return op_LessThanOrEqual(a, Convert.ToUInt64(b)); - } - return op_LessThanOrEqual(a, Convert.ToInt64(b)); - } - - public static bool op_LessThanOrEqual(Enum a, T b) - { - ThrowOnNull(a, "<="); - return op_GreaterThanOrEqual(b, a); - } - - public static bool op_GreaterThanOrEqual(T a, Enum b) - { - ThrowOnNull(b, ">="); - - if (b.GetType().GetEnumUnderlyingType() == typeof(UInt64)) - { - return op_GreaterThanOrEqual(a, Convert.ToUInt64(b)); - } - return op_GreaterThanOrEqual(a, Convert.ToInt64(b)); - } - - public static bool op_GreaterThanOrEqual(Enum a, T b) - { - ThrowOnNull(a, ">="); - return op_LessThanOrEqual(b, a); - } - - #endregion - - #region Object equality operators - - public static bool op_Equality(T a, object b) - { - return false; - } - - public static bool op_Equality(object a, T b) - { - return false; - } - - public static bool op_Inequality(T a, object b) - { - return true; - } - - public static bool op_Inequality(object a, T b) - { - return true; - } - - #endregion } } From d2a06ce5cb5b950eb70cceb044e4fcb19f84867d Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 15 Aug 2025 11:28:11 -0400 Subject: [PATCH 19/24] Handle managed enum constructor from Python (#106) * Make EnumObject derive ClassObject for constructor handling * Bump version to 2.0.48 --- src/embed_tests/EnumTests.cs | 24 +++++++++++++++++++ src/perf_tests/Python.PerformanceTests.csproj | 4 ++-- src/runtime/Properties/AssemblyInfo.cs | 4 ++-- src/runtime/Python.Runtime.csproj | 2 +- src/runtime/Types/EnumObject.cs | 2 +- 5 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/embed_tests/EnumTests.cs b/src/embed_tests/EnumTests.cs index f8f1789d2..8deeea1cd 100644 --- a/src/embed_tests/EnumTests.cs +++ b/src/embed_tests/EnumTests.cs @@ -621,6 +621,30 @@ public void ThrowsOnNullComparisonOperators([Values("<", "<=", ">", ">=")] strin Assert.Throws(() => module.InvokeMethod("compare_with_csharp_object2", pyNull)); } + [TestCase(VerticalDirection.Down)] + [TestCase(VerticalDirection.Flat)] + [TestCase(VerticalDirection.Up)] + public void CanInstantiateEnumFromInt(VerticalDirection expectedEnumValue) + { + using var _ = Py.GIL(); + using var module = PyModule.FromString("CanInstantiateEnumFromInt", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +def get_enum(int_value): + return {nameof(EnumTests)}.{nameof(VerticalDirection)}(int_value) + +"); + + using var pyEnumIntValue = ((int)expectedEnumValue).ToPython(); + PyObject pyEnumValue = null; + Assert.DoesNotThrow(() => pyEnumValue = module.InvokeMethod("get_enum", pyEnumIntValue)); + var enumValue = pyEnumValue.As(); + Assert.AreEqual(expectedEnumValue, enumValue); + } + public class TestClass { } diff --git a/src/perf_tests/Python.PerformanceTests.csproj b/src/perf_tests/Python.PerformanceTests.csproj index 8ea99d9b2..88cb63d9e 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 4bada6682..1a42244d7 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.47")] -[assembly: AssemblyFileVersion("2.0.47")] +[assembly: AssemblyVersion("2.0.48")] +[assembly: AssemblyFileVersion("2.0.48")] diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index b60b36e6b..829f11ca4 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.47 + 2.0.48 false LICENSE https://github.com/pythonnet/pythonnet diff --git a/src/runtime/Types/EnumObject.cs b/src/runtime/Types/EnumObject.cs index 8c146ff50..d836a88ad 100644 --- a/src/runtime/Types/EnumObject.cs +++ b/src/runtime/Types/EnumObject.cs @@ -7,7 +7,7 @@ namespace Python.Runtime /// Managed class that provides the implementation for reflected enum types. /// [Serializable] - internal class EnumObject : ClassBase + internal class EnumObject : ClassObject { internal EnumObject(Type type) : base(type) { From d0feb901cfe01ae1cd83a4e8749851e9550375c2 Mon Sep 17 00:00:00 2001 From: Martin-Molinero Date: Fri, 15 Aug 2025 12:32:11 -0300 Subject: [PATCH 20/24] Improve numeric implicit conversion handling (#107) --- src/embed_tests/TestMethodBinder.cs | 6 ++-- src/runtime/MethodBinder.cs | 50 ++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/embed_tests/TestMethodBinder.cs b/src/embed_tests/TestMethodBinder.cs index 2e20870f3..979592492 100644 --- a/src/embed_tests/TestMethodBinder.cs +++ b/src/embed_tests/TestMethodBinder.cs @@ -1448,7 +1448,7 @@ public bool SomeMethod() return true; } - public virtual string OnlyClass(TestImplicitConversion data) + public virtual string OnlyClass(TestImplicitConversion data, double anotherArgument = 0) { return "OnlyClass impl"; } @@ -1458,12 +1458,12 @@ public virtual string OnlyString(string data) return "OnlyString impl: " + data; } - public virtual string InvokeModel(string data) + public virtual string InvokeModel(string data, double anotherArgument = 0) { return "string impl: " + data; } - public virtual string InvokeModel(TestImplicitConversion data) + public virtual string InvokeModel(TestImplicitConversion data, double anotherArgument = 0) { return "TestImplicitConversion impl"; } diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index d567ced0c..54fd33ff4 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -546,7 +546,7 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe var margs = new object[clrArgCount]; int paramsArrayIndex = paramsArray ? pi.Length - 1 : -1; // -1 indicates no paramsArray - var usedImplicitConversion = false; + int implicitConversions = 0; var kwargsMatched = 0; // Conversion loop for each parameter @@ -658,10 +658,14 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe else if (matches.Count == 0) { // accepts non-decimal numbers in decimal parameters - if (underlyingType == typeof(decimal)) + if (underlyingType == typeof(decimal) || underlyingType == typeof(double)) { clrtype = parameter.ParameterType; - usedImplicitConversion |= typematch = Converter.ToManaged(op, clrtype, out arg, false); + typematch = Converter.ToManaged(op, clrtype, out arg, false); + if (typematch) + { + implicitConversions++; + } } if (!typematch) { @@ -669,7 +673,11 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe var opImplicit = parameter.ParameterType.GetMethod("op_Implicit", new[] { clrtype }); if (opImplicit != null) { - usedImplicitConversion |= typematch = opImplicit.ReturnType == parameter.ParameterType; + typematch = opImplicit.ReturnType == parameter.ParameterType; + if (typematch) + { + implicitConversions++; + } clrtype = parameter.ParameterType; } } @@ -739,13 +747,10 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe } } - var match = new MatchedMethod(kwargsMatched, margs, outs, methodInformation); - if (usedImplicitConversion) + var match = new MatchedMethod(kwargsMatched, margs, outs, methodInformation, implicitConversions); + if (implicitConversions > 0) { - if (matchesUsingImplicitConversion == null) - { - matchesUsingImplicitConversion = new List(); - } + matchesUsingImplicitConversion ??= new List(); matchesUsingImplicitConversion.Add(match); } else @@ -767,11 +772,22 @@ internal Binding Bind(BorrowedReference inst, BorrowedReference args, BorrowedRe // 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(x.MethodInformation, pyArgCount, kwArgDict?.Keys)); + MatchedMethod bestMatch; + if (matchesTouse.Count == 1) + { + bestMatch = matchesTouse[0]; + } + else + { + bestMatch = matchesTouse + .GroupBy(x => x.KwargsMatched) + .OrderByDescending(x => x.Key) + .First() + .GroupBy(x => x.ImplicitOperations) + .OrderBy(x => x.Key) + .First() + .MinBy(x => GetMatchedArgumentsPrecedence(x.MethodInformation, pyArgCount, kwArgDict?.Keys)); + } var margs = bestMatch.ManagedArgs; var outs = bestMatch.Outs; @@ -1135,15 +1151,17 @@ private readonly struct MatchedMethod public int KwargsMatched { get; } public object?[] ManagedArgs { get; } public int Outs { get; } + public int ImplicitOperations { get; } public MethodInformation MethodInformation { get; } public MethodBase Method => MethodInformation.MethodBase; - public MatchedMethod(int kwargsMatched, object?[] margs, int outs, MethodInformation methodInformation) + public MatchedMethod(int kwargsMatched, object?[] margs, int outs, MethodInformation methodInformation, int implicitOperations) { KwargsMatched = kwargsMatched; ManagedArgs = margs; Outs = outs; MethodInformation = methodInformation; + ImplicitOperations = implicitOperations; } } From 223d1bef36f3ed3cff0f1c1dd8c4123e2ce3b462 Mon Sep 17 00:00:00 2001 From: Martin-Molinero Date: Tue, 21 Oct 2025 11:02:17 -0300 Subject: [PATCH 21/24] Remove `Py.AllowThreads` due to performance degradation (#108) * Remove AllowThreads call causing performance hit * Bump to 2.0.49 --- src/perf_tests/Python.PerformanceTests.csproj | 4 ++-- src/runtime/Properties/AssemblyInfo.cs | 4 ++-- src/runtime/Python.Runtime.csproj | 2 +- src/runtime/Types/FieldObject.cs | 20 ++++--------------- src/runtime/Types/PropertyObject.cs | 10 ++-------- 5 files changed, 11 insertions(+), 29 deletions(-) diff --git a/src/perf_tests/Python.PerformanceTests.csproj b/src/perf_tests/Python.PerformanceTests.csproj index 88cb63d9e..6a5f349b0 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 1a42244d7..867d91130 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.48")] -[assembly: AssemblyFileVersion("2.0.48")] +[assembly: AssemblyVersion("2.0.49")] +[assembly: AssemblyFileVersion("2.0.49")] diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index 829f11ca4..3b4b62f0c 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.48 + 2.0.49 false LICENSE https://github.com/pythonnet/pythonnet diff --git a/src/runtime/Types/FieldObject.cs b/src/runtime/Types/FieldObject.cs index b8c7ed9c7..34c5d605f 100644 --- a/src/runtime/Types/FieldObject.cs +++ b/src/runtime/Types/FieldObject.cs @@ -64,18 +64,12 @@ public static NewReference tp_descr_get(BorrowedReference ds, BorrowedReference // Fasterflect does not support constant fields if (info.IsLiteral && !info.IsInitOnly) { - using (Py.AllowThreads()) - { - result = info.GetValue(null); - } + result = info.GetValue(null); } else { var getter = self.GetMemberGetter(info.DeclaringType); - using (Py.AllowThreads()) - { - result = getter(info.DeclaringType); - } + result = getter(info.DeclaringType); } return Converter.ToPython(result, info.FieldType); @@ -99,20 +93,14 @@ public static NewReference tp_descr_get(BorrowedReference ds, BorrowedReference // Fasterflect does not support constant fields if (info.IsLiteral && !info.IsInitOnly) { - using (Py.AllowThreads()) - { - result = info.GetValue(co.inst); - } + result = info.GetValue(co.inst); } else { var type = co.inst.GetType(); var getter = self.GetMemberGetter(type); var argument = self.IsValueType(type) ? co.inst.WrapIfValueType() : co.inst; - using (Py.AllowThreads()) - { - result = getter(argument); - } + result = getter(argument); } return Converter.ToPython(result, info.FieldType); diff --git a/src/runtime/Types/PropertyObject.cs b/src/runtime/Types/PropertyObject.cs index a274e91e4..839835c09 100644 --- a/src/runtime/Types/PropertyObject.cs +++ b/src/runtime/Types/PropertyObject.cs @@ -77,10 +77,7 @@ public static NewReference tp_descr_get(BorrowedReference ds, BorrowedReference try { var getterFunc = self.GetMemberGetter(info.DeclaringType); - using (Py.AllowThreads()) - { - result = getterFunc(info.DeclaringType); - } + result = getterFunc(info.DeclaringType); return Converter.ToPython(result, info.PropertyType); } catch (Exception e) @@ -97,10 +94,7 @@ public static NewReference tp_descr_get(BorrowedReference ds, BorrowedReference try { - using (Py.AllowThreads()) - { - result = getter.Invoke(co.inst, Array.Empty()); - } + result = getter.Invoke(co.inst, Array.Empty()); return Converter.ToPython(result, info.PropertyType); } catch (Exception e) From 52f13ad8da12ee3cc8a43b0fa1bc145dd6afffc3 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Fri, 31 Oct 2025 14:51:37 -0400 Subject: [PATCH 22/24] Minor fix for fatal error when converting C# enums to int (#110) * Minor fix for fatal error when converting C# enums to in * Bump version to 2.0.50 --- src/perf_tests/Python.PerformanceTests.csproj | 4 ++-- src/runtime/Properties/AssemblyInfo.cs | 4 ++-- src/runtime/Python.Runtime.csproj | 2 +- src/runtime/Util/OpsHelper.cs | 6 ++---- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/perf_tests/Python.PerformanceTests.csproj b/src/perf_tests/Python.PerformanceTests.csproj index 6a5f349b0..caf5ae300 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 867d91130..614537465 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.49")] -[assembly: AssemblyFileVersion("2.0.49")] +[assembly: AssemblyVersion("2.0.50")] +[assembly: AssemblyFileVersion("2.0.50")] diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index 3b4b62f0c..7cbbfe39e 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.49 + 2.0.50 false LICENSE https://github.com/pythonnet/pythonnet diff --git a/src/runtime/Util/OpsHelper.cs b/src/runtime/Util/OpsHelper.cs index 135a67163..1d6e55246 100644 --- a/src/runtime/Util/OpsHelper.cs +++ b/src/runtime/Util/OpsHelper.cs @@ -83,11 +83,9 @@ internal static class EnumOps where T : Enum [ForbidPythonThreads] #pragma warning disable IDE1006 // Naming Styles - must match Python - public static PyInt __int__(T value) + public static object __int__(T value) #pragma warning restore IDE1006 // Naming Styles - => IsUnsigned - ? new PyInt(Convert.ToUInt64(value)) - : new PyInt(Convert.ToInt64(value)); + => IsUnsigned ? Convert.ToUInt64(value) : Convert.ToInt64(value); #region Arithmetic operators From 258ba5ca75f7614fe330e9ae2bfeb4dd4e45aa2f Mon Sep 17 00:00:00 2001 From: Martin-Molinero Date: Fri, 26 Dec 2025 16:43:24 -0300 Subject: [PATCH 23/24] Update to net10 (#111) --- src/console/Console.csproj | 2 +- src/embed_tests/Python.EmbeddingTest.csproj | 2 +- src/perf_tests/Python.PerformanceTests.csproj | 6 +++--- src/python_tests_runner/Python.PythonTestsRunner.csproj | 2 +- src/runtime/Properties/AssemblyInfo.cs | 4 ++-- src/runtime/Python.Runtime.csproj | 4 ++-- src/testing/Python.Test.csproj | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/console/Console.csproj b/src/console/Console.csproj index edd9054ef..418179393 100644 --- a/src/console/Console.csproj +++ b/src/console/Console.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 Exe nPython Python.Runtime diff --git a/src/embed_tests/Python.EmbeddingTest.csproj b/src/embed_tests/Python.EmbeddingTest.csproj index f50311141..7de30ad0e 100644 --- a/src/embed_tests/Python.EmbeddingTest.csproj +++ b/src/embed_tests/Python.EmbeddingTest.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 ..\pythonnet.snk true diff --git a/src/perf_tests/Python.PerformanceTests.csproj b/src/perf_tests/Python.PerformanceTests.csproj index caf5ae300..8f00c10f1 100644 --- a/src/perf_tests/Python.PerformanceTests.csproj +++ b/src/perf_tests/Python.PerformanceTests.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 false @@ -13,7 +13,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + compile @@ -25,7 +25,7 @@ - + diff --git a/src/python_tests_runner/Python.PythonTestsRunner.csproj b/src/python_tests_runner/Python.PythonTestsRunner.csproj index 16e563ff6..5ae9e922c 100644 --- a/src/python_tests_runner/Python.PythonTestsRunner.csproj +++ b/src/python_tests_runner/Python.PythonTestsRunner.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 diff --git a/src/runtime/Properties/AssemblyInfo.cs b/src/runtime/Properties/AssemblyInfo.cs index 614537465..286e75938 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.50")] -[assembly: AssemblyFileVersion("2.0.50")] +[assembly: AssemblyVersion("2.0.51")] +[assembly: AssemblyFileVersion("2.0.51")] diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index 7cbbfe39e..840482980 100644 --- a/src/runtime/Python.Runtime.csproj +++ b/src/runtime/Python.Runtime.csproj @@ -1,11 +1,11 @@ - net9.0 + net10.0 AnyCPU Python.Runtime Python.Runtime QuantConnect.pythonnet - 2.0.50 + 2.0.51 false LICENSE https://github.com/pythonnet/pythonnet diff --git a/src/testing/Python.Test.csproj b/src/testing/Python.Test.csproj index 7f688f0ba..b39411a87 100644 --- a/src/testing/Python.Test.csproj +++ b/src/testing/Python.Test.csproj @@ -1,6 +1,6 @@ - net9.0 + net10.0 true true ..\pythonnet.snk From ff33bafca9690cba77e35623eaa364cdb5d64209 Mon Sep 17 00:00:00 2001 From: Martin-Molinero Date: Mon, 29 Dec 2025 17:10:11 -0300 Subject: [PATCH 24/24] Update support for newer clr-loader supporting net10 (#112) --- pythonnet/__init__.py | 158 +++++++++++++++--- src/perf_tests/Python.PerformanceTests.csproj | 4 +- src/runtime/Properties/AssemblyInfo.cs | 4 +- src/runtime/Python.Runtime.csproj | 2 +- 4 files changed, 138 insertions(+), 30 deletions(-) diff --git a/pythonnet/__init__.py b/pythonnet/__init__.py index 10dc403e4..5c1ca108a 100644 --- a/pythonnet/__init__.py +++ b/pythonnet/__init__.py @@ -1,60 +1,168 @@ +"""Python.NET runtime loading and configuration""" + import sys +from pathlib import Path +from typing import Dict, Optional, Union, Any import clr_loader -_RUNTIME = None -_LOADER_ASSEMBLY = None -_FFI = None -_LOADED = False +__all__ = ["set_runtime", "set_runtime_from_env", "load", "unload", "get_runtime_info"] + +_RUNTIME: Optional[clr_loader.Runtime] = None +_LOADER_ASSEMBLY: Optional[clr_loader.Assembly] = None +_LOADED: bool = False + + +def set_runtime(runtime: Union[clr_loader.Runtime, str], **params: str) -> None: + """Set up a clr_loader runtime without loading it + :param runtime: + Either an already initialised `clr_loader` runtime, or one of netfx, + coreclr, mono, or default. If a string parameter is given, the runtime + will be created. + """ -def set_runtime(runtime): global _RUNTIME if _LOADED: - raise RuntimeError("The runtime {} has already been loaded".format(_RUNTIME)) + raise RuntimeError(f"The runtime {_RUNTIME} has already been loaded") - _RUNTIME = runtime + if isinstance(runtime, str): + runtime = _create_runtime_from_spec(runtime, params) + _RUNTIME = runtime -def set_default_runtime() -> None: - if sys.platform == "win32": - set_runtime(clr_loader.get_netfx()) - else: - set_runtime(clr_loader.get_mono()) +def get_runtime_info() -> Optional[clr_loader.RuntimeInfo]: + """Retrieve information on the configured runtime""" -def load(): - global _FFI, _LOADED, _LOADER_ASSEMBLY + if _RUNTIME is None: + return None + else: + return _RUNTIME.info() + + +def _get_params_from_env(prefix: str) -> Dict[str, str]: + from os import environ + + full_prefix = f"PYTHONNET_{prefix.upper()}_" + len_ = len(full_prefix) + + env_vars = { + (k[len_:].lower()): v + for k, v in environ.items() + if k.upper().startswith(full_prefix) + } + + return env_vars + + +def _create_runtime_from_spec( + spec: str, params: Optional[Dict[str, Any]] = None +) -> clr_loader.Runtime: + was_default = False + if spec == "default": + was_default = True + if sys.platform == "win32": + spec = "netfx" + else: + spec = "mono" + + params = params or _get_params_from_env(spec) + + try: + if spec == "netfx": + return clr_loader.get_netfx(**params) + elif spec == "mono": + return clr_loader.get_mono(**params) + elif spec == "coreclr": + return clr_loader.get_coreclr(**params) + else: + raise RuntimeError(f"Invalid runtime name: '{spec}'") + except Exception as exc: + if was_default: + raise RuntimeError( + f"""Failed to create a default .NET runtime, which would + have been "{spec}" on this system. Either install a + compatible runtime or configure it explicitly via + `set_runtime` or the `PYTHONNET_*` environment variables + (see set_runtime_from_env).""" + ) from exc + else: + raise RuntimeError( + f"""Failed to create a .NET runtime ({spec}) using the + parameters {params}.""" + ) from exc + + +def set_runtime_from_env() -> None: + """Set up the runtime using the environment + + This will use the environment variable PYTHONNET_RUNTIME to decide the + runtime to use, which may be one of netfx, coreclr or mono. The parameters + of the respective clr_loader.get_ functions can also be given as + environment variables, named `PYTHONNET__`. In + particular, to use `PYTHONNET_RUNTIME=coreclr`, the variable + `PYTHONNET_CORECLR_RUNTIME_CONFIG` has to be set to a valid + `.runtimeconfig.json`. + + If no environment variable is specified, a globally installed Mono is used + for all environments but Windows, on Windows the legacy .NET Framework is + used. + """ + from os import environ + + spec = environ.get("PYTHONNET_RUNTIME", "default") + runtime = _create_runtime_from_spec(spec) + set_runtime(runtime) + + +def load(runtime: Union[clr_loader.Runtime, str, None] = None, **params: str) -> None: + """Load Python.NET in the specified runtime + + The same parameters as for `set_runtime` can be used. By default, + `set_default_runtime` is called if no environment has been set yet and no + parameters are passed. + + After a successful call, further invocations will return immediately.""" + global _LOADED, _LOADER_ASSEMBLY if _LOADED: return - from os.path import join, dirname + if _RUNTIME is None: + if runtime is None: + set_runtime_from_env() + else: + set_runtime(runtime, **params) if _RUNTIME is None: - # TODO: Warn, in the future the runtime must be set explicitly, either - # as a config/env variable or via set_runtime - set_default_runtime() + raise RuntimeError("No valid runtime selected") - dll_path = join(dirname(__file__), "runtime", "Python.Runtime.dll") + dll_path = Path(__file__).parent / "runtime" / "Python.Runtime.dll" - _LOADER_ASSEMBLY = _RUNTIME.get_assembly(dll_path) + _LOADER_ASSEMBLY = assembly = _RUNTIME.get_assembly(str(dll_path)) + func = assembly.get_function("Python.Runtime.Loader.Initialize") - func = _LOADER_ASSEMBLY["Python.Runtime.Loader.Initialize"] if func(b"") != 0: raise RuntimeError("Failed to initialize Python.Runtime.dll") + + _LOADED = True import atexit atexit.register(unload) -def unload(): - global _RUNTIME +def unload() -> None: + """Explicitly unload a loaded runtime and shut down Python.NET""" + + global _RUNTIME, _LOADER_ASSEMBLY if _LOADER_ASSEMBLY is not None: - func = _LOADER_ASSEMBLY["Python.Runtime.Loader.Shutdown"] + func = _LOADER_ASSEMBLY.get_function("Python.Runtime.Loader.Shutdown") if func(b"full_shutdown") != 0: raise RuntimeError("Failed to call Python.NET shutdown") + _LOADER_ASSEMBLY = None + if _RUNTIME is not None: - # TODO: Add explicit `close` to clr_loader + _RUNTIME.shutdown() _RUNTIME = None diff --git a/src/perf_tests/Python.PerformanceTests.csproj b/src/perf_tests/Python.PerformanceTests.csproj index 8f00c10f1..dd31e6b21 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 286e75938..5e90074cf 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.51")] -[assembly: AssemblyFileVersion("2.0.51")] +[assembly: AssemblyVersion("2.0.52")] +[assembly: AssemblyFileVersion("2.0.52")] diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index 840482980..85aae5de1 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.51 + 2.0.52 false LICENSE https://github.com/pythonnet/pythonnet