8000 enable expanding set of marshaling conversions via PyObjectConversions · losttech/pythonnet@c0ffc29 · GitHub
[go: up one dir, main page]

Skip to content

Commit c0ffc29

Browse files
committed
enable expanding set of marshaling conversions via PyObjectConversions
added sample TupleCodec (only supporting ValueTuple)
1 parent fe61a0a commit c0ffc29

File tree

6 files changed

+389
-7
lines changed

6 files changed

+389
-7
lines changed

src/embed_tests/Codecs.cs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
namespace Python.EmbeddingTest {
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Text;
5+
using NUnit.Framework;
6+
using Python.Runtime;
7+
using Python.Runtime.Codecs;
8+
9+
public class Codecs {
10+
[SetUp]
11+
public void SetUp() {
12+
PythonEngine.Initialize();
13+
}
14+
15+
[TearDown]
16+
public void Dispose() {
17+
PythonEngine.Shutdown();
18+
}
19+
20+
[Test]
21+
public void ConversionsGeneric() {
22+
ConversionsGeneric<ValueTuple<int, string, object>, ValueTuple>();
23+
}
24+
25+
static void ConversionsGeneric<T, TTuple>() {
26+
TupleCodec<TTuple>.Register();
27+
var tuple = Activator.CreateInstance(typeof(T), 42, "42", new object());
28+
T restored = default;
29+
using (Py.GIL())
30+
using (var scope = Py.CreateScope()) {
31+
void Accept(T value) => restored = value;
32+
var accept = new Action<T>(Accept).ToPython();
33+
scope.Set(nameof(tuple), tuple);
34+
scope.Set(nameof(accept), accept);
35+
scope.Exec($"{nameof(accept)}({nameof(tuple)})");
36+
Assert.AreEqual(expected: tuple, actual: restored);
37+
}
38+
}
39+
40+
[Test]
41+
public void ConversionsObject() {
42+
ConversionsGeneric<ValueTuple<int, string, object>, ValueTuple>();
43+
}
44+
static void ConversionsObject<T, TTuple>() {
45+
TupleCodec<TTuple>.Register();
46+
var tuple = Activator.CreateInstance(typeof(T), 42, "42", new object());
47+
T restored = default;
48+
using (Py.GIL())
49+
using (var scope = Py.CreateScope()) {
50+
void Accept(object value) => restored = (T)value;
51+
var accept = new Action<object>(Accept).ToPython();
52+
scope.Set(nameof(tuple), tuple);
53+
scope.Set(nameof(accept), accept);
54+
scope.Exec($"{nameof(accept)}({nameof(tuple)})");
55+
Assert.AreEqual(expected: tuple, actual: restored);
56+
}
57+
}
58+
59+
[Test]
60+
public void TupleRoundtripObject() {
61+
TupleRoundtripObject<ValueTuple<int, string, object>, ValueTuple>();
62+
}
63+
static void TupleRoundtripObject<T, TTuple>() {
64+
var tuple = Activator.CreateInstance(typeof(T), 42, "42", new object());
65+
using (Py.GIL()) {
66+
var pyTuple = TupleCodec<TTuple>.Instance.TryEncode(tuple);
67+
Assert.IsTrue(TupleCodec<TTuple>.Instance.TryDecode(pyTuple, out object restored));
68+
Assert.AreEqual(expected: tuple, actual: restored);
69+
}
70+
}
71+
72+
[Test]
73+
public void TupleRoundtripGeneric() {
74+
TupleRoundtripGeneric<ValueTuple<int, string, object>, ValueTuple>();
75+
}
76+
77+
static void TupleRoundtripGeneric<T, TTuple>() {
78+
var tuple = Activator.CreateInstance(typeof(T), 42, "42", new object());
79+
using (Py.GIL()) {
80+
var pyTuple = TupleCodec<TTuple>.Instance.TryEncode(tuple);
81+
Assert.IsTrue(TupleCodec<TTuple>.Instance.TryDecode(pyTuple, out T restored));
82+
Assert.AreEqual(expected: tuple, actual: restored);
83+
}
84+
}
85+
}
86+
}

src/runtime/Codecs/TupleCodecs.cs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
namespace Python.Runtime.Codecs {
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Reflection;
6+
7+
public sealed class TupleCodec<TTuple> : IPyObjectEncoder, IPyObjectDecoder {
8+
TupleCodec() { }
9+
public static TupleCodec<TTuple> Instance { get; } = new TupleCodec<TTuple>();
10+
11+
public bool CanEncode(Type type)
12+
=> type.Namespace == typeof(TTuple).Namespace && type.Name.StartsWith(typeof(TTuple).Name + '`')
13+
|| type == typeof(object) || type == typeof(TTuple);
14+
15+
public PyObject TryEncode(object value) {
16+
if (value == null) return null;
17+
18+
var tupleType = value.GetType();
19+
if (tupleType == typeof(object)) return null;
20+
if (!this.CanEncode(tupleType)) return null;
21+
if (tupleType == typeof(TTuple)) return new PyTuple();
22+
23+
long fieldCount = tupleType.GetGenericArguments().Length;
24+
var tuple = Runtime.PyTuple_New(fieldCount);
25+
Exceptions.ErrorCheck(tuple);
26+
int fieldIndex = 0;
27+
foreach (FieldInfo field in tupleType.GetFields()) {
28+
var item = field.GetValue(value);
29+
IntPtr pyItem = Converter.ToPython(item);
30+
Runtime.XIncref(pyItem);
31+
Runtime.PyTuple_SetItem(tuple, fieldIndex, pyItem);
32+
fieldIndex++;
33+
}
34+
return new PyTuple(Runtime.SelfIncRef(tuple));
35+
}
36+
37+
public bool CanDecode(PyObject objectType, Type targetType)
38+
=> objectType.Handle == Runtime.PyTuple && this.CanEncode(targetType);
39+
40+
public bool TryDecode<T>(PyObject pyObj, out T value) {
41+
if (pyObj == null) throw new ArgumentNullException(nameof(pyObj));
42+
43+
value = default;
44+
45+
if (!Runtime.PyTuple_Check(pyObj.Handle)) return false;
46+
47+
if (typeof(T) == typeof(object)) {
48+
bool converted = Decode(pyObj, out object result);
49+
if (converted) {
50+
value = (T)result;
51+
return true;
52+
}
53+
54+
return false;
55+
}
56+
57+
var itemTypes = typeof(T).GetGenericArguments();
58+
long itemCount = Runtime.PyTuple_Size(pyObj.Handle);
59+
if (itemTypes.Length != itemCount) return false;
60+
61+
if (itemCount == 0) {
62+
value = (T)EmptyTuple;
63+
return true;
64+
}
65+
66+
var elements = new object[itemCount];
67+
for (int itemIndex = 0; itemIndex < itemTypes.Length; itemIndex++) {
68+
IntPtr pyItem = Runtime.PyTuple_GetItem(pyObj.Handle, itemIndex);
69+
if (!Converter.ToManaged(pyItem, itemTypes[itemIndex], out elements[itemIndex], setError: false)) {
70+
return false;
71+
}
72+
}
73+
var factory = tupleCreate[itemCount].MakeGenericMethod(itemTypes);
74+
value = (T)factory.Invoke(null, elements);
75+
return true;
76+
}
77+
78+
static bool Decode(PyObject tuple, out object value) {
79+
long itemCount = Runtime.PyTuple_Size(tuple.Handle);
80+
if (itemCount == 0) {
81+
value = EmptyTuple;
82+
return true;
83+
}
84+
var elements = new object[itemCount];
85+
var itemTypes = new Type[itemCount];
86+
value = null;
87+
for (int itemIndex = 0; itemIndex < elements.Length; itemIndex++) {
88+
var pyItem = Runtime.PyTuple_GetItem(tuple.Handle, itemIndex);
89+
if (!Converter.ToManaged(pyItem, typeof(object), out elements[itemIndex], setError: false)) {
90+
return false;
91+
}
92+
93+
itemTypes[itemIndex] = elements[itemIndex]?.GetType() ?? typeof(object);
94+
}
95+
96+
var factory = tupleCreate[itemCount].MakeGenericMethod(itemTypes);
97+
value = factory.Invoke(null, elements);
98+
return true;
99+
}
100+
101+
static readonly MethodInfo[] tupleCreate =
102+
typeof(TTuple).GetMethods(BindingFlags.Public | BindingFlags.Static)
103+
.Where(m => m.Name == nameof(Tuple.Create))
104+
.OrderBy(m => m.GetParameters().Length)
105+
.ToArray();
106+
107+
static readonly object EmptyTuple = tupleCreate[0].Invoke(null, parameters: new object[0]);
108+
109+
public static void Register() {
110+
PyObjectConversions.RegisterEncoder(Instance);
111+
PyObjectConversions.RegisterDecoder(Instance);
112+
}
113+
}
114+
}

src/runtime/converter.cs

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,15 @@ internal static IntPtr ToPython(object value, Type type)
145145
return result;
146146
}
147147

148-
if (value is IList && !(value is INotifyPropertyChanged) && value.GetType().IsGenericType)
148+
if (Type.GetTypeCode(type) == TypeCode.Object && value.GetType() != typeof(object)) {
149+
var encoded = PyObjectConversions.TryEncode(value, type);
150+
if (encoded != null) {
151+
Runtime.XIncref(encoded.Handle);
152+
return encoded.Handle;
153+
}
154+
}
155+
156+
if (value is IList && !(value is INotifyPropertyChanged) && value.GetType().IsGenericType)
149157
{
150158
using (var resultlist = new PyList())
151159
{
@@ -448,6 +456,12 @@ internal static bool ToManagedValue(IntPtr value, Type obType,
448456
{
449457
return true;
450458
}
459+
460+
IntPtr pyType = Runtime.PyObject_TYPE(value);
461+
if (PyObjectConversions.TryDecode(value, pyType, obType, out result))
462+
{
463+
return true;
464+
}
451465
}
452466

453467
if (obType.IsEnum)
@@ -476,17 +490,18 @@ static TryConvertFromPythonDelegate GetConverter(Type targetType)
476490
convert = convert.MakeGenericMethod(targetType);
477491

478492
bool TryConvert(IntPtr pyHandle, out object result) {
479-
var pyObj = new PyObject(Runtime.SelfIncRef(pyHandle));
480-
var @params = new object[] { pyObj, null };
481-
bool success = (bool)convert.Invoke(converterAttribute, @params);
482-
result = @params[1];
483-
return success;
493+
using (var pyObj = new PyObject(Runtime.SelfIncRef(pyHandle))) {
494+
var @params = new object[] {pyObj, null};
495+
bool success = (bool)convert.Invoke(converterAttribute, @params);
496+
result = @params[1];
497+
return success;
498+
}
484499
}
485500

486501
return TryConvert;
487502
}
488503

489-
delegate bool TryConvertFromPythonDelegate(IntPtr pyObj, out object result);
504+
internal delegate bool TryConvertFromPythonDelegate(IntPtr pyObj, out object result);
490505
static readonly ConcurrentDictionary<Type, TryConvertFromPythonDelegate> TypeConverterCache =
491506
new ConcurrentDictionary<Type, TryConvertFromPythonDelegate>();
492507

0 commit comments

Comments
 (0)
0