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()