Skip to content

Commit 514040c

Browse files
committed
add function codec
1 parent 62cb929 commit 514040c

File tree

3 files changed

+392
-0
lines changed

3 files changed

+392
-0
lines changed

src/FunctionCodec.cs

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
using System;
2+
using System.Reflection;
3+
4+
namespace Python.Runtime.Codecs
5+
{
6+
//converts python functions to C# actions
7+
public class FunctionCodec : IPyObjectDecoder
8+
{
9+
private static int GetNumArgs(PyObject pyCallable)
10+
{
11+
var locals = new PyDict();
12+
locals.SetItem("f", pyCallable);
13+
using (Py.GIL())
14+
PythonEngine.Exec(@"
15+
from inspect import signature
16+
try:
17+
x = len(signature(f).parameters)
18+
except:
19+
x = 0
20+
", null, locals.Handle);
21+
22+
var x = locals.GetItem("x");
23+
return new PyInt(x).ToInt32();
24+
}
25+
26+
private static int GetNumArgs(Type targetType)
27+
{
28+
MethodInfo invokeMethod = targetType.GetMethod("Invoke");
29+
return invokeMethod.GetParameters().Length;
30+
}
31+
32+
private static bool IsUnaryAction(Type targetType)
33+
{
34+
return targetType == typeof(Action);
35+
}
36+
37+
private static bool IsVariadicObjectAction(Type targetType)
38+
{
39+
return targetType == typeof(Action<object[]>);
40+
}
41+
42+
private static bool IsUnaryFunc(Type targetType)
43+
{
44+
return targetType == typeof(Func<object>);
45+
}
46+
47+
private static bool IsVariadicObjectFunc(Type targetType)
48+
{
49+
return targetType == typeof(Func<object[], object>);
50+
}
51+
52+
private static bool IsAction(Type targetType)
53+
{
54+
return IsUnaryAction(targetType) || IsVariadicObjectAction(targetType);
55+
}
56+
57+
private static bool IsFunc(Type targetType)
58+
{
59+
return IsUnaryFunc(targetType) || IsVariadicObjectFunc(targetType);
60+
}
61+
62+
private static bool IsCallable(Type targetType)
63+
{
64+
//Python.Runtime.ClassManager dtype
65+
return targetType.IsSubclassOf(typeof(MulticastDelegate));
66+
}
67+
68+
public static FunctionCodec Instance { get; } = new FunctionCodec();
69+
public bool CanDecode(PyObject objectType, Type targetType)
70+
{
71+
//python object must be callable
72+
if (!objectType.IsCallable()) return false;
73+
74+
//C# object must be callable
75+
if (!IsCallable(targetType))
76+
return false;
77+
78+
return true;
79+
}
80+
81+
private static object ConvertUnaryAction(PyObject pyObj)
82+
{
83+
Func<object> func = (Func<object>)ConvertUnaryFunc(pyObj);
84+
Action action = () => { func(); };
85+
return (object)action;
86+
}
87+
88+
private static object ConvertVariadicObjectAction(PyObject pyObj, int numArgs)
89+
{
90+
Func<object[], object> func = (Func<object[], object>)ConvertVariadicObjectFunc(pyObj, numArgs);
91+
Action<object[]> action = (object[] args) => { func(args); };
92+
return (object)action;
93+
}
94+
95+
//TODO share code between ConvertUnaryFunc and ConvertVariadicObjectFunc
96+
private static object ConvertUnaryFunc(PyObject pyObj)
97+
{
98+
var pyAction = new PyObject(pyObj.Handle);
99+
Func<object> func = () =>
100+
{
101+
var pyArgs = new PyObject[0];
102+
using (Py.GIL())
103+
{
104+
var pyResult = pyAction.Invoke(pyArgs);
105+
return pyResult.As<object>();
106+
}
107+
};
108+
return (object)func;
109+
}
110+
111+
private static object ConvertVariadicObjectFunc(PyObject pyObj, int numArgs)
112+
{
113+
var pyAction = new PyObject(pyObj.Handle);
114+
Func<object[], object> func = (object[] o) =>
115+
{
116+
var pyArgs = new PyObject[numArgs];
117+
int i = 0;
118+
foreach (object obj in o)
119+
{
120+
pyArgs[i++] = obj.ToPython();
121+
}
122+
123+
using (Py.GIL())
124+
{
125+
var pyResult = pyAction.Invoke(pyArgs);
126+
return pyResult.As<object>();
127+
}
128+
};
129+
return (object)func;
130+
}
131+
132+
public bool TryDecode<T>(PyObject pyObj, out T value)
133+
{
134+
value = default(T);
135+
var tT = typeof(T);
136+
if (!IsCallable(tT))
137+
return false;
138+
139+
var numArgs = GetNumArgs(pyObj);
140+
if (numArgs != GetNumArgs(tT))
141+
return false;
142+
143+
if (IsAction(tT))
144+
{
145+
object actionObj = null;
146+
if (numArgs == 0)
147+
{
148+
actionObj = ConvertUnaryAction(pyObj);
149+
}
150+
else
151+
{
152+
actionObj = ConvertVariadicObjectAction(pyObj, numArgs);
153+
}
154+
155+
value = (T)actionObj;
156+
return true;
157+
}
158+
else if (IsFunc(tT))
159+
{
160+
161+
object funcObj = null;
162+
if (numArgs == 0)
163+
{
164+
funcObj = ConvertUnaryFunc(pyObj);
165+
}
166+
else
167+
{
168+
funcObj = ConvertVariadicObjectFunc(pyObj, numArgs);
169+
}
170+
171+
value = (T)funcObj;
172+
return true;
173+
}
174+
else
175+
{
176+
return false;
177+
}
178+
}
179+
180+
public static void Register()
181+
{
182+
PyObjectConversions.RegisterDecoder(Instance);
183+
}
184+
}
185+
}

tests/FunctionCodecTests.cs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
using System;
2+
using NUnit.Framework;
3+
4+
namespace Python.Runtime.Codecs
5+
{
6+
class FunctionCodecTests
7+
{
8+
[SetUp]
9+
public void SetUp()
10+
{
11+
PythonEngine.Initialize();
12+
}
13+
14+
15+
[TearDown]
16+
public void Dispose()
17+
{
18+
PythonEngine.Shutdown();
19+
}
20+
21+
[Test]
22+
public void FunctionAction()
23+
{
24+
var codec = FunctionCodec.Instance;
25+
26+
PyInt x = new PyInt(1);
27+
PyDict y = new PyDict();
28+
//non-callables can't be decoded into Action
29+
Assert.IsFalse(codec.CanDecode(x, typeof(Action)));
30+
Assert.IsFalse(codec.CanDecode(y, typeof(Action)));
31+
32+
var locals = new PyDict();
33+
PythonEngine.Exec(@"
34+
def foo():
35+
return 1
36+
def bar(a):
37+
return 2
38+
", null, locals.Handle);
39+
40+
//foo, the function with no arguments
41+
var fooFunc = locals.GetItem("foo");
42+
Assert.IsFalse(codec.CanDecode(fooFunc, typeof(bool)));
43+
44+
//CanDecode does not work for variadic actions
45+
//Assert.IsFalse(codec.CanDecode(fooFunc, typeof(Action<object[]>)));
46+
Assert.IsTrue(codec.CanDecode(fooFunc, typeof(Action)));
47+
48+
Action fooAction;
49+
Assert.IsTrue(codec.TryDecode(fooFunc, out fooAction));
50+
Assert.DoesNotThrow(() => fooAction());
51+
52+
//bar, the function with an argument
53+
var barFunc = locals.GetItem("bar");
54+
Assert.IsFalse(codec.CanDecode(barFunc, typeof(bool)));
55+
//Assert.IsFalse(codec.CanDecode(barFunc, typeof(Action)));
56+
Assert.IsTrue(codec.CanDecode(barFunc, typeof(Action<object[]>)));
57+
58+
Action<object[]> barAction;
59+
Assert.IsTrue(codec.TryDecode(barFunc, out barAction));
60+
Assert.DoesNotThrow(() => barAction(new[] { (object)true }));
61+
}
62+
63+
[Test]
64+
public void FunctionFunc()
65+
{
66+
var codec = FunctionCodec.Instance;
67+
68+
PyInt x = new PyInt(1);
69+
PyDict y = new PyDict();
70+
//non-callables can't be decoded into Func
71+
Assert.IsFalse(codec.CanDecode(x, typeof(Func<object>)));
72+
Assert.IsFalse(codec.CanDecode(y, typeof(Func<object>)));
73+
74+
var locals = new PyDict();
75+
PythonEngine.Exec(@"
76+
def foo():
77+
return 1
78+
def bar(a):
79+
return 2
80+
", null, locals.Handle);
81+
82+
//foo, the function with no arguments
83+
var fooFunc = locals.GetItem("foo");
84+
Assert.IsFalse(codec.CanDecode(fooFunc, typeof(bool)));
85+
86+
//CanDecode does not work for variadic actions
87+
//Assert.IsFalse(codec.CanDecode(fooFunc, typeof(Func<object[], object>)));
88+
Assert.IsTrue(codec.CanDecode(fooFunc, typeof(Func<object>)));
89+
90+
Func<object> foo;
91+
Assert.IsTrue(codec.TryDecode(fooFunc, out foo));
92+
object res1 = null;
93+
Assert.DoesNotThrow(() => res1 = foo());
94+
Assert.AreEqual(res1, 1);
95+
96+
//bar, the function with an argument
97+
var barFunc = locals.GetItem("bar");
98+
Assert.IsFalse(codec.CanDecode(barFunc, typeof(bool)));
99+
//Assert.IsFalse(codec.CanDecode(barFunc, typeof(Func<object>)));
100+
Assert.IsTrue(codec.CanDecode(barFunc, typeof(Func<object[], object>)));
101+
102+
Func<object[], object> bar;
103+
Assert.IsTrue(codec.TryDecode(barFunc, out bar));
104+
object res2 = null;
105+
Assert.DoesNotThrow(() => res2 = bar(new[] { (object)true }));
106+
Assert.AreEqual(res2, 2);
107+
}
108+
109+
}
110+
}

tests/TestCallbacks.cs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using System;
2+
using NUnit.Framework;
3+
4+
namespace Python.Runtime.Codecs
5+
{
6+
public class TestCallbacks
7+
{
8+
[OneTimeSetUp]
9+
public void SetUp()
10+
{
11+
PythonEngine.Initialize();
12+
}
13+
14+
[OneTimeTearDown]
15+
public void Dispose()
16+
{
17+
PythonEngine.Shutdown();
18+
}
19+
20+
private class Callables
21+
{
22+
internal object CallFunction0(Func<object> func)
23+
{
24+
return func();
25+
}
26+
27+
internal object CallFunction1(Func<object[], object> func, object arg)
28+
{
29+
return func(new[] { arg });
30+
}
31+
32+
internal void CallAction0(Action func)
33+
{
34+
func();
35+
}
36+
37+
internal void CallAction1(Action<object[]> func, object arg)
38+
{
39+
func(new[] { arg });
40+
}
41+
}
42+
43+
[Test]
44+
public void TestPythonFunctionPassedIntoCLRMethod()
45+
{
46+
var locals = new PyDict();
47+
PythonEngine.Exec(@"
48+
def ret_1():
49+
return 1
50+
def str_len(a):
51+
return len(a)
52+
", null, locals.Handle);
53+
54+
var ret1 = locals.GetItem("ret_1");
55+
var strLen = locals.GetItem("str_len");
56+
57+
var callables = new Callables();
58+
59+
FunctionCodec.Register();
60+
61+
//ret1. A function with no arguments that returns an integer
62+
//it must be convertible to Action or Func<object> and not to Func<object, object>
63+
{
64+
Action result1 = null;
65+
Func<object> result2 = null;
66+
Assert.DoesNotThrow(() => { result1 = ret1.As<Action>(); });
67+
Assert.DoesNotThrow(() => { result2 = ret1.As<Func<object>>(); });
68+
69+
Assert.DoesNotThrow(() => { callables.CallAction0((Action)result1); });
70+
object ret2 = null;
71+
Assert.DoesNotThrow(() => { ret2 = callables.CallFunction0((Func<object>)result2); });
72+
Assert.AreEqual(ret2, 1);
73+
}
74+
75+
//strLen. A function that takes something with a __len__ and returns the result of that function
76+
//It must be convertible to an Action<object[]> and Func<object[], object>) and not to an Action or Func<object>
77+
{
78+
Action<object[]> result3 = null;
79+
Func<object[], object> result4 = null;
80+
Assert.DoesNotThrow(() => { result3 = strLen.As<Action<object[]>>(); });
81+
Assert.DoesNotThrow(() => { result4 = strLen.As<Func<object[], object>>(); });
82+
83+
//try using both func and action to show you can get __len__ of a string but not an integer
84+
Assert.Throws<PythonException>(() => { callables.CallAction1((Action<object[]>)result3, 2); });
85+
Assert.DoesNotThrow(() => { callables.CallAction1((Action<object[]>)result3, "hello"); });
86+
Assert.Throws<PythonException>(() => { callables.CallFunction1((Func<object[], object>)result4, 2); });
87+
88+
object ret2 = null;
89+
Assert.DoesNotThrow(() => { ret2 = callables.CallFunction1((Func<object[], object>)result4, "hello"); });
90+
Assert.AreEqual(ret2, 5);
91+
}
92+
93+
//TODO - this function is internal inside of PythonNet. It probably should be public.
94+
//PyObjectConversions.Reset();
95+
}
96+
}
97+
}

0 commit comments

Comments
 (0)