Skip to content

Commit c0dccfa

Browse files
committed
Convert Python functions to managed delegates
1 parent 5f36aa7 commit c0dccfa

File tree

2 files changed

+311
-0
lines changed

2 files changed

+311
-0
lines changed

src/embed_tests/TestMethodBinder.cs

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,6 +1152,206 @@ def call_method():
11521152
Assert.AreEqual("MethodWithEnumParam With Enum", result.As<string>());
11531153
}
11541154

1155+
[Test]
1156+
public void BindsPythonToCSharpFuncDelegates()
1157+
{
1158+
using var _ = Py.GIL();
1159+
1160+
var module = PyModule.FromString("BindsPythonToCSharpFuncDelegates", @$"
1161+
from clr import AddReference
1162+
AddReference(""System"")
1163+
from Python.EmbeddingTest import *
1164+
1165+
from System import Func
1166+
1167+
class PythonModel:
1168+
last_delegate_called = None
1169+
1170+
def func1():
1171+
PythonModel.last_delegate_called = 'func1'
1172+
return TestMethodBinder.CSharpModel();
1173+
1174+
def func2(model):
1175+
if model is None or not isinstance(model, TestMethodBinder.CSharpModel):
1176+
raise TypeError(""model must be of type CSharpModel"")
1177+
PythonModel.last_delegate_called = 'func2'
1178+
return model
1179+
1180+
def func3(model1, model2):
1181+
if model1 is None or model2 is None or not isinstance(model1, TestMethodBinder.CSharpModel) or not isinstance(model2, TestMethodBinder.CSharpModel):
1182+
raise TypeError(""model1 and model2 must be of type CSharpModel"")
1183+
PythonModel.last_delegate_called = 'func3'
1184+
return model1
1185+
1186+
def call_method_with_func1():
1187+
return TestMethodBinder.CSharpModel.MethodWithFunc1(func1)
1188+
1189+
def call_method_with_func2():
1190+
return TestMethodBinder.CSharpModel.MethodWithFunc2(func2)
1191+
1192+
def call_method_with_func3():
1193+
return TestMethodBinder.CSharpModel.MethodWithFunc3(func3)
1194+
1195+
def call_method_with_func1_lambda():
1196+
return TestMethodBinder.CSharpModel.MethodWithFunc1(lambda: func1())
1197+
1198+
def call_method_with_func2_lambda():
1199+
return TestMethodBinder.CSharpModel.MethodWithFunc2(lambda model: func2(model))
1200+
1201+
def call_method_with_func3_lambda():
1202+
return TestMethodBinder.CSharpModel.MethodWithFunc3(lambda model1, model2: func3(model1, model2))
1203+
");
1204+
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+
1215+
Assert.DoesNotThrow(() =>
1216+
{
1217+
using var result = module.GetAttr("call_method_with_func1").Invoke();
1218+
var managedResult = result.As<CSharpModel>();
1219+
});
1220+
assertCalledMethods("MethodWithFunc1", "func1");
1221+
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");
1249+
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");
1256+
}
1257+
1258+
[Test]
1259+
public void BindsPythonToCSharpActionDelegates()
1260+
{
1261+
using var _ = Py.GIL();
1262+
1263+
var module = PyModule.FromString("BindsPythonToCSharpActionDelegates", @$"
1264+
from clr import AddReference
1265+
AddReference(""System"")
1266+
from Python.EmbeddingTest import *
1267+
1268+
from System import Func
1269+
1270+
class PythonModel:
1271+
last_delegate_called = None
1272+
1273+
def action1():
1274+
PythonModel.last_delegate_called = 'action1'
1275+
pass
1276+
1277+
def action2(model):
1278+
if model is None or not isinstance(model, TestMethodBinder.CSharpModel):
1279+
raise TypeError(""model must be of type CSharpModel"")
1280+
PythonModel.last_delegate_called = 'action2'
1281+
pass
1282+
1283+
def action3(model1, model2):
1284+
if model1 is None or model2 is None or not isinstance(model1, TestMethodBinder.CSharpModel) or not isinstance(model2, TestMethodBinder.CSharpModel):
1285+
raise TypeError(""model1 and model2 must be of type CSharpModel"")
1286+
PythonModel.last_delegate_called = 'action3'
1287+
pass
1288+
1289+
def call_method_with_action1():
1290+
return TestMethodBinder.CSharpModel.MethodWithAction1(action1)
1291+
1292+
def call_method_with_action2():
1293+
return TestMethodBinder.CSharpModel.MethodWithAction2(action2)
1294+
1295+
def call_method_with_action3():
1296+
return TestMethodBinder.CSharpModel.MethodWithAction3(action3)
1297+
1298+
def call_method_with_action1_lambda():
1299+
return TestMethodBinder.CSharpModel.MethodWithAction1(lambda: action1())
1300+
1301+
def call_method_with_action2_lambda():
1302+
return TestMethodBinder.CSharpModel.MethodWithAction2(lambda model: action2(model))
1303+
1304+
def call_method_with_action3_lambda():
1305+
return TestMethodBinder.CSharpModel.MethodWithAction3(lambda model1, model2: action3(model1, model2))
1306+
");
1307+
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+
1318+
Assert.DoesNotThrow(() =>
1319+
{
1320+
using var result = module.GetAttr("call_method_with_action1").Invoke();
1321+
});
1322+
assertCalledMethods("MethodWithAction1", "action1");
1323+
1324+
Assert.DoesNotThrow(() =>
1325+
{
1326+
using var result = module.GetAttr("call_method_with_action2").Invoke();
1327+
});
1328+
assertCalledMethods("MethodWithAction2", "action2");
1329+
1330+
Assert.DoesNotThrow(() =>
1331+
{
1332+
using var result = module.GetAttr("call_method_with_action3").Invoke();
1333+
});
1334+
assertCalledMethods("MethodWithAction3", "action3");
1335+
1336+
Assert.DoesNotThrow(() =>
1337+
{
1338+
using var result = module.GetAttr("call_method_with_action1_lambda").Invoke();
1339+
});
1340+
assertCalledMethods("MethodWithAction1", "action1");
1341+
1342+
Assert.DoesNotThrow(() =>
1343+
{
1344+
using var result = module.GetAttr("call_method_with_action2_lambda").Invoke();
1345+
});
1346+
assertCalledMethods("MethodWithAction2", "action2");
1347+
1348+
Assert.DoesNotThrow(() =>
1349+
{
1350+
using var result = module.GetAttr("call_method_with_action3_lambda").Invoke();
1351+
});
1352+
assertCalledMethods("MethodWithAction3", "action3");
1353+
}
1354+
11551355
// Used to test that we match this function with Py DateTime & Date Objects
11561356
public static int GetMonth(DateTime test)
11571357
{
@@ -1288,6 +1488,50 @@ public static void MethodDateTimeAndTimeSpan(CSharpModel pepe, Func<DateTime, Da
12881488
{
12891489
AssertErrorNotOccurred();
12901490
}
1491+
1492+
public static string LastDelegateCalled { get; private set; }
1493+
1494+
public static CSharpModel MethodWithFunc1(Func<CSharpModel> func)
1495+
{
1496+
AssertErrorNotOccurred();
1497+
LastDelegateCalled = "MethodWithFunc1";
1498+
return func();
1499+
}
1500+
1501+
public static CSharpModel MethodWithFunc2(Func<CSharpModel, CSharpModel> func)
1502+
{
1503+
AssertErrorNotOccurred();
1504+
LastDelegateCalled = "MethodWithFunc2";
1505+
return func(new CSharpModel());
1506+
}
1507+
1508+
public static CSharpModel MethodWithFunc3(Func<CSharpModel, CSharpModel, CSharpModel> func)
1509+
{
1510+
AssertErrorNotOccurred();
1511+
LastDelegateCalled = "MethodWithFunc3";
1512+
return func(new CSharpModel(), new CSharpModel());
1513+
}
1514+
1515+
public static void MethodWithAction1(Action action)
1516+
{
1517+
AssertErrorNotOccurred();
1518+
LastDelegateCalled = "MethodWithAction1";
1519+
action();
1520+
}
1521+
1522+
public static void MethodWithAction2(Action<CSharpModel> action)
1523+
{
1524+
AssertErrorNotOccurred();
1525+
LastDelegateCalled = "MethodWithAction2";
1526+
action(new CSharpModel());
1527+
}
1528+
1529+
public static void MethodWithAction3(Action<CSharpModel, CSharpModel> action)
1530+
{
1531+
AssertErrorNotOccurred();
1532+
LastDelegateCalled = "MethodWithAction3";
1533+
action(new CSharpModel(), new CSharpModel());
1534+
}
12911535
}
12921536

12931537
public class TestImplicitConversion

src/runtime/Converter.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,12 @@ internal static bool ToManagedValue(BorrowedReference value, Type obType,
515515
return true;
516516
}
517517

518+
if (typeof(MulticastDelegate).IsAssignableFrom(obType) && Runtime.PyCallable_Check(value) != 0 &&
519+
TryConvertToDelegate(value, obType, out result))
520+
{
521+
return true;
522+
}
523+
518524
if (obType.IsGenericType && obType.GetGenericTypeDefinition() == typeof(Nullable<>))
519525
{
520526
if (value == Runtime.PyNone)
@@ -722,6 +728,67 @@ internal static bool ToManagedExplicit(BorrowedReference value, Type obType,
722728
return ToPrimitive(explicitlyCoerced.Borrow(), obType, out result, false, out var _);
723729
}
724730

731+
/// <summary>
732+
/// Tries to convert the given Python object into a managed delegate
733+
/// </summary>
734+
/// <param name="pyValue">Python object to be converted</param>
735+
/// <param name="delegateType">The wanted delegate type</param>
736+
/// <param name="result">Managed delegate</param>
737+
/// <returns>True if successful conversion</returns>
738+
internal static bool TryConvertToDelegate(BorrowedReference pyValue, Type delegateType, out object result)
739+
{
740+
result = null;
741+
742+
if (!typeof(MulticastDelegate).IsAssignableFrom(delegateType))
743+
{
744+
return false;
745+
}
746+
747+
if (pyValue.IsNull)
748+
{
749+
return true;
750+
}
751+
752+
var code = string.Empty;
753+
var types = delegateType.GetGenericArguments();
754+
755+
using var _ = Py.GIL();
756+
using var locals = new PyDict();
757+
try
758+
{
759+
for (var i = 0; i < types.Length; i++)
760+
{
761+
var iString = i.ToString(CultureInfo.InvariantCulture);
762+
code += $",t{iString}";
763+
locals.SetItem($"t{iString}", types[i].ToPython());
764+
}
765+
766+
using var pyCallable = new PyObject(pyValue);
767+
locals.SetItem("pyCallable", pyCallable);
768+
769+
if (types.Length > 0)
770+
{
771+
var name = delegateType.FullName.Substring(0, delegateType.FullName.IndexOf('`'));
772+
code = $"import System; delegate = {name}[{code.Substring(1)}](pyCallable)";
773+
}
774+
else
775+
{
776+
var name = delegateType.FullName;
777+
code = $"import System; delegate = {name}(pyCallable)";
778+
}
779+
780+
PythonEngine.Exec(code, null, locals);
781+
result = locals.GetItem("delegate").AsManagedObject(delegateType);
782+
783+
return true;
784+
}
785+
catch
786+
{
787+
}
788+
789+
return false;
790+
}
791+
725792
/// Determine if the comparing class is a subclass of a generic type
726793
private static bool IsSubclassOfRawGeneric(Type generic, Type comparingClass)
727794
{

0 commit comments

Comments
 (0)