diff --git a/src/FunctionCodec.cs b/src/FunctionCodec.cs new file mode 100644 index 0000000..e69347b --- /dev/null +++ b/src/FunctionCodec.cs @@ -0,0 +1,185 @@ +using System; +using System.Reflection; + +namespace Python.Runtime.Codecs +{ + //converts python functions to C# actions + public class FunctionCodec : IPyObjectDecoder + { + private static int GetNumArgs(PyObject pyCallable) + { + var locals = new PyDict(); + locals.SetItem("f", pyCallable); + using (Py.GIL()) + PythonEngine.Exec(@" +from inspect import signature +try: + x = len(signature(f).parameters) +except: + x = 0 +", null, locals.Handle); + + var x = locals.GetItem("x"); + return new PyInt(x).ToInt32(); + } + + private static int GetNumArgs(Type targetType) + { + MethodInfo invokeMethod = targetType.GetMethod("Invoke"); + return invokeMethod.GetParameters().Length; + } + + private static bool IsUnaryAction(Type targetType) + { + return targetType == typeof(Action); + } + + private static bool IsVariadicObjectAction(Type targetType) + { + return targetType == typeof(Action); + } + + private static bool IsUnaryFunc(Type targetType) + { + return targetType == typeof(Func); + } + + private static bool IsVariadicObjectFunc(Type targetType) + { + return targetType == typeof(Func); + } + + private static bool IsAction(Type targetType) + { + return IsUnaryAction(targetType) || IsVariadicObjectAction(targetType); + } + + private static bool IsFunc(Type targetType) + { + return IsUnaryFunc(targetType) || IsVariadicObjectFunc(targetType); + } + + private static bool IsCallable(Type targetType) + { + //Python.Runtime.ClassManager dtype + return targetType.IsSubclassOf(typeof(MulticastDelegate)); + } + + public static FunctionCodec Instance { get; } = new FunctionCodec(); + public bool CanDecode(PyObject objectType, Type targetType) + { + //python object must be callable + if (!objectType.IsCallable()) return false; + + //C# object must be callable + if (!IsCallable(targetType)) + return false; + + return true; + } + + private static object ConvertUnaryAction(PyObject pyObj) + { + Func func = (Func)ConvertUnaryFunc(pyObj); + Action action = () => { func(); }; + return (object)action; + } + + private static object ConvertVariadicObjectAction(PyObject pyObj, int numArgs) + { + Func func = (Func)ConvertVariadicObjectFunc(pyObj, numArgs); + Action action = (object[] args) => { func(args); }; + return (object)action; + } + + //TODO share code between ConvertUnaryFunc and ConvertVariadicObjectFunc + private static object ConvertUnaryFunc(PyObject pyObj) + { + var pyAction = new PyObject(pyObj.Handle); + Func func = () => + { + var pyArgs = new PyObject[0]; + using (Py.GIL()) + { + var pyResult = pyAction.Invoke(pyArgs); + return pyResult.AsManagedObject(typeof(object)); + } + }; + return (object)func; + } + + private static object ConvertVariadicObjectFunc(PyObject pyObj, int numArgs) + { + var pyAction = new PyObject(pyObj.Handle); + Func func = (object[] o) => + { + var pyArgs = new PyObject[numArgs]; + int i = 0; + foreach (object obj in o) + { + pyArgs[i++] = obj.ToPython(); + } + + using (Py.GIL()) + { + var pyResult = pyAction.Invoke(pyArgs); + return pyResult.AsManagedObject(typeof(object)); + } + }; + return (object)func; + } + + public bool TryDecode(PyObject pyObj, out T value) + { + value = default(T); + var tT = typeof(T); + if (!IsCallable(tT)) + return false; + + var numArgs = GetNumArgs(pyObj); + if (numArgs != GetNumArgs(tT)) + return false; + + if (IsAction(tT)) + { + object actionObj = null; + if (numArgs == 0) + { + actionObj = ConvertUnaryAction(pyObj); + } + else + { + actionObj = ConvertVariadicObjectAction(pyObj, numArgs); + } + + value = (T)actionObj; + return true; + } + else if (IsFunc(tT)) + { + + object funcObj = null; + if (numArgs == 0) + { + funcObj = ConvertUnaryFunc(pyObj); + } + else + { + funcObj = ConvertVariadicObjectFunc(pyObj, numArgs); + } + + value = (T)funcObj; + return true; + } + else + { + return false; + } + } + + public static void Register() + { + PyObjectConversions.RegisterDecoder(Instance); + } + } +} diff --git a/tests/FunctionCodecTests.cs b/tests/FunctionCodecTests.cs new file mode 100644 index 0000000..a60eddf --- /dev/null +++ b/tests/FunctionCodecTests.cs @@ -0,0 +1,111 @@ +using System; +using NUnit.Framework; + +namespace Python.Runtime.Codecs +{ + class FunctionCodecTests + { + [SetUp] + public void SetUp() + { + TestsRuntimeConfig.Ensure(); + PythonEngine.Initialize(); + } + + + [TearDown] + public void Dispose() + { + PythonEngine.Shutdown(); + } + + [Test] + public void FunctionAction() + { + var codec = FunctionCodec.Instance; + + PyInt x = new PyInt(1); + PyDict y = new PyDict(); + //non-callables can't be decoded into Action + Assert.IsFalse(codec.CanDecode(x, typeof(Action))); + Assert.IsFalse(codec.CanDecode(y, typeof(Action))); + + var locals = new PyDict(); + PythonEngine.Exec(@" +def foo(): + return 1 +def bar(a): + return 2 +", null, locals.Handle); + + //foo, the function with no arguments + var fooFunc = locals.GetItem("foo"); + Assert.IsFalse(codec.CanDecode(fooFunc, typeof(bool))); + + //CanDecode does not work for variadic actions + //Assert.IsFalse(codec.CanDecode(fooFunc, typeof(Action))); + Assert.IsTrue(codec.CanDecode(fooFunc, typeof(Action))); + + Action fooAction; + Assert.IsTrue(codec.TryDecode(fooFunc, out fooAction)); + Assert.DoesNotThrow(() => fooAction()); + + //bar, the function with an argument + var barFunc = locals.GetItem("bar"); + Assert.IsFalse(codec.CanDecode(barFunc, typeof(bool))); + //Assert.IsFalse(codec.CanDecode(barFunc, typeof(Action))); + Assert.IsTrue(codec.CanDecode(barFunc, typeof(Action))); + + Action barAction; + Assert.IsTrue(codec.TryDecode(barFunc, out barAction)); + Assert.DoesNotThrow(() => barAction(new[] { (object)true })); + } + + [Test] + public void FunctionFunc() + { + var codec = FunctionCodec.Instance; + + PyInt x = new PyInt(1); + PyDict y = new PyDict(); + //non-callables can't be decoded into Func + Assert.IsFalse(codec.CanDecode(x, typeof(Func))); + Assert.IsFalse(codec.CanDecode(y, typeof(Func))); + + var locals = new PyDict(); + PythonEngine.Exec(@" +def foo(): + return 1 +def bar(a): + return 2 +", null, locals.Handle); + + //foo, the function with no arguments + var fooFunc = locals.GetItem("foo"); + Assert.IsFalse(codec.CanDecode(fooFunc, typeof(bool))); + + //CanDecode does not work for variadic actions + //Assert.IsFalse(codec.CanDecode(fooFunc, typeof(Func))); + Assert.IsTrue(codec.CanDecode(fooFunc, typeof(Func))); + + Func foo; + Assert.IsTrue(codec.TryDecode(fooFunc, out foo)); + object res1 = null; + Assert.DoesNotThrow(() => res1 = foo()); + Assert.AreEqual(res1, 1); + + //bar, the function with an argument + var barFunc = locals.GetItem("bar"); + Assert.IsFalse(codec.CanDecode(barFunc, typeof(bool))); + //Assert.IsFalse(codec.CanDecode(barFunc, typeof(Func))); + Assert.IsTrue(codec.CanDecode(barFunc, typeof(Func))); + + Func bar; + Assert.IsTrue(codec.TryDecode(barFunc, out bar)); + object res2 = null; + Assert.DoesNotThrow(() => res2 = bar(new[] { (object)true })); + Assert.AreEqual(res2, 2); + } + + } +} diff --git a/tests/TestCallbacks.cs b/tests/TestCallbacks.cs new file mode 100644 index 0000000..dec68ac --- /dev/null +++ b/tests/TestCallbacks.cs @@ -0,0 +1,98 @@ +using System; +using NUnit.Framework; + +namespace Python.Runtime.Codecs +{ + public class TestCallbacks + { + [OneTimeSetUp] + public void SetUp() + { + TestsRuntimeConfig.Ensure(); + PythonEngine.Initialize(); + } + + [OneTimeTearDown] + public void Dispose() + { + PythonEngine.Shutdown(); + } + + private class Callables + { + internal object CallFunction0(Func func) + { + return func(); + } + + internal object CallFunction1(Func func, object arg) + { + return func(new[] { arg }); + } + + internal void CallAction0(Action func) + { + func(); + } + + internal void CallAction1(Action func, object arg) + { + func(new[] { arg }); + } + } + + [Test] + public void TestPythonFunctionPassedIntoCLRMethod() + { + var locals = new PyDict(); + PythonEngine.Exec(@" +def ret_1(): + return 1 +def str_len(a): + return len(a) +", null, locals.Handle); + + var ret1 = locals.GetItem("ret_1"); + var strLen = locals.GetItem("str_len"); + + var callables = new Callables(); + + FunctionCodec.Register(); + + //ret1. A function with no arguments that returns an integer + //it must be convertible to Action or Func and not to Func + { + Action result1 = null; + Func result2 = null; + Assert.DoesNotThrow(() => { result1 = ret1.As(); }); + Assert.DoesNotThrow(() => { result2 = ret1.As>(); }); + + Assert.DoesNotThrow(() => { callables.CallAction0((Action)result1); }); + object ret2 = null; + Assert.DoesNotThrow(() => { ret2 = callables.CallFunction0((Func)result2); }); + Assert.AreEqual(ret2, 1); + } + + //strLen. A function that takes something with a __len__ and returns the result of that function + //It must be convertible to an Action and Func) and not to an Action or Func + { + Action result3 = null; + Func result4 = null; + Assert.DoesNotThrow(() => { result3 = strLen.As>(); }); + Assert.DoesNotThrow(() => { result4 = strLen.As>(); }); + + //try using both func and action to show you can get __len__ of a string but not an integer + Assert.Throws(() => { callables.CallAction1((Action)result3, 2); }); + Assert.DoesNotThrow(() => { callables.CallAction1((Action)result3, "hello"); }); + Assert.Throws(() => { callables.CallFunction1((Func)result4, 2); }); + + object ret2 = null; + Assert.DoesNotThrow(() => { ret2 = callables.CallFunction1((Func)result4, "hello"); }); + Assert.AreEqual(ret2, 5); + } + + //TODO - this function is internal inside of PythonNet. It probably should be public. + //PyObjectConversions.Reset(); + } + } +}