Skip to content

Commit f77b131

Browse files
committed
reworked Enum marshaling
- enums are no longer converted to and from PyLong automatically #1220 - one can construct an instance of MyEnum from Python using MyEnum(numeric_val), e.g. MyEnum(10) - in the above, if MyEnum does not have [Flags] and does not have value 10 defined, to create MyEnum with value 10 one must call MyEnum(10, True). Here True is an unnamed parameter, that allows unchecked conversion - legacy behavior has been moved to a codec (EnumPyLongCodec); enums can now be encoded by codecs - flags enums support bitwise ops via EnumOps class
1 parent 6f1219f commit f77b131

17 files changed

+316
-147
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ when .NET expects an integer [#1342][i1342]
3636
- BREAKING: Methods with `ref` or `out` parameters and void return type return a tuple of only the `ref` and `out` parameters.
3737
- BREAKING: to call Python from .NET `Runtime.PythonDLL` property must be set to Python DLL name
3838
or the DLL must be loaded in advance. This must be done before calling any other Python.NET functions.
39+
- BREAKING: disabled implicit conversion from C# enums to Python `int` and back.
40+
One must now either use enum members (e.g. `MyEnum.Option`), or use enum constructor
41+
(e.g. `MyEnum(42)` or `MyEnum(42, True)` when `MyEnum` does not have a member with value 42).
3942
- Sign Runtime DLL with a strong name
4043
- Implement loading through `clr_loader` instead of the included `ClrModule`, enables
4144
support for .NET Core

src/embed_tests/TestOperator.cs

+8
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,14 @@ public void SymmetricalOperatorOverloads()
335335
");
336336
}
337337

338+
[Test]
339+
public void EnumOperator()
340+
{
341+
PythonEngine.Exec($@"
342+
from System.IO import FileAccess
343+
c = FileAccess.Read | FileAccess.Write");
344+
}
345+
338346
[Test]
339347
public void OperatorOverloadMissingArgument()
340348
{

src/runtime/Codecs/EnumPyLongCodec.cs

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System;
2+
3+
namespace Python.Runtime.Codecs
4+
{
5+
[Obsolete]
6+
public sealed class EnumPyLongCodec : IPyObjectEncoder, IPyObjectDecoder
7+
{
8+
public static EnumPyLongCodec Instance { get; } = new EnumPyLongCodec();
9+
10+
public bool CanDecode(PyObject objectType, Type targetType)
11+
{
12+
return targetType.IsEnum
13+
&& objectType.IsSubclass(new BorrowedReference(Runtime.PyLongType));
14+
}
15+
16+
public bool CanEncode(Type type)
17+
{
18+
return type == typeof(object) || type == typeof(ValueType) || type.IsEnum;
19+
}
20+
21+
public bool TryDecode<T>(PyObject pyObj, out T value)
22+
{
23+
value = default;
24+
if (!typeof(T).IsEnum) return false;
25+
26+
Type etype = Enum.GetUnderlyingType(typeof(T));
27+
28+
if (!PyLong.IsLongType(pyObj)) return false;
29+
30+
object result;
31+
try
32+
{
33+
result = pyObj.AsManagedObject(etype);
34+
}
35+
catch (InvalidCastException)
36+
{
37+
return false;
38+
}
39+
40+
if (Enum.IsDefined(typeof(T), result) || typeof(T).IsFlagsEnum())
41+
{
42+
value = (T)Enum.ToObject(typeof(T), result);
43+
return true;
44+
}
45+
46+
return false;
47+
}
48+
49+
public PyObject TryEncode(object value)
50+
{
51+
if (value is null) return null;
52+
53+
var enumType = value.GetType();
54+
if (!enumType.IsEnum) return null;
55+
56+
try
57+
{
58+
return new PyLong((long)value);
59+
}
60+
catch (InvalidCastException)
61+
{
62+
return new PyLong((ulong)value);
63+
}
64+
}
65+
66+
private EnumPyLongCodec() { }
67+
}
68+
}

src/runtime/classmanager.cs

+11
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,17 @@ private static ClassInfo GetClassInfo(Type type)
403403
}
404404
}
405405

406+
// only [Flags] enums support bitwise operations
407+
if (type.IsEnum && type.IsFlagsEnum())
408+
{
409+
var opsImpl = typeof(EnumOps<>).MakeGenericType(type);
410+
foreach (var op in opsImpl.GetMethods(OpsHelper.BindingFlags))
411+
{
412+
local[op.Name] = 1;
413+
}
414+
info = info.Concat(opsImpl.GetMethods(OpsHelper.BindingFlags)).ToArray();
415+
}
416+
406417
// Now again to filter w/o losing overloaded member info
407418
for (i = 0; i < info.Length; i++)
408419
{

src/runtime/classobject.cs

+42-5
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@ internal NewReference GetDocString()
5050
/// <summary>
5151
/// Implements __new__ for reflected classes and value types.
5252
/// </summary>
53-
public static IntPtr tp_new(IntPtr tp, IntPtr args, IntPtr kw)
53+
public static IntPtr tp_new(IntPtr tpRaw, IntPtr args, IntPtr kw)
5454
{
55+
var tp = new BorrowedReference(tpRaw);
5556
var self = GetManagedObject(tp) as ClassObject;
5657

5758
// Sanity check: this ensures a graceful error if someone does
@@ -87,7 +88,7 @@ public static IntPtr tp_new(IntPtr tp, IntPtr args, IntPtr kw)
8788
return IntPtr.Zero;
8889
}
8990

90-
return CLRObject.GetInstHandle(result, tp);
91+
return CLRObject.GetInstHandle(result, tp).DangerousMoveToPointerOrNull();
9192
}
9293

9394
if (type.IsAbstract)
@@ -98,8 +99,7 @@ public static IntPtr tp_new(IntPtr tp, IntPtr args, IntPtr kw)
9899

99100
if (type.IsEnum)
100101
{
101-
Exceptions.SetError(Exceptions.TypeError, "cannot instantiate enumeration");
102-
return IntPtr.Zero;
102+
return NewEnum(type, new BorrowedReference(args), tp).DangerousMoveToPointerOrNull();
103103
}
104104

105105
object obj = self.binder.InvokeRaw(IntPtr.Zero, args, kw);
@@ -108,7 +108,44 @@ public static IntPtr tp_new(IntPtr tp, IntPtr args, IntPtr kw)
108108
return IntPtr.Zero;
109109
}
110110

111-
return CLRObject.GetInstHandle(obj, tp);
111+
return CLRObject.GetInstHandle(obj, tp).DangerousMoveToPointerOrNull();
112+
}
113+
114+
private static NewReference NewEnum(Type type, BorrowedReference args, BorrowedReference tp)
115+
{
116+
nint argCount = Runtime.PyTuple_Size(args);
117+
bool allowUnchecked = false;
118+
if (argCount == 2)
119+
{
120+
var allow = Runtime.PyTuple_GetItem(args, 1);
121+
if (!Converter.ToManaged(allow, typeof(bool), out var allowObj, true) || allowObj is null)
122+
{
123+
Exceptions.RaiseTypeError("second argument to enum constructor must be a boolean");
124+
return default;
125+
}
126+
allowUnchecked |= (bool)allowObj;
127+
}
128+
129+
if (argCount < 1 || argCount > 2)
130+
{
131+
Exceptions.SetError(Exceptions.TypeError, "no constructors match given arguments");
132+
return default;
133+
}
134+
135+
var op = Runtime.PyTuple_GetItem(args, 0);
136+
if (!Converter.ToManaged(op, type.GetEnumUnderlyingType(), out object result, true))
137+
{
138+
return default;
139+
}
140+
141+
if (!allowUnchecked && !Enum.IsDefined(type, result) && !type.IsFlagsEnum())
142+
{
143+
Exceptions.SetError(Exceptions.ValueError, "Invalid enumeration value. Pass True as the second argument if unchecked conversion is desired");
144+
return default;
145+
}
146+
147+
object enumValue = Enum.ToObject(type, result);
148+
return CLRObject.GetInstHandle(enumValue, tp);
112149
}
113150

114151

src/runtime/converter.cs

+30-44
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ private Converter()
2727
private static Type int16Type;
2828
private static Type int32Type;
2929
private static Type int64Type;
30-
private static Type flagsType;
3130
private static Type boolType;
3231
private static Type typeType;
3332

@@ -42,7 +41,6 @@ static Converter()
4241
singleType = typeof(Single);
4342
doubleType = typeof(Double);
4443
decimalType = typeof(Decimal);
45-
flagsType = typeof(FlagsAttribute);
4644
boolType = typeof(Boolean);
4745
typeType = typeof(Type);
4846
}
@@ -148,7 +146,8 @@ internal static IntPtr ToPython(object value, Type type)
148146
return result;
149147
}
150148

151-
if (Type.GetTypeCode(type) == TypeCode.Object && value.GetType() != typeof(object)) {
149+
if (Type.GetTypeCode(type) == TypeCode.Object && value.GetType() != typeof(object)
150+
|| type.IsEnum) {
152151
var encoded = PyObjectConversions.TryEncode(value, type);
153152
if (encoded != null) {
154153
result = encoded.Handle;
@@ -203,6 +202,11 @@ internal static IntPtr ToPython(object value, Type type)
203202

204203
type = value.GetType();
205204

205+
if (type.IsEnum)
206+
{
207+
return CLRObject.GetInstHandle(value, type);
208+
}
209+
206210
TypeCode tc = Type.GetTypeCode(type);
207211

208212
switch (tc)
@@ -317,6 +321,18 @@ internal static bool ToManaged(IntPtr value, Type type,
317321
}
318322
return Converter.ToManagedValue(value, type, out result, setError);
319323
}
324+
/// <summary>
325+
/// Return a managed object for the given Python object, taking funny
326+
/// byref types into account.
327+
/// </summary>
328+
/// <param name="value">A Python object</param>
329+
/// <param name="type">The desired managed type</param>
330+
/// <param name="result">Receives the managed object</param>
331+
/// <param name="setError">If true, call <c>Exceptions.SetError</c> with the reason for failure.</param>
332+
/// <returns>True on success</returns>
333+
internal static bool ToManaged(BorrowedReference value, Type type,
334+
out object result, bool setError)
335+
=> ToManaged(value.DangerousGetAddress(), type, out result, setError);
320336

321337
internal static bool ToManagedValue(BorrowedReference value, Type obType,
322338
out object result, bool setError)
@@ -398,11 +414,6 @@ internal static bool ToManagedValue(IntPtr value, Type obType,
398414
return ToArray(value, obType, out result, setError);
399415
}
400416

401-
if (obType.IsEnum)
402-
{
403-
return ToEnum(value, obType, out result, setError);
404-
}
405-
406417
// Conversion to 'Object' is done based on some reasonable default
407418
// conversions (Python string -> managed string, Python int -> Int32 etc.).
408419
if (obType == objectType)
@@ -497,7 +508,7 @@ internal static bool ToManagedValue(IntPtr value, Type obType,
497508
}
498509

499510
TypeCode typeCode = Type.GetTypeCode(obType);
500-
if (typeCode == TypeCode.Object)
511+
if (typeCode == TypeCode.Object || obType.IsEnum)
501512
{
502513
IntPtr pyType = Runtime.PyObject_TYPE(value);
503514
if (PyObjectConversions.TryDecode(value, pyType, obType, out result))
@@ -516,8 +527,17 @@ internal static bool ToManagedValue(IntPtr value, Type obType,
516527
/// </summary>
517528
private static bool ToPrimitive(IntPtr value, Type obType, out object result, bool setError)
518529
{
519-
TypeCode tc = Type.GetTypeCode(obType);
520530
result = null;
531+
if (obType.IsEnum)
532+
{
533+
if (setError)
534+
{
535+
Exceptions.SetError(Exceptions.TypeError, "since Python.NET 3.0 int can not be converted to Enum implicitly. Use Enum(int_value)");
536+
}
537+
return false;
538+
}
539+
540+
TypeCode tc = Type.GetTypeCode(obType);
521541
IntPtr op = IntPtr.Zero;
522542

523543
switch (tc)
@@ -876,40 +896,6 @@ private static bool ToArray(IntPtr value, Type obType, out object result, bool s
876896
result = items;
877897
return true;
878898
}
879-
880-
881-
/// <summary>
882-
/// Convert a Python value to a correctly typed managed enum instance.
883-
/// </summary>
884-
private static bool ToEnum(IntPtr value, Type obType, out object result, bool setError)
885-
{
886-
Type etype = Enum.GetUnderlyingType(obType);
887-
result = null;
888-
889-
if (!ToPrimitive(value, etype, out result, setError))
890-
{
891-
return false;
892-
}
893-
894-
if (Enum.IsDefined(obType, result))
895-
{
896-
result = Enum.ToObject(obType, result);
897-
return true;
898-
}
899-
900-
if (obType.GetCustomAttributes(flagsType, true).Length > 0)
901-
{
902-
result = Enum.ToObject(obType, result);
903-
return true;
904-
}
905-
906-
if (setError)
907-
{
908-
Exceptions.SetError(Exceptions.ValueError, "invalid enumeration value");
909-
}
910-
911-
return false;
912-
}
913899
}
914900

915901
public static class ConverterExtension

src/runtime/exceptions.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ public static void Clear()
343343
public static void warn(string message, IntPtr exception, int stacklevel)
344344
{
345345
if (exception == IntPtr.Zero ||
346-
(Runtime.PyObject_IsSubclass(exception, Exceptions.Warning) != 1))
346+
(Runtime.PyObject_IsSubclass(new BorrowedReference(exception), new BorrowedReference(Exceptions.Warning)) != 1))
347347
{
348348
Exceptions.RaiseTypeError("Invalid exception");
349349
}

src/runtime/operatormethod.cs

+11-5
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ static OperatorMethod()
5151
["op_OnesComplement"] = new SlotDefinition("__invert__", TypeOffset.nb_invert),
5252
["op_UnaryNegation"] = new SlotDefinition("__neg__", TypeOffset.nb_negative),
5353
["op_UnaryPlus"] = new SlotDefinition("__pos__", TypeOffset.nb_positive),
54-
["op_OneComplement"] = new SlotDefinition("__invert__", TypeOffset.nb_invert),
5554
};
5655
ComparisonOpMap = new Dictionary<string, string>
5756
{
@@ -80,7 +79,7 @@ public static void Shutdown()
8079

8180
public static bool IsOperatorMethod(MethodBase method)
8281
{
83-
if (!method.IsSpecialName)
82+
if (!method.IsSpecialName && !method.IsOpsHelper())
8483
{
8584
return false;
8685
}
@@ -102,7 +101,12 @@ public static void FixupSlots(IntPtr pyType, Type clrType)
102101
{
103102
const BindingFlags flags = BindingFlags.Public | BindingFlags.Static;
104103
Debug.Assert(_opType != null);
105-
foreach (var method in clrType.GetMethods(flags))
104+
105+
var staticMethods =
106+
clrType.IsEnum ? typeof(EnumOps<>).MakeGenericType(clrType).GetMethods(flags)
107+
: clrType.GetMethods(flags);
108+
109+
foreach (var method in staticMethods)
106110
{
107111
// We only want to override slots for operators excluding
108112
// comparison operators, which are handled by ClassBase.tp_richcompare.
@@ -170,9 +174,11 @@ public static string ReversePyMethodName(string pyName)
170174
/// <returns></returns>
171175
public static bool IsReverse(MethodInfo method)
172176
{
173-
Type declaringType = method.DeclaringType;
177+
Type primaryType = method.IsOpsHelper()
178+
? method.DeclaringType.GetGenericArguments()[0]
179+
: method.DeclaringType;
174180
Type leftOperandType = method.GetParameters()[0].ParameterType;
175-
return leftOperandType != declaringType;
181+
return leftOperandType != primaryType;
176182
}
177183

178184
public static void FilterMethods(MethodInfo[] methods, out MethodInfo[] forwardMethods, out MethodInfo[] reverseMethods)

0 commit comments

Comments
 (0)