From 73c40fc6d484acaed68514955963a5512d086739 Mon Sep 17 00:00:00 2001 From: David Suarez Date: Sat, 6 May 2017 16:14:02 +0200 Subject: [PATCH 1/7] Added python "with" construction --- src/embed_tests/TestPyWith.cs | 88 +++++++++++++++++++++++++++++++++++ src/runtime/pythonengine.cs | 31 +++++++++++- 2 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 src/embed_tests/TestPyWith.cs diff --git a/src/embed_tests/TestPyWith.cs b/src/embed_tests/TestPyWith.cs new file mode 100644 index 000000000..8d0e1fb0f --- /dev/null +++ b/src/embed_tests/TestPyWith.cs @@ -0,0 +1,88 @@ +using System; +using NUnit.Framework; +using Python.Runtime; + +namespace Python.EmbeddingTest +{ + public class TestPyWith + { + [OneTimeSetUp] + public void SetUp() + { + PythonEngine.Initialize(); + } + + [OneTimeTearDown] + public void Dispose() + { + PythonEngine.Shutdown(); + } + + /// + /// Test that exception is raised in context manager that ignores it. + /// + [Test] + public void TestPositiveWith() + { + var locals = new PyDict(); + + PythonEngine.Exec(@" +class cmTest: + def __enter__(self): + print('Enter') + return self + def __exit__(self, t, v, tb): + # Exception not handled, return will be False + print('Exit') + def fail(self): + return 5 / 0 + +a = cmTest() +", null, locals.Handle); + + var a = locals.GetItem("a"); + + try + { + Py.With(a, cmTest => + { + cmTest.fail(); + }); + } + catch (PythonException e) + { + Assert.IsTrue(e.Message.Contains("division by zero")); + } + } + + + /// + /// Test that exception is not raised in context manager that handles it + /// + [Test] + public void TestNegativeWith() + { + var locals = new PyDict(); + + PythonEngine.Exec(@" +class cmTest: + def __enter__(self): + print('Enter') + return self + def __exit__(self, t, v, tb): + # Signal exception is handled by returning true + return True + def fail(self): + return 5 / 0 + +a = cmTest() +", null, locals.Handle); + + var a = locals.GetItem("a"); + Py.With(a, cmTest => + { + cmTest.fail(); + }); + } + } +} diff --git a/src/runtime/pythonengine.cs b/src/runtime/pythonengine.cs index 1fd3b239a..9ddd85da6 100644 --- a/src/runtime/pythonengine.cs +++ b/src/runtime/pythonengine.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -632,5 +632,34 @@ public static void SetArgv(IEnumerable argv) Runtime.CheckExceptionOccurred(); } } + + public static void With(PyObject obj, Action Body) + { + // Behavior described here: + // https://docs.python.org/2/reference/datamodel.html#with-statement-context-managers + + IntPtr type = Runtime.PyNone; + IntPtr val = Runtime.PyNone; + IntPtr traceBack = Runtime.PyNone; + PythonException ex = null; + + try + { + PyObject enterResult = obj.InvokeMethod("__enter__"); + + Body(enterResult); + } + catch (PythonException e) + { + ex = e; + type = ex.PyType; + val = ex.PyValue; + traceBack = ex.PyTB; + } + + var exitResult = obj.InvokeMethod("__exit__", new PyObject(type), new PyObject(val), new PyObject(traceBack)); + + if (ex != null && !exitResult.IsTrue()) throw ex; + } } } From 500d3d37a05e77b9f24f00431e5a8b9d03b77b26 Mon Sep 17 00:00:00 2001 From: David Suarez Date: Sat, 6 May 2017 16:18:05 +0200 Subject: [PATCH 2/7] Added some unit tests for new With method --- src/embed_tests/Python.EmbeddingTest.csproj | 241 ++++++++++---------- 1 file changed, 121 insertions(+), 120 deletions(-) diff --git a/src/embed_tests/Python.EmbeddingTest.csproj b/src/embed_tests/Python.EmbeddingTest.csproj index 3ec0d4b57..a665b2e29 100644 --- a/src/embed_tests/Python.EmbeddingTest.csproj +++ b/src/embed_tests/Python.EmbeddingTest.csproj @@ -1,120 +1,121 @@ - - - - Debug - AnyCPU - {4165C59D-2822-499F-A6DB-EACA4C331EB5} - Library - Python.EmbeddingTest - Python.EmbeddingTest - bin\Python.EmbeddingTest.xml - bin\ - v4.0 - - 1591 - ..\..\ - $(SolutionDir)\bin\ - 6 - true - prompt - - - x86 - - - x64 - - - true - DEBUG;TRACE - full - - - - - true - pdbonly - - - true - DEBUG;TRACE - full - - - - - true - pdbonly - - - true - DEBUG;TRACE - full - - - - - true - pdbonly - - - true - DEBUG;TRACE - full - - - - - true - pdbonly - - - - - ..\..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {097B4AC0-74E9-4C58-BCF8-C69746EC8271} - Python.Runtime - - - - - - - - $(TargetPath) - $(TargetDir)$(TargetName).pdb - - - - - - + + + + Debug + AnyCPU + {4165C59D-2822-499F-A6DB-EACA4C331EB5} + Library + Python.EmbeddingTest + Python.EmbeddingTest + bin\Python.EmbeddingTest.xml + bin\ + v4.0 + + 1591 + ..\..\ + $(SolutionDir)\bin\ + 6 + true + prompt + + + x86 + + + x64 + + + true + DEBUG;TRACE + full + + + + + true + pdbonly + + + true + DEBUG;TRACE + full + + + + + true + pdbonly + + + true + DEBUG;TRACE + full + + + + + true + pdbonly + + + true + DEBUG;TRACE + full + + + + + true + pdbonly + + + + + ..\..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {097B4AC0-74E9-4C58-BCF8-C69746EC8271} + Python.Runtime + + + + + + + + $(TargetPath) + $(TargetDir)$(TargetName).pdb + + + + + + \ No newline at end of file From 35a7dc648841d543fef88e11d8206cb541475980 Mon Sep 17 00:00:00 2001 From: David Suarez Date: Sat, 6 May 2017 16:21:03 +0200 Subject: [PATCH 3/7] Renamed With tests for easier grouping --- src/embed_tests/TestPyWith.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/embed_tests/TestPyWith.cs b/src/embed_tests/TestPyWith.cs index 8d0e1fb0f..a56299b65 100644 --- a/src/embed_tests/TestPyWith.cs +++ b/src/embed_tests/TestPyWith.cs @@ -22,7 +22,7 @@ public void Dispose() /// Test that exception is raised in context manager that ignores it. /// [Test] - public void TestPositiveWith() + public void TestWithPositive() { var locals = new PyDict(); @@ -60,7 +60,7 @@ def fail(self): /// Test that exception is not raised in context manager that handles it /// [Test] - public void TestNegativeWith() + public void TestWithNegative() { var locals = new PyDict(); From c3c1c5643e75fffd9a6b8911ade126d9564509f4 Mon Sep 17 00:00:00 2001 From: David Suarez Date: Sat, 6 May 2017 16:57:36 +0200 Subject: [PATCH 4/7] Support for named arguments to invoke python methods --- src/embed_tests/Python.EmbeddingTest.csproj | 1 + src/embed_tests/TestNamedArguments.cs | 65 ++++++++++++++++++ src/runtime/pyobject.cs | 76 ++++++++++++++++----- 3 files changed, 125 insertions(+), 17 deletions(-) create mode 100644 src/embed_tests/TestNamedArguments.cs diff --git a/src/embed_tests/Python.EmbeddingTest.csproj b/src/embed_tests/Python.EmbeddingTest.csproj index a665b2e29..35340b137 100644 --- a/src/embed_tests/Python.EmbeddingTest.csproj +++ b/src/embed_tests/Python.EmbeddingTest.csproj @@ -97,6 +97,7 @@ + diff --git a/src/embed_tests/TestNamedArguments.cs b/src/embed_tests/TestNamedArguments.cs new file mode 100644 index 000000000..7408cdd88 --- /dev/null +++ b/src/embed_tests/TestNamedArguments.cs @@ -0,0 +1,65 @@ +using System; +using NUnit.Framework; +using Python.Runtime; + +namespace Python.EmbeddingTest +{ + public class TestNamedArguments + { + [OneTimeSetUp] + public void SetUp() + { + PythonEngine.Initialize(); + } + + [OneTimeTearDown] + public void Dispose() + { + PythonEngine.Shutdown(); + } + + /// + /// Test named arguments support + /// + [Test] + public void TestKeywordArgs() + { + dynamic a = CreateTestClass(); + var result = (int)a.Test3(2, Py.kw("a4", 8)); + + Assert.AreEqual(12, result); + } + + + /// + /// Test named arguments support + /// + [Test] + public void TestNamedArgs() + { + dynamic a = CreateTestClass(); + var result = (int)a.Test3(2, a4: 8); + + Assert.AreEqual(12, result); + } + + + + private static dynamic CreateTestClass() + { + var locals = new PyDict(); + + PythonEngine.Exec(@" +class cmTest3: + def Test3(self, a1 = 1, a2 = 1, a3 = 1, a4 = 1): + return a1 + a2 + a3 + a4 + +a = cmTest3() +", null, locals.Handle); + + dynamic a = locals.GetItem("a"); + return a; + } + + } +} diff --git a/src/runtime/pyobject.cs b/src/runtime/pyobject.cs index 47f413409..80704c59e 100644 --- a/src/runtime/pyobject.cs +++ b/src/runtime/pyobject.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Dynamic; using System.Linq.Expressions; @@ -915,6 +915,34 @@ public override bool TrySetMember(SetMemberBinder binder, object value) return true; } + private void GetArgs(object[] inargs, CallInfo callInfo, out PyTuple args, out PyDict kwargs) + { + if (callInfo == null || callInfo.ArgumentNames.Count == 0) + { + GetArgs(inargs, out args, out kwargs); + return; + } + + // Support for .net named arguments + var namedArgumentCount = callInfo.ArgumentNames.Count; + var regularArgumentCount = callInfo.ArgumentCount - namedArgumentCount; + + var argTuple = Runtime.PyTuple_New(regularArgumentCount); + for (int i = 0; i < regularArgumentCount; ++i) + { + AddArgument(argTuple, i, inargs[i]); + } + args = new PyTuple(argTuple); + + var namedArgs = new object[namedArgumentCount * 2]; + for (int i = 0; i < namedArgumentCount; ++i) + { + namedArgs[i * 2] = callInfo.ArgumentNames[i]; + namedArgs[i * 2 + 1] = inargs[regularArgumentCount + i]; + } + kwargs = Py.kw(namedArgs); + } + private void GetArgs(object[] inargs, out PyTuple args, out PyDict kwargs) { int arg_count; @@ -925,22 +953,10 @@ private void GetArgs(object[] inargs, out PyTuple args, out PyDict kwargs) IntPtr argtuple = Runtime.PyTuple_New(arg_count); for (var i = 0; i < arg_count; i++) { - IntPtr ptr; - if (inargs[i] is PyObject) - { - ptr = ((PyObject)inargs[i]).Handle; - Runtime.XIncref(ptr); - } - else - { - ptr = Converter.ToPython(inargs[i], inargs[i]?.GetType()); - } - if (Runtime.PyTuple_SetItem(argtuple, i, ptr) < 0) - { - throw new PythonException(); - } + AddArgument(argtuple, i, inargs[i]); } args = new PyTuple(argtuple); + kwargs = null; for (int i = arg_count; i < inargs.Length; i++) { @@ -959,6 +975,32 @@ private void GetArgs(object[] inargs, out PyTuple args, out PyDict kwargs) } } + private static void AddArgument(IntPtr argtuple, int i, object target) + { + IntPtr ptr = GetPythonObject(target); + + if (Runtime.PyTuple_SetItem(argtuple, i, ptr) < 0) + { + throw new PythonException(); + } + } + + private static IntPtr GetPythonObject(object target) + { + IntPtr ptr; + if (target is PyObject) + { + ptr = ((PyObject)target).Handle; + Runtime.XIncref(ptr); + } + else + { + ptr = Converter.ToPython(target, target?.GetType()); + } + + return ptr; + } + public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { if (this.HasAttr(binder.Name) && this.GetAttr(binder.Name).IsCallable()) @@ -967,7 +1009,7 @@ public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, o PyDict kwargs = null; try { - GetArgs(args, out pyargs, out kwargs); + GetArgs(args, binder.CallInfo, out pyargs, out kwargs); result = CheckNone(InvokeMethod(binder.Name, pyargs, kwargs)); } finally @@ -997,7 +1039,7 @@ public override bool TryInvoke(InvokeBinder binder, object[] args, out object re PyDict kwargs = null; try { - GetArgs(args, out pyargs, out kwargs); + GetArgs(args, binder.CallInfo, out pyargs, out kwargs); result = CheckNone(Invoke(pyargs, kwargs)); } finally From 6c27e84a988863152f2b2fcaf433f8a0465c4141 Mon Sep 17 00:00:00 2001 From: David Suarez Date: Sat, 6 May 2017 17:01:08 +0200 Subject: [PATCH 5/7] Named argument tests cosmetic changed --- src/embed_tests/TestNamedArguments.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/embed_tests/TestNamedArguments.cs b/src/embed_tests/TestNamedArguments.cs index 7408cdd88..1d7076956 100644 --- a/src/embed_tests/TestNamedArguments.cs +++ b/src/embed_tests/TestNamedArguments.cs @@ -19,7 +19,7 @@ public void Dispose() } /// - /// Test named arguments support + /// Test named arguments support through Py.kw method /// [Test] public void TestKeywordArgs() @@ -32,7 +32,7 @@ public void TestKeywordArgs() /// - /// Test named arguments support + /// Test keyword arguments with .net named arguments /// [Test] public void TestNamedArgs() @@ -45,7 +45,7 @@ public void TestNamedArgs() - private static dynamic CreateTestClass() + private static PyObject CreateTestClass() { var locals = new PyDict(); @@ -57,8 +57,7 @@ def Test3(self, a1 = 1, a2 = 1, a3 = 1, a4 = 1): a = cmTest3() ", null, locals.Handle); - dynamic a = locals.GetItem("a"); - return a; + return locals.GetItem("a"); } } From 66864add513720fd43dedd2cf9ab61f38212fe5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Su=C3=A1rez?= Date: Thu, 18 May 2017 19:25:33 +0200 Subject: [PATCH 6/7] Fixed failing test in python 2.7 --- src/embed_tests/TestPyWith.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/embed_tests/TestPyWith.cs b/src/embed_tests/TestPyWith.cs index a56299b65..fd3f8e662 100644 --- a/src/embed_tests/TestPyWith.cs +++ b/src/embed_tests/TestPyWith.cs @@ -27,7 +27,7 @@ public void TestWithPositive() var locals = new PyDict(); PythonEngine.Exec(@" -class cmTest: +class CmTest: def __enter__(self): print('Enter') return self @@ -37,7 +37,7 @@ def __exit__(self, t, v, tb): def fail(self): return 5 / 0 -a = cmTest() +a = CmTest() ", null, locals.Handle); var a = locals.GetItem("a"); @@ -51,7 +51,7 @@ def fail(self): } catch (PythonException e) { - Assert.IsTrue(e.Message.Contains("division by zero")); + Assert.IsTrue(e.Message.Contains("ZeroDivisionError")); } } @@ -65,7 +65,7 @@ public void TestWithNegative() var locals = new PyDict(); PythonEngine.Exec(@" -class cmTest: +class CmTest: def __enter__(self): print('Enter') return self @@ -75,7 +75,7 @@ def __exit__(self, t, v, tb): def fail(self): return 5 / 0 -a = cmTest() +a = CmTest() ", null, locals.Handle); var a = locals.GetItem("a"); From 93b1bda3533e71690ea5225abf7a6e2f9a121fb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Su=C3=A1rez?= Date: Thu, 18 May 2017 19:45:38 +0200 Subject: [PATCH 7/7] Reset line endings in csproj to LF --- src/embed_tests/Python.EmbeddingTest.csproj | 242 ++++++++++---------- 1 file changed, 121 insertions(+), 121 deletions(-) diff --git a/src/embed_tests/Python.EmbeddingTest.csproj b/src/embed_tests/Python.EmbeddingTest.csproj index 35340b137..46d93bf58 100644 --- a/src/embed_tests/Python.EmbeddingTest.csproj +++ b/src/embed_tests/Python.EmbeddingTest.csproj @@ -1,122 +1,122 @@ - - - - Debug - AnyCPU - {4165C59D-2822-499F-A6DB-EACA4C331EB5} - Library - Python.EmbeddingTest - Python.EmbeddingTest - bin\Python.EmbeddingTest.xml - bin\ - v4.0 - - 1591 - ..\..\ - $(SolutionDir)\bin\ - 6 - true - prompt - - - x86 - - - x64 - - - true - DEBUG;TRACE - full - - - - - true - pdbonly - - - true - DEBUG;TRACE - full - - - - - true - pdbonly - - - true - DEBUG;TRACE - full - - - - - true - pdbonly - - - true - DEBUG;TRACE - full - - - - - true - pdbonly - - - - - ..\..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {097B4AC0-74E9-4C58-BCF8-C69746EC8271} - Python.Runtime - - - - - - - - $(TargetPath) - $(TargetDir)$(TargetName).pdb - - - - - + + + + Debug + AnyCPU + {4165C59D-2822-499F-A6DB-EACA4C331EB5} + Library + Python.EmbeddingTest + Python.EmbeddingTest + bin\Python.EmbeddingTest.xml + bin\ + v4.0 + + 1591 + ..\..\ + $(SolutionDir)\bin\ + 6 + true + prompt + + + x86 + + + x64 + + + true + DEBUG;TRACE + full + + + + + true + pdbonly + + + true + DEBUG;TRACE + full + + + + + true + pdbonly + + + true + DEBUG;TRACE + full + + + + + true + pdbonly + + + true + DEBUG;TRACE + full + + + + + true + pdbonly + + + + + ..\..\packages\NUnit.3.6.0\lib\net40\nunit.framework.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {097B4AC0-74E9-4C58-BCF8-C69746EC8271} + Python.Runtime + + + + + + + + $(TargetPath) + $(TargetDir)$(TargetName).pdb + + + + + \ No newline at end of file