Skip to content

Commit b5cae6c

Browse files
committed
enabled an ability to override Python to .NET argument coversion using PyArgConverter attribute
1 parent c355bd6 commit b5cae6c

File tree

3 files changed

+134
-4
lines changed

3 files changed

+134
-4
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using System;
2+
using NUnit.Framework;
3+
using Python.Runtime;
4+
5+
namespace Python.EmbeddingTest
6+
{
7+
class TestCustomArgMarshal
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+
[Test]
22+
public void CustomArgMarshaller()
23+
{
24+
var obj = new CustomArgMarshaling();
25+
using (Py.GIL()) {
26+
dynamic callWithInt = PythonEngine.Eval("lambda o: o.CallWithInt('42')");
27+
callWithInt(obj.ToPython());
28+
}
29+
Assert.AreEqual(expected: 42, actual: obj.LastArgument);
30+
}
31+
}
32+
33+
[PyArgConverter(typeof(CustomArgConverter))]
34+
class CustomArgMarshaling {
35+
public object LastArgument { get; private set; }
36+
public void CallWithInt(int value) => this.LastArgument = value;
37+
}
38+
39+
class CustomArgConverter : DefaultPyArgumentConverter {
40+
public override bool TryConvertArgument(IntPtr pyarg, Type parameterType, bool needsResolution,
41+
out object arg, out bool isOut) {
42+
if (parameterType != typeof(int))
43+
return base.TryConvertArgument(pyarg, parameterType, needsResolution, out arg, out isOut);
44+
45+
bool isString = base.TryConvertArgument(pyarg, typeof(string), needsResolution,
46+
out arg, out isOut);
47+
if (!isString) return false;
48+
49+
int number;
50+
if (!int.TryParse((string)arg, out number)) return false;
51+
arg = number;
52+
return true;
53+
}
54+
}
55+
}

src/runtime/methodbinder.cs

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
using System;
22
using System.Collections;
3+
using System.Diagnostics;
34
using System.Reflection;
45
using System.Text;
56
using System.Collections.Generic;
67
using System.Linq;
78

89
namespace Python.Runtime
910
{
11+
using System.Linq;
12+
1013
/// <summary>
1114
/// A MethodBinder encapsulates information about a (possibly overloaded)
1215
/// managed method, and is responsible for selecting the right method given
@@ -19,16 +22,16 @@ internal class MethodBinder
1922
public MethodBase[] methods;
2023
public bool init = false;
2124
public bool allow_threads = true;
22-
readonly IPyArgumentConverter pyArgumentConverter = DefaultPyArgumentConverter.Instance;
25+
IPyArgumentConverter pyArgumentConverter;
2326

2427
internal MethodBinder()
2528
{
2629
list = new ArrayList();
2730
}
2831

29-
internal MethodBinder(MethodInfo mi)
32+
internal MethodBinder(MethodInfo mi): this()
3033
{
31-
list = new ArrayList { mi };
34+
this.AddMethod(mi);
3235
}
3336

3437
public int Count
@@ -38,6 +41,7 @@ public int Count
3841

3942
internal void AddMethod(MethodBase m)
4043
{
44+
Debug.Assert(!init);
4145
list.Add(m);
4246
}
4347

@@ -165,11 +169,40 @@ internal MethodBase[] GetMethods()
165169
// I'm sure this could be made more efficient.
166170
list.Sort(new MethodSorter());
167171
methods = (MethodBase[])list.ToArray(typeof(MethodBase));
172+
pyArgumentConverter = this.GetArgumentConverter();
168173
init = true;
169174
}
170175
return methods;
171176
}
172177

178+
IPyArgumentConverter GetArgumentConverter() {
179+
IPyArgumentConverter converter = null;
180+
Type converterType = null;
181+
foreach (MethodBase method in this.methods)
182+
{
183+
var attribute = method.DeclaringType?
184+
.GetCustomAttributes(typeof(PyArgConverterAttribute), inherit: false)
185+
.OfType<PyArgConverterAttribute>()
186+
.SingleOrDefault()
187+
?? method.DeclaringType?.Assembly
188+
.GetCustomAttributes(typeof(PyArgConverterAttribute), inherit: false)
189+
.OfType<PyArgConverterAttribute>()
190+
.SingleOrDefault();
191+
if (converterType == null)
192+
{
193+
if (attribute == null) continue;
194+
195+
converterType = attribute.ConverterType;
196+
converter = attribute.Converter;
197+
} else if (converterType != attribute?.ConverterType)
198+
{
199+
throw new NotSupportedException("All methods must have the same IPyArgumentConverter");
200+
}
201+
}
202+
203+
return converter ?? DefaultPyArgumentConverter.Instance;
204+
}
205+
173206
/// <summary>
174207
/// Precedence algorithm largely lifted from Jython - the concerns are
175208
/// generally the same so we'll start with this and tweak as necessary.

src/runtime/pyargconverter.cs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,14 @@ bool TryConvertArgument(IntPtr pyarg, Type parameterType,
2121
bool needsResolution, out object arg, out bool isOut);
2222
}
2323

24+
/// <summary>
25+
/// The implementation of <see cref="IPyArgumentConverter"/> used by default
26+
/// </summary>
2427
public class DefaultPyArgumentConverter: IPyArgumentConverter {
25-
public static DefaultPyArgumentConverter Instance { get; }= new DefaultPyArgumentConverter();
28+
/// <summary>
29+
/// Gets the singleton instance.
30+
/// </summary>
31+
public static DefaultPyArgumentConverter Instance { get; } = new DefaultPyArgumentConverter();
2632

2733
/// <summary>
2834
/// Attempts to convert an argument passed by Python to the specified parameter type.
@@ -44,4 +50,40 @@ public virtual bool TryConvertArgument(
4450
out arg, out isOut);
4551
}
4652
}
53+
54+
/// <summary>
55+
/// Specifies an argument converter to be used, when methods in this class/assembly are called from Python.
56+
/// </summary>
57+
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Struct)]
58+
public class PyArgConverterAttribute : Attribute
59+
{
60+
static readonly Type[] EmptyArgTypeList = new Type[0];
61+
static readonly object[] EmptyArgList = new object[0];
62+
63+
/// <summary>
64+
/// Gets the instance of the converter, that will be used when calling methods
65+
/// of this class/assembly from Python
66+
/// </summary>
67+
public IPyArgumentConverter Converter { get; }
68+
/// <summary>
69+
/// Gets the type of the converter, that will be used when calling methods
70+
/// of this class/assembly from Python
71+
/// </summary>
72+
public Type ConverterType { get; }
73+
74+
/// <summary>
75+
/// Specifies an argument converter to be used, when methods
76+
/// in this class/assembly are called from Python.
77+
/// </summary>
78+
/// <param name="converterType">Type of the converter to use.
79+
/// Must implement <see cref="IPyArgumentConverter"/>.</param>
80+
public PyArgConverterAttribute(Type converterType)
81+
{
82+
if (converterType == null) throw new ArgumentNullException(nameof(converterType));
83+
var ctor = converterType.GetConstructor(EmptyArgTypeList);
84+
if (ctor == null) throw new ArgumentException("Specified converter must have public parameterless constructor");
85+
this.Converter = (IPyArgumentConverter)ctor.Invoke(EmptyArgList);
86+
this.ConverterType = converterType;
87+
}
88+
}
4789
}

0 commit comments

Comments
 (0)