From c0dccfa9a8ff90d4c2220bbc82970eb8c9ce8b77 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Mon, 4 Aug 2025 17:54:20 -0400 Subject: [PATCH 1/5] Convert Python functions to managed delegates --- src/embed_tests/TestMethodBinder.cs | 244 ++++++++++++++++++++++++++++ src/runtime/Converter.cs | 67 ++++++++ 2 files changed, 311 insertions(+) diff --git a/src/embed_tests/TestMethodBinder.cs b/src/embed_tests/TestMethodBinder.cs index 0b3f6497c..62e9a254a 100644 --- a/src/embed_tests/TestMethodBinder.cs +++ b/src/embed_tests/TestMethodBinder.cs @@ -1152,6 +1152,206 @@ def call_method(): Assert.AreEqual("MethodWithEnumParam With Enum", result.As()); } + [Test] + public void BindsPythonToCSharpFuncDelegates() + { + 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)) +"); + + using var pythonModel = module.GetAttr("PythonModel"); + + var assertCalledMethods = (string csharpCalledMethod, string pythonCalledMethod) => + { + Assert.AreEqual(csharpCalledMethod, CSharpModel.LastDelegateCalled); + var lastDelegateCalled = pythonModel.GetAttr("last_delegate_called"); + Assert.AreEqual(pythonCalledMethod, lastDelegateCalled.As()); + lastDelegateCalled.Dispose(); + }; + + Assert.DoesNotThrow(() => + { + using var result = module.GetAttr("call_method_with_func1").Invoke(); + var managedResult = result.As(); + }); + assertCalledMethods("MethodWithFunc1", "func1"); + + Assert.DoesNotThrow(() => + { + using var result = module.GetAttr("call_method_with_func2").Invoke(); + var managedResult = result.As(); + }); + assertCalledMethods("MethodWithFunc2", "func2"); + + Assert.DoesNotThrow(() => + { + using var result = module.GetAttr("call_method_with_func3").Invoke(); + var managedResult = result.As(); + }); + assertCalledMethods("MethodWithFunc3", "func3"); + + Assert.DoesNotThrow(() => + { + using var result = module.GetAttr("call_method_with_func1_lambda").Invoke(); + var managedResult = result.As(); + }); + assertCalledMethods("MethodWithFunc1", "func1"); + + Assert.DoesNotThrow(() => + { + using var result = module.GetAttr("call_method_with_func2_lambda").Invoke(); + var managedResult = result.As(); + }); + assertCalledMethods("MethodWithFunc2", "func2"); + + Assert.DoesNotThrow(() => + { + using var result = module.GetAttr("call_method_with_func3_lambda").Invoke(); + var managedResult = result.As(); + }); + assertCalledMethods("MethodWithFunc3", "func3"); + } + + [Test] + public void BindsPythonToCSharpActionDelegates() + { + 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)) +"); + + using var pythonModel = module.GetAttr("PythonModel"); + + var assertCalledMethods = (string csharpCalledMethod, string pythonCalledMethod) => + { + Assert.AreEqual(csharpCalledMethod, CSharpModel.LastDelegateCalled); + var lastDelegateCalled = pythonModel.GetAttr("last_delegate_called"); + Assert.AreEqual(pythonCalledMethod, lastDelegateCalled.As()); + lastDelegateCalled.Dispose(); + }; + + Assert.DoesNotThrow(() => + { + using var result = module.GetAttr("call_method_with_action1").Invoke(); + }); + assertCalledMethods("MethodWithAction1", "action1"); + + Assert.DoesNotThrow(() => + { + using var result = module.GetAttr("call_method_with_action2").Invoke(); + }); + assertCalledMethods("MethodWithAction2", "action2"); + + Assert.DoesNotThrow(() => + { + using var result = module.GetAttr("call_method_with_action3").Invoke(); + }); + assertCalledMethods("MethodWithAction3", "action3"); + + Assert.DoesNotThrow(() => + { + using var result = module.GetAttr("call_method_with_action1_lambda").Invoke(); + }); + assertCalledMethods("MethodWithAction1", "action1"); + + Assert.DoesNotThrow(() => + { + using var result = module.GetAttr("call_method_with_action2_lambda").Invoke(); + }); + assertCalledMethods("MethodWithAction2", "action2"); + + Assert.DoesNotThrow(() => + { + using var result = module.GetAttr("call_method_with_action3_lambda").Invoke(); + }); + assertCalledMethods("MethodWithAction3", "action3"); + } + // Used to test that we match this function with Py DateTime & Date Objects public static int GetMonth(DateTime test) { @@ -1288,6 +1488,50 @@ 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 class TestImplicitConversion diff --git a/src/runtime/Converter.cs b/src/runtime/Converter.cs index fc6437bc1..24b179cc9 100644 --- a/src/runtime/Converter.cs +++ b/src/runtime/Converter.cs @@ -515,6 +515,12 @@ internal static bool ToManagedValue(BorrowedReference value, Type obType, return true; } + if (typeof(MulticastDelegate).IsAssignableFrom(obType) && Runtime.PyCallable_Check(value) != 0 && + TryConvertToDelegate(value, obType, out result)) + { + return true; + } + if (obType.IsGenericType && obType.GetGenericTypeDefinition() == typeof(Nullable<>)) { if (value == Runtime.PyNone) @@ -722,6 +728,67 @@ 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)) + { + return false; + } + + if (pyValue.IsNull) + { + return true; + } + + var code = string.Empty; + var types = delegateType.GetGenericArguments(); + + using var _ = Py.GIL(); + using var locals = new PyDict(); + try + { + for (var i = 0; i < types.Length; i++) + { + var iString = i.ToString(CultureInfo.InvariantCulture); + code += $",t{iString}"; + locals.SetItem($"t{iString}", types[i].ToPython()); + } + + using var pyCallable = new PyObject(pyValue); + locals.SetItem("pyCallable", pyCallable); + + if (types.Length > 0) + { + var name = delegateType.FullName.Substring(0, delegateType.FullName.IndexOf('`')); + code = $"import System; delegate = {name}[{code.Substring(1)}](pyCallable)"; + } + else + { + var name = delegateType.FullName; + code = $"import System; 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) { From 84efc34e3148d200557328cb1d3231e49e23cd50 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Mon, 4 Aug 2025 18:00:34 -0400 Subject: [PATCH 2/5] Bump version to 2.0.46 --- 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 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/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 From dd35a84ec4ed275bb6cb9144defd8be60ebbf5d0 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 5 Aug 2025 13:06:53 -0400 Subject: [PATCH 3/5] Support managed delegates wrapped in PyObjects Add more unit tests --- src/embed_tests/TestMethodBinder.cs | 224 +++++++++++++++++----------- src/runtime/Converter.cs | 27 ++-- src/runtime/PythonTypes/PyObject.cs | 67 ++++++++- 3 files changed, 220 insertions(+), 98 deletions(-) diff --git a/src/embed_tests/TestMethodBinder.cs b/src/embed_tests/TestMethodBinder.cs index 62e9a254a..3f8b0a05d 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,14 @@ public void Dispose() PythonEngine.Shutdown(); } + [SetUp] + public void SetUp() + { + CSharpModel.LastDelegateCalled = null; + CSharpModel.LastFuncCalled = null; + CSharpModel.MethodCalled = null; + } + [Test] public void MethodCalledList() { @@ -1152,8 +1160,13 @@ def call_method(): Assert.AreEqual("MethodWithEnumParam With Enum", result.As()); } - [Test] - public void BindsPythonToCSharpFuncDelegates() + [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(); @@ -1202,61 +1215,28 @@ def call_method_with_func3_lambda(): return TestMethodBinder.CSharpModel.MethodWithFunc3(lambda model1, model2: func3(model1, model2)) "); - using var pythonModel = module.GetAttr("PythonModel"); - - var assertCalledMethods = (string csharpCalledMethod, string pythonCalledMethod) => - { - Assert.AreEqual(csharpCalledMethod, CSharpModel.LastDelegateCalled); - var lastDelegateCalled = pythonModel.GetAttr("last_delegate_called"); - Assert.AreEqual(pythonCalledMethod, lastDelegateCalled.As()); - lastDelegateCalled.Dispose(); - }; - + CSharpModel managedResult = null; Assert.DoesNotThrow(() => { - using var result = module.GetAttr("call_method_with_func1").Invoke(); - var managedResult = result.As(); + using var result = module.GetAttr(pythonFuncToCall).Invoke(); + managedResult = result.As(); }); - assertCalledMethods("MethodWithFunc1", "func1"); - Assert.DoesNotThrow(() => - { - using var result = module.GetAttr("call_method_with_func2").Invoke(); - var managedResult = result.As(); - }); - assertCalledMethods("MethodWithFunc2", "func2"); - - Assert.DoesNotThrow(() => - { - using var result = module.GetAttr("call_method_with_func3").Invoke(); - var managedResult = result.As(); - }); - assertCalledMethods("MethodWithFunc3", "func3"); - - Assert.DoesNotThrow(() => - { - using var result = module.GetAttr("call_method_with_func1_lambda").Invoke(); - var managedResult = result.As(); - }); - assertCalledMethods("MethodWithFunc1", "func1"); - - Assert.DoesNotThrow(() => - { - using var result = module.GetAttr("call_method_with_func2_lambda").Invoke(); - var managedResult = result.As(); - }); - assertCalledMethods("MethodWithFunc2", "func2"); + Assert.IsNotNull(managedResult); + Assert.AreEqual(expectedCSharpMethodCalled, CSharpModel.LastDelegateCalled); - Assert.DoesNotThrow(() => - { - using var result = module.GetAttr("call_method_with_func3_lambda").Invoke(); - var managedResult = result.As(); - }); - assertCalledMethods("MethodWithFunc3", "func3"); + using var pythonModel = module.GetAttr("PythonModel"); + using var lastDelegateCalled = pythonModel.GetAttr("last_delegate_called"); + Assert.AreEqual(expectedPythonFuncCalled, lastDelegateCalled.As()); } - [Test] - public void BindsPythonToCSharpActionDelegates() + [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(); @@ -1305,51 +1285,79 @@ def call_method_with_action3_lambda(): return TestMethodBinder.CSharpModel.MethodWithAction3(lambda model1, model2: action3(model1, model2)) "); - using var pythonModel = module.GetAttr("PythonModel"); - - var assertCalledMethods = (string csharpCalledMethod, string pythonCalledMethod) => - { - Assert.AreEqual(csharpCalledMethod, CSharpModel.LastDelegateCalled); - var lastDelegateCalled = pythonModel.GetAttr("last_delegate_called"); - Assert.AreEqual(pythonCalledMethod, lastDelegateCalled.As()); - lastDelegateCalled.Dispose(); - }; - Assert.DoesNotThrow(() => { - using var result = module.GetAttr("call_method_with_action1").Invoke(); + using var result = module.GetAttr(pythonFuncToCall).Invoke(); }); - assertCalledMethods("MethodWithAction1", "action1"); - Assert.DoesNotThrow(() => - { - using var result = module.GetAttr("call_method_with_action2").Invoke(); - }); - assertCalledMethods("MethodWithAction2", "action2"); + Assert.AreEqual(expectedCSharpMethodCalled, CSharpModel.LastDelegateCalled); - Assert.DoesNotThrow(() => - { - using var result = module.GetAttr("call_method_with_action3").Invoke(); - }); - assertCalledMethods("MethodWithAction3", "action3"); + using var pythonModel = module.GetAttr("PythonModel"); + using var lastDelegateCalled = pythonModel.GetAttr("last_delegate_called"); + Assert.AreEqual(expectedPythonFuncCalled, lastDelegateCalled.As()); + } - Assert.DoesNotThrow(() => - { - using var result = module.GetAttr("call_method_with_action1_lambda").Invoke(); - }); - assertCalledMethods("MethodWithAction1", "action1"); + [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("call_method_with_action2_lambda").Invoke(); + using var result = module.GetAttr(pythonFuncToCall).Invoke(); + managedResult = result.As(); }); - assertCalledMethods("MethodWithAction2", "action2"); + 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("call_method_with_action3_lambda").Invoke(); + using var result = module.GetAttr(pythonFuncToCall).Invoke(); }); - assertCalledMethods("MethodWithAction3", "action3"); + Assert.AreEqual(expectedMethodCalled, CSharpModel.LastDelegateCalled); + Assert.AreEqual(expectedInnerMethodCalled, CSharpModel.LastFuncCalled); } // Used to test that we match this function with Py DateTime & Date Objects @@ -1489,7 +1497,8 @@ public static void MethodDateTimeAndTimeSpan(CSharpModel pepe, Func func) { @@ -1532,6 +1541,55 @@ public static void MethodWithAction3(Action action) 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/runtime/Converter.cs b/src/runtime/Converter.cs index 24b179cc9..21c5f3bea 100644 --- a/src/runtime/Converter.cs +++ b/src/runtime/Converter.cs @@ -505,8 +505,12 @@ 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) + { + // shouldn't happen + return false; + } } if (value == Runtime.PyNone && !obType.IsValueType) @@ -515,12 +519,6 @@ internal static bool ToManagedValue(BorrowedReference value, Type obType, return true; } - if (typeof(MulticastDelegate).IsAssignableFrom(obType) && Runtime.PyCallable_Check(value) != 0 && - TryConvertToDelegate(value, obType, out result)) - { - return true; - } - if (obType.IsGenericType && obType.GetGenericTypeDefinition() == typeof(Nullable<>)) { if (value == Runtime.PyNone) @@ -551,6 +549,11 @@ internal static bool ToManagedValue(BorrowedReference value, Type obType, return ToEnum(value, obType, out result, setError, out usedImplicit); } + if (Runtime.PyCallable_Check(value) != 0 && 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) @@ -768,13 +771,13 @@ internal static bool TryConvertToDelegate(BorrowedReference pyValue, Type delega if (types.Length > 0) { - var name = delegateType.FullName.Substring(0, delegateType.FullName.IndexOf('`')); - code = $"import System; delegate = {name}[{code.Substring(1)}](pyCallable)"; + var name = delegateType.Name.Substring(0, delegateType.Name.IndexOf('`')); + code = $"from System import {name}; delegate = {name}[{code.Substring(1)}](pyCallable)"; } else { - var name = delegateType.FullName; - code = $"import System; delegate = {name}(pyCallable)"; + var name = delegateType.Name; + code = $"from System import {name}; delegate = {name}(pyCallable)"; } PythonEngine.Exec(code, null, locals); diff --git a/src/runtime/PythonTypes/PyObject.cs b/src/runtime/PythonTypes/PyObject.cs index e0a17bed5..f70915d08 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,67 @@ 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(); + if (TryAsManagedObject(typeof(T), out var obj)) + { + if (obj is T t) + { + result = t; + return true; + } + } + + result = default!; + return false; + } + internal bool IsDisposed => rawPtr == IntPtr.Zero; void CheckDisposed() @@ -235,7 +296,7 @@ public void Dispose() { GC.SuppressFinalize(this); Dispose(true); - + } internal StolenReference Steal() From 4172e3d80e1ac46f8f2d060cd7648310975b5c05 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 5 Aug 2025 18:11:50 -0400 Subject: [PATCH 4/5] Cleanup --- src/runtime/Converter.cs | 22 ++++++++++------------ src/runtime/PythonTypes/PyObject.cs | 12 +----------- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/src/runtime/Converter.cs b/src/runtime/Converter.cs index 21c5f3bea..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 { @@ -508,7 +509,6 @@ internal static bool ToManagedValue(BorrowedReference value, Type obType, // Method bindings will be handled below along with actual Python callables if (mt is not MethodBinding) { - // shouldn't happen return false; } } @@ -549,7 +549,7 @@ internal static bool ToManagedValue(BorrowedReference value, Type obType, return ToEnum(value, obType, out result, setError, out usedImplicit); } - if (Runtime.PyCallable_Check(value) != 0 && TryConvertToDelegate(value, obType, out result)) + if (TryConvertToDelegate(value, obType, out result)) { return true; } @@ -742,7 +742,7 @@ internal static bool TryConvertToDelegate(BorrowedReference pyValue, Type delega { result = null; - if (!typeof(MulticastDelegate).IsAssignableFrom(delegateType)) + if (!typeof(MulticastDelegate).IsAssignableFrom(delegateType) || Runtime.PyCallable_Check(pyValue) == 0) { return false; } @@ -755,24 +755,22 @@ internal static bool TryConvertToDelegate(BorrowedReference pyValue, Type delega var code = string.Empty; var types = delegateType.GetGenericArguments(); - using var _ = Py.GIL(); using var locals = new PyDict(); try { - for (var i = 0; i < types.Length; i++) - { - var iString = i.ToString(CultureInfo.InvariantCulture); - code += $",t{iString}"; - locals.SetItem($"t{iString}", types[i].ToPython()); - } - 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.Substring(1)}](pyCallable)"; + code = $"from System import {name}; delegate = {name}[{code}](pyCallable)"; } else { diff --git a/src/runtime/PythonTypes/PyObject.cs b/src/runtime/PythonTypes/PyObject.cs index f70915d08..96472ce25 100644 --- a/src/runtime/PythonTypes/PyObject.cs +++ b/src/runtime/PythonTypes/PyObject.cs @@ -225,17 +225,7 @@ public T SafeAs() public bool TrySafeAs(out T result) { using var _ = Py.GIL(); - if (TryAsManagedObject(typeof(T), out var obj)) - { - if (obj is T t) - { - result = t; - return true; - } - } - - result = default!; - return false; + return TryAs(out result); } internal bool IsDisposed => rawPtr == IntPtr.Zero; From 15ae38604866dcc95c83c7c8f512623d695c53b2 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 6 Aug 2025 16:26:38 -0400 Subject: [PATCH 5/5] Fix enums precedence in MethodBinder --- src/embed_tests/TestMethodBinder.cs | 46 +++++++++++++++++++++++++++++ src/runtime/MethodBinder.cs | 20 +++++++++---- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/src/embed_tests/TestMethodBinder.cs b/src/embed_tests/TestMethodBinder.cs index 3f8b0a05d..2e20870f3 100644 --- a/src/embed_tests/TestMethodBinder.cs +++ b/src/embed_tests/TestMethodBinder.cs @@ -95,6 +95,7 @@ public void SetUp() CSharpModel.LastDelegateCalled = null; CSharpModel.LastFuncCalled = null; CSharpModel.MethodCalled = null; + CSharpModel.ProvidedArgument = null; } [Test] @@ -1360,6 +1361,47 @@ def call_method_with_action3(): 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) { @@ -1442,6 +1484,10 @@ public void NumericalArgumentMethod(decimal value) { ProvidedArgument = value; } + public void NumericalArgumentMethod(DayOfWeek value) + { + ProvidedArgument = value; + } public void EnumerableKeyValuePair(IEnumerable> value) { ProvidedArgument = value; 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;