Skip to content

Commit ce14424

Browse files
dsuarezvdenfromufa
authored and
denfromufa
committed
Implement named arguments and With semantics in C# embedding side (#461)
* Added python "with" construction * Added some unit tests for new With method * Renamed With tests for easier grouping * Support for named arguments to invoke python methods * Named argument tests cosmetic changed * Fixed failing test in python 2.7 * Reset line endings in csproj to LF
1 parent d9a3666 commit ce14424

File tree

5 files changed

+245
-20
lines changed

5 files changed

+245
-20
lines changed

src/embed_tests/Python.EmbeddingTest.csproj

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<?xml version="1.0" encoding="utf-8"?>
1+
<?xml version="1.0" encoding="utf-8"?>
22
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
33
<PropertyGroup>
44
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@@ -97,6 +97,8 @@
9797
<Compile Include="TestPythonException.cs" />
9898
<Compile Include="TestPythonEngineProperties.cs" />
9999
<Compile Include="TestPyTuple.cs" />
100+
<Compile Include="TestNamedArguments.cs" />
101+
<Compile Include="TestPyWith.cs" />
100102
<Compile Include="TestRuntime.cs" />
101103
</ItemGroup>
102104
<ItemGroup>
@@ -117,4 +119,4 @@
117119
<Copy SourceFiles="$(TargetAssembly)" DestinationFolder="$(PythonBuildDir)" />
118120
<!--Copy SourceFiles="$(TargetAssemblyPdb)" Condition="Exists('$(TargetAssemblyPdb)')" DestinationFolder="$(PythonBuildDir)" /-->
119121
</Target>
120-
</Project>
122+
</Project>

src/embed_tests/TestNamedArguments.cs

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using System;
2+
using NUnit.Framework;
3+
using Python.Runtime;
4+
5+
namespace Python.EmbeddingTest
6+
{
7+
public class TestNamedArguments
8+
{
9+
[OneTimeSetUp]
10+
public void SetUp()
11+
{
12+
PythonEngine.Initialize();
13+
}
14+
15+
[OneTimeTearDown]
16+
public void Dispose()
17+
{
18+
PythonEngine.Shutdown();
19+
}
20+
21+
/// <summary>
22+
/// Test named arguments support through Py.kw method
23+
/// </summary>
24+
[Test]
25+
public void TestKeywordArgs()
26+
{
27+
dynamic a = CreateTestClass();
28+
var result = (int)a.Test3(2, Py.kw("a4", 8));
29+
30+
Assert.AreEqual(12, result);
31+
}
32+
33+
34+
/// <summary>
35+
/// Test keyword arguments with .net named arguments
36+
/// </summary>
37+
[Test]
38+
public void TestNamedArgs()
39+
{
40+
dynamic a = CreateTestClass();
41+
var result = (int)a.Test3(2, a4: 8);
42+
43+
Assert.AreEqual(12, result);
44+
}
45+
46+
47+
48+
private static PyObject CreateTestClass()
49+
{
50+
var locals = new PyDict();
51+
52+
PythonEngine.Exec(@"
53+
class cmTest3:
54+
def Test3(self, a1 = 1, a2 = 1, a3 = 1, a4 = 1):
55+
return a1 + a2 + a3 + a4
56+
57+
a = cmTest3()
58+
", null, locals.Handle);
59+
60+
return locals.GetItem("a");
61+
}
62+
63+
}
64+
}

src/embed_tests/TestPyWith.cs

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using System;
2+
using NUnit.Framework;
3+
using Python.Runtime;
4+
5+
namespace Python.EmbeddingTest
6+
{
7+
public class TestPyWith
8+
{
9+
[OneTimeSetUp]
10+
public void SetUp()
11+
{
12+
PythonEngine.Initialize();
13+
}
14+
15+
[OneTimeTearDown]
16+
public void Dispose()
17+
{
18+
PythonEngine.Shutdown();
19+
}
20+
21+
/// <summary>
22+
/// Test that exception is raised in context manager that ignores it.
23+
/// </summary>
24+
[Test]
25+
public void TestWithPositive()
26+
{
27+
var locals = new PyDict();
28+
29+
PythonEngine.Exec(@"
30+
class CmTest:
31+
def __enter__(self):
32+
print('Enter')
33+
return self
34+
def __exit__(self, t, v, tb):
35+
# Exception not handled, return will be False
36+
print('Exit')
37+
def fail(self):
38+
return 5 / 0
39+
40+
a = CmTest()
41+
", null, locals.Handle);
42+
43+
var a = locals.GetItem("a");
44+
45+
try
46+
{
47+
Py.With(a, cmTest =>
48+
{
49+
cmTest.fail();
50+
});
51+
}
52+
catch (PythonException e)
53+
{
54+
Assert.IsTrue(e.Message.Contains("ZeroDivisionError"));
55+
}
56+
}
57+
58+
59+
/// <summary>
60+
/// Test that exception is not raised in context manager that handles it
61+
/// </summary>
62+
[Test]
63+
public void TestWithNegative()
64+
{
65+
var locals = new PyDict();
66+
67+
PythonEngine.Exec(@"
68+
class CmTest:
69+
def __enter__(self):
70+
print('Enter')
71+
return self
72+
def __exit__(self, t, v, tb):
73+
# Signal exception is handled by returning true
74+
return True
75+
def fail(self):
76+
return 5 / 0
77+
78+
a = CmTest()
79+
", null, locals.Handle);
80+
81+
var a = locals.GetItem("a");
82+
Py.With(a, cmTest =>
83+
{
84+
cmTest.fail();
85+
});
86+
}
87+
}
88+
}

src/runtime/pyobject.cs

+59-17
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections;
33
using System.Dynamic;
44
using System.Linq.Expressions;
@@ -915,6 +915,34 @@ public override bool TrySetMember(SetMemberBinder binder, object value)
915915
return true;
916916
}
917917

918+
private void GetArgs(object[] inargs, CallInfo callInfo, out PyTuple args, out PyDict kwargs)
919+
{
920+
if (callInfo == null || callInfo.ArgumentNames.Count == 0)
921+
{
922+
GetArgs(inargs, out args, out kwargs);
923+
return;
924+
}
925+
926+
// Support for .net named arguments
927+
var namedArgumentCount = callInfo.ArgumentNames.Count;
928+
var regularArgumentCount = callInfo.ArgumentCount - namedArgumentCount;
929+
930+
var argTuple = Runtime.PyTuple_New(regularArgumentCount);
931+
for (int i = 0; i < regularArgumentCount; ++i)
932+
{
933+
AddArgument(argTuple, i, inargs[i]);
934+
}
935+
args = new PyTuple(argTuple);
936+
937+
var namedArgs = new object[namedArgumentCount * 2];
938+
for (int i = 0; i < namedArgumentCount; ++i)
939+
{
940+
namedArgs[i * 2] = callInfo.ArgumentNames[i];
941+
namedArgs[i * 2 + 1] = inargs[regularArgumentCount + i];
942+
}
943+
kwargs = Py.kw(namedArgs);
944+
}
945+
918946
private void GetArgs(object[] inargs, out PyTuple args, out PyDict kwargs)
919947
{
920948
int arg_count;
@@ -925,22 +953,10 @@ private void GetArgs(object[] inargs, out PyTuple args, out PyDict kwargs)
925953
IntPtr argtuple = Runtime.PyTuple_New(arg_count);
926954
for (var i = 0; i < arg_count; i++)
927955
{
928-
IntPtr ptr;
929-
if (inargs[i] is PyObject)
930-
{
931-
ptr = ((PyObject)inargs[i]).Handle;
932-
Runtime.XIncref(ptr);
933-
}
934-
else
935-
{
936-
ptr = Converter.ToPython(inargs[i], inargs[i]?.GetType());
937-
}
938-
if (Runtime.PyTuple_SetItem(argtuple, i, ptr) < 0)
939-
{
940-
throw new PythonException();
941-
}
956+
AddArgument(argtuple, i, inargs[i]);
942957
}
943958
args = new PyTuple(argtuple);
959+
944960
kwargs = null;
945961
for (int i = arg_count; i < inargs.Length; i++)
946962
{
@@ -959,6 +975,32 @@ private void GetArgs(object[] inargs, out PyTuple args, out PyDict kwargs)
959975
}
960976
}
961977

978+
private static void AddArgument(IntPtr argtuple, int i, object target)
979+
{
980+
IntPtr ptr = GetPythonObject(target);
981+
982+
if (Runtime.PyTuple_SetItem(argtuple, i, ptr) < 0)
983+
{
984+
throw new PythonException();
985+
}
986+
}
987+
988+
private static IntPtr GetPythonObject(object target)
989+
{
990+
IntPtr ptr;
991+
if (target is PyObject)
992+
{
993+
ptr = ((PyObject)target).Handle;
994+
Runtime.XIncref(ptr);
995+
}
996+
else
997+
{
998+
ptr = Converter.ToPython(target, target?.GetType());
999+
}
1000+
1001+
return ptr;
1002+
}
1003+
9621004
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
9631005
{
9641006
if (this.HasAttr(binder.Name) && this.GetAttr(binder.Name).IsCallable())
@@ -967,7 +1009,7 @@ public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, o
9671009
PyDict kwargs = null;
9681010
try
9691011
{
970-
GetArgs(args, out pyargs, out kwargs);
1012+
GetArgs(args, binder.CallInfo, out pyargs, out kwargs);
9711013
result = CheckNone(InvokeMethod(binder.Name, pyargs, kwargs));
9721014
}
9731015
finally
@@ -997,7 +1039,7 @@ public override bool TryInvoke(InvokeBinder binder, object[] args, out object re
9971039
PyDict kwargs = null;
9981040
try
9991041
{
1000-
GetArgs(args, out pyargs, out kwargs);
1042+
GetArgs(args, binder.CallInfo, out pyargs, out kwargs);
10011043
result = CheckNone(Invoke(pyargs, kwargs));
10021044
}
10031045
finally

src/runtime/pythonengine.cs

+30-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.IO;
44
using System.Linq;
@@ -632,5 +632,34 @@ public static void SetArgv(IEnumerable<string> argv)
632632
Runtime.CheckExceptionOccurred();
633633
}
634634
}
635+
636+
public static void With(PyObject obj, Action<dynamic> Body)
637+
{
638+
// Behavior described here:
639+
// https://docs.python.org/2/reference/datamodel.html#with-statement-context-managers
640+
641+
IntPtr type = Runtime.PyNone;
642+
IntPtr val = Runtime.PyNone;
643+
IntPtr traceBack = Runtime.PyNone;
644+
PythonException ex = null;
645+
646+
try
647+
{
648+
PyObject enterResult = obj.InvokeMethod("__enter__");
649+
650+
Body(enterResult);
651+
}
652+
catch (PythonException e)
653+
{
654+
ex = e;
655+
type = ex.PyType;
656+
val = ex.PyValue;
657+
traceBack = ex.PyTB;
658+
}
659+
660+
var exitResult = obj.InvokeMethod("__exit__", new PyObject(type), new PyObject(val), new PyObject(traceBack));
661+
662+
if (ex != null && !exitResult.IsTrue()) throw ex;
663+
}
635664
}
636665
}

0 commit comments

Comments
 (0)