Skip to content

Commit dd35a84

Browse files
committed
Support managed delegates wrapped in PyObjects
Add more unit tests
1 parent 84efc34 commit dd35a84

File tree

3 files changed

+220
-98
lines changed

3 files changed

+220
-98
lines changed

src/embed_tests/TestMethodBinder.cs

Lines changed: 141 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def TestEnumerable(self):
6767
public static dynamic Numpy;
6868

6969
[OneTimeSetUp]
70-
public void SetUp()
70+
public void OneTimeSetUp()
7171
{
7272
PythonEngine.Initialize();
7373
using var _ = Py.GIL();
@@ -89,6 +89,14 @@ public void Dispose()
8989
PythonEngine.Shutdown();
9090
}
9191

92+
[SetUp]
93+
public void SetUp()
94+
{
95+
CSharpModel.LastDelegateCalled = null;
96+
CSharpModel.LastFuncCalled = null;
97+
CSharpModel.MethodCalled = null;
98+
}
99+
92100
[Test]
93101
public void MethodCalledList()
94102
{
@@ -1152,8 +1160,13 @@ def call_method():
11521160
Assert.AreEqual("MethodWithEnumParam With Enum", result.As<string>());
11531161
}
11541162

1155-
[Test]
1156-
public void BindsPythonToCSharpFuncDelegates()
1163+
[TestCase("call_method_with_func1", "MethodWithFunc1", "func1")]
1164+
[TestCase("call_method_with_func2", "MethodWithFunc2", "func2")]
1165+
[TestCase("call_method_with_func3", "MethodWithFunc3", "func3")]
1166+
[TestCase("call_method_with_func1_lambda", "MethodWithFunc1", "func1")]
1167+
[TestCase("call_method_with_func2_lambda", "MethodWithFunc2", "func2")]
1168+
[TestCase("call_method_with_func3_lambda", "MethodWithFunc3", "func3")]
1169+
public void BindsPythonToCSharpFuncDelegates(string pythonFuncToCall, string expectedCSharpMethodCalled, string expectedPythonFuncCalled)
11571170
{
11581171
using var _ = Py.GIL();
11591172

@@ -1202,61 +1215,28 @@ def call_method_with_func3_lambda():
12021215
return TestMethodBinder.CSharpModel.MethodWithFunc3(lambda model1, model2: func3(model1, model2))
12031216
");
12041217

1205-
using var pythonModel = module.GetAttr("PythonModel");
1206-
1207-
var assertCalledMethods = (string csharpCalledMethod, string pythonCalledMethod) =>
1208-
{
1209-
Assert.AreEqual(csharpCalledMethod, CSharpModel.LastDelegateCalled);
1210-
var lastDelegateCalled = pythonModel.GetAttr("last_delegate_called");
1211-
Assert.AreEqual(pythonCalledMethod, lastDelegateCalled.As<string>());
1212-
lastDelegateCalled.Dispose();
1213-
};
1214-
1218+
CSharpModel managedResult = null;
12151219
Assert.DoesNotThrow(() =>
12161220
{
1217-
using var result = module.GetAttr("call_method_with_func1").Invoke();
1218-
var managedResult = result.As<CSharpModel>();
1221+
using var result = module.GetAttr(pythonFuncToCall).Invoke();
1222+
managedResult = result.As<CSharpModel>();
12191223
});
1220-
assertCalledMethods("MethodWithFunc1", "func1");
12211224

1222-
Assert.DoesNotThrow(() =>
1223-
{
1224-
using var result = module.GetAttr("call_method_with_func2").Invoke();
1225-
var managedResult = result.As<CSharpModel>();
1226-
});
1227-
assertCalledMethods("MethodWithFunc2", "func2");
1228-
1229-
Assert.DoesNotThrow(() =>
1230-
{
1231-
using var result = module.GetAttr("call_method_with_func3").Invoke();
1232-
var managedResult = result.As<CSharpModel>();
1233-
});
1234-
assertCalledMethods("MethodWithFunc3", "func3");
1235-
1236-
Assert.DoesNotThrow(() =>
1237-
{
1238-
using var result = module.GetAttr("call_method_with_func1_lambda").Invoke();
1239-
var managedResult = result.As<CSharpModel>();
1240-
});
1241-
assertCalledMethods("MethodWithFunc1", "func1");
1242-
1243-
Assert.DoesNotThrow(() =>
1244-
{
1245-
using var result = module.GetAttr("call_method_with_func2_lambda").Invoke();
1246-
var managedResult = result.As<CSharpModel>();
1247-
});
1248-
assertCalledMethods("MethodWithFunc2", "func2");
1225+
Assert.IsNotNull(managedResult);
1226+
Assert.AreEqual(expectedCSharpMethodCalled, CSharpModel.LastDelegateCalled);
12491227

1250-
Assert.DoesNotThrow(() =>
1251-
{
1252-
using var result = module.GetAttr("call_method_with_func3_lambda").Invoke();
1253-
var managedResult = result.As<CSharpModel>();
1254-
});
1255-
assertCalledMethods("MethodWithFunc3", "func3");
1228+
using var pythonModel = module.GetAttr("PythonModel");
1229+
using var lastDelegateCalled = pythonModel.GetAttr("last_delegate_called");
1230+
Assert.AreEqual(expectedPythonFuncCalled, lastDelegateCalled.As<string>());
12561231
}
12571232

1258-
[Test]
1259-
public void BindsPythonToCSharpActionDelegates()
1233+
[TestCase("call_method_with_action1", "MethodWithAction1", "action1")]
1234+
[TestCase("call_method_with_action2", "MethodWithAction2", "action2")]
1235+
[TestCase("call_method_with_action3", "MethodWithAction3", "action3")]
1236+
[TestCase("call_method_with_action1_lambda", "MethodWithAction1", "action1")]
1237+
[TestCase("call_method_with_action2_lambda", "MethodWithAction2", "action2")]
1238+
[TestCase("call_method_with_action3_lambda", "MethodWithAction3", "action3")]
1239+
public void BindsPythonToCSharpActionDelegates(string pythonFuncToCall, string expectedCSharpMethodCalled, string expectedPythonFuncCalled)
12601240
{
12611241
using var _ = Py.GIL();
12621242

@@ -1305,51 +1285,79 @@ def call_method_with_action3_lambda():
13051285
return TestMethodBinder.CSharpModel.MethodWithAction3(lambda model1, model2: action3(model1, model2))
13061286
");
13071287

1308-
using var pythonModel = module.GetAttr("PythonModel");
1309-
1310-
var assertCalledMethods = (string csharpCalledMethod, string pythonCalledMethod) =>
1311-
{
1312-
Assert.AreEqual(csharpCalledMethod, CSharpModel.LastDelegateCalled);
1313-
var lastDelegateCalled = pythonModel.GetAttr("last_delegate_called");
1314-
Assert.AreEqual(pythonCalledMethod, lastDelegateCalled.As<string>());
1315-
lastDelegateCalled.Dispose();
1316-
};
1317-
13181288
Assert.DoesNotThrow(() =>
13191289
{
1320-
using var result = module.GetAttr("call_method_with_action1").Invoke();
1290+
using var result = module.GetAttr(pythonFuncToCall).Invoke();
13211291
});
1322-
assertCalledMethods("MethodWithAction1", "action1");
13231292

1324-
Assert.DoesNotThrow(() =>
1325-
{
1326-
using var result = module.GetAttr("call_method_with_action2").Invoke();
1327-
});
1328-
assertCalledMethods("MethodWithAction2", "action2");
1293+
Assert.AreEqual(expectedCSharpMethodCalled, CSharpModel.LastDelegateCalled);
13291294

1330-
Assert.DoesNotThrow(() =>
1331-
{
1332-
using var result = module.GetAttr("call_method_with_action3").Invoke();
1333-
});
1334-
assertCalledMethods("MethodWithAction3", "action3");
1295+
using var pythonModel = module.GetAttr("PythonModel");
1296+
using var lastDelegateCalled = pythonModel.GetAttr("last_delegate_called");
1297+
Assert.AreEqual(expectedPythonFuncCalled, lastDelegateCalled.As<string>());
1298+
}
13351299

1336-
Assert.DoesNotThrow(() =>
1337-
{
1338-
using var result = module.GetAttr("call_method_with_action1_lambda").Invoke();
1339-
});
1340-
assertCalledMethods("MethodWithAction1", "action1");
1300+
[TestCase("call_method_with_func1", "MethodWithFunc1", "TestFunc1")]
1301+
[TestCase("call_method_with_func2", "MethodWithFunc2", "TestFunc2")]
1302+
[TestCase("call_method_with_func3", "MethodWithFunc3", "TestFunc3")]
1303+
public void BindsCSharpFuncFromPythonToCSharpFuncDelegates(string pythonFuncToCall, string expectedMethodCalled, string expectedInnerMethodCalled)
1304+
{
1305+
using var _ = Py.GIL();
13411306

1307+
var module = PyModule.FromString("BindsCSharpFuncFromPythonToCSharpFuncDelegates", @$"
1308+
from clr import AddReference
1309+
AddReference(""System"")
1310+
from Python.EmbeddingTest import *
1311+
1312+
def call_method_with_func1():
1313+
return TestMethodBinder.CSharpModel.MethodWithFunc1(TestMethodBinder.CSharpModel.TestFunc1)
1314+
1315+
def call_method_with_func2():
1316+
return TestMethodBinder.CSharpModel.MethodWithFunc2(TestMethodBinder.CSharpModel.TestFunc2)
1317+
1318+
def call_method_with_func3():
1319+
return TestMethodBinder.CSharpModel.MethodWithFunc3(TestMethodBinder.CSharpModel.TestFunc3)
1320+
");
1321+
1322+
CSharpModel managedResult = null;
13421323
Assert.DoesNotThrow(() =>
13431324
{
1344-
using var result = module.GetAttr("call_method_with_action2_lambda").Invoke();
1325+
using var result = module.GetAttr(pythonFuncToCall).Invoke();
1326+
managedResult = result.As<CSharpModel>();
13451327
});
1346-
assertCalledMethods("MethodWithAction2", "action2");
1328+
Assert.IsNotNull(managedResult);
1329+
Assert.AreEqual(expectedMethodCalled, CSharpModel.LastDelegateCalled);
1330+
Assert.AreEqual(expectedInnerMethodCalled, CSharpModel.LastFuncCalled);
1331+
}
1332+
1333+
[TestCase("call_method_with_action1", "MethodWithAction1", "TestAction1")]
1334+
[TestCase("call_method_with_action2", "MethodWithAction2", "TestAction2")]
1335+
[TestCase("call_method_with_action3", "MethodWithAction3", "TestAction3")]
1336+
public void BindsCSharpActionFromPythonToCSharpActionDelegates(string pythonFuncToCall, string expectedMethodCalled, string expectedInnerMethodCalled)
1337+
{
1338+
using var _ = Py.GIL();
1339+
1340+
var module = PyModule.FromString("BindsCSharpActionFromPythonToCSharpActionDelegates", @$"
1341+
from clr import AddReference
1342+
AddReference(""System"")
1343+
from Python.EmbeddingTest import *
1344+
1345+
def call_method_with_action1():
1346+
return TestMethodBinder.CSharpModel.MethodWithAction1(TestMethodBinder.CSharpModel.TestAction1)
1347+
1348+
def call_method_with_action2():
1349+
return TestMethodBinder.CSharpModel.MethodWithAction2(TestMethodBinder.CSharpModel.TestAction2)
1350+
1351+
def call_method_with_action3():
1352+
return TestMethodBinder.CSharpModel.MethodWithAction3(TestMethodBinder.CSharpModel.TestAction3)
1353+
");
13471354

13481355
Assert.DoesNotThrow(() =>
13491356
{
1350-
using var result = module.GetAttr("call_method_with_action3_lambda").Invoke();
1357+
using var result = module.GetAttr(pythonFuncToCall).Invoke();
13511358
});
1352-
assertCalledMethods("MethodWithAction3", "action3");
1359+
Assert.AreEqual(expectedMethodCalled, CSharpModel.LastDelegateCalled);
1360+
Assert.AreEqual(expectedInnerMethodCalled, CSharpModel.LastFuncCalled);
13531361
}
13541362

13551363
// Used to test that we match this function with Py DateTime & Date Objects
@@ -1489,7 +1497,8 @@ public static void MethodDateTimeAndTimeSpan(CSharpModel pepe, Func<DateTime, Da
14891497
AssertErrorNotOccurred();
14901498
}
14911499

1492-
public static string LastDelegateCalled { get; private set; }
1500+
public static string LastDelegateCalled { get; set; }
1501+
public static string LastFuncCalled { get; set; }
14931502

14941503
public static CSharpModel MethodWithFunc1(Func<CSharpModel> func)
14951504
{
@@ -1532,6 +1541,55 @@ public static void MethodWithAction3(Action<CSharpModel, CSharpModel> action)
15321541
LastDelegateCalled = "MethodWithAction3";
15331542
action(new CSharpModel(), new CSharpModel());
15341543
}
1544+
1545+
public static CSharpModel TestFunc1()
1546+
{
1547+
LastFuncCalled = "TestFunc1";
1548+
return new CSharpModel();
1549+
}
1550+
1551+
public static CSharpModel TestFunc2(CSharpModel model)
1552+
{
1553+
if (model == null)
1554+
{
1555+
throw new ArgumentNullException(nameof(model));
1556+
}
1557+
LastFuncCalled = "TestFunc2";
1558+
return model;
1559+
}
1560+
1561+
public static CSharpModel TestFunc3(CSharpModel model1, CSharpModel model2)
1562+
{
1563+
if (model1 == null || model2 == null)
1564+
{
1565+
throw new ArgumentNullException(model1 == null ? nameof(model1) : nameof(model2));
1566+
}
1567+
LastFuncCalled = "TestFunc3";
1568+
return model1;
1569+
}
1570+
1571+
public static void TestAction1()
1572+
{
1573+
LastFuncCalled = "TestAction1";
1574+
}
1575+
1576+
public static void TestAction2(CSharpModel model)
1577+
{
1578+
if (model == null)
1579+
{
1580+
throw new ArgumentNullException(nameof(model));
1581+
}
1582+
LastFuncCalled = "TestAction2";
1583+
}
1584+
1585+
public static void TestAction3(CSharpModel model1, CSharpModel model2)
1586+
{
1587+
if (model1 == null || model2 == null)
1588+
{
1589+
throw new ArgumentNullException(model1 == null ? nameof(model1) : nameof(model2));
1590+
}
1591+
LastFuncCalled = "TestAction3";
1592+
}
15351593
}
15361594

15371595
public class TestImplicitConversion

src/runtime/Converter.cs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -505,8 +505,12 @@ internal static bool ToManagedValue(BorrowedReference value, Type obType,
505505
result = cb.type.Value;
506506
return true;
507507
}
508-
// shouldn't happen
509-
return false;
508+
// Method bindings will be handled below along with actual Python callables
509+
if (mt is not MethodBinding)
510+
{
511+
// shouldn't happen
512+
return false;
513+
}
510514
}
511515

512516
if (value == Runtime.PyNone && !obType.IsValueType)
@@ -515,12 +519,6 @@ internal static bool ToManagedValue(BorrowedReference value, Type obType,
515519
return true;
516520
}
517521

518-
if (typeof(MulticastDelegate).IsAssignableFrom(obType) && Runtime.PyCallable_Check(value) != 0 &&
519-
TryConvertToDelegate(value, obType, out result))
520-
{
521-
return true;
522-
}
523-
524522
if (obType.IsGenericType && obType.GetGenericTypeDefinition() == typeof(Nullable<>))
525523
{
526524
if (value == Runtime.PyNone)
@@ -551,6 +549,11 @@ internal static bool ToManagedValue(BorrowedReference value, Type obType,
551549
return ToEnum(value, obType, out result, setError, out usedImplicit);
552550
}
553551

552+
if (Runtime.PyCallable_Check(value) != 0 && TryConvertToDelegate(value, obType, out result))
553+
{
554+
return true;
555+
}
556+
554557
// Conversion to 'Object' is done based on some reasonable default
555558
// conversions (Python string -> managed string, Python int -> Int32 etc.).
556559
if (obType == objectType)
@@ -768,13 +771,13 @@ internal static bool TryConvertToDelegate(BorrowedReference pyValue, Type delega
768771

769772
if (types.Length > 0)
770773
{
771-
var name = delegateType.FullName.Substring(0, delegateType.FullName.IndexOf('`'));
772-
code = $"import System; delegate = {name}[{code.Substring(1)}](pyCallable)";
774+
var name = delegateType.Name.Substring(0, delegateType.Name.IndexOf('`'));
775+
code = $"from System import {name}; delegate = {name}[{code.Substring(1)}](pyCallable)";
773776
}
774777
else
775778
{
776-
var name = delegateType.FullName;
777-
code = $"import System; delegate = {name}(pyCallable)";
779+
var name = delegateType.Name;
780+
code = $"from System import {name}; delegate = {name}(pyCallable)";
778781
}
779782

780783
PythonEngine.Exec(code, null, locals);

0 commit comments

Comments
 (0)