From e0064ea0d4d6c622d28c391a3252d716941c7f53 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 30 Apr 2025 12:47:39 -0400 Subject: [PATCH 1/3] Support pythonic manipulation of managed enums. Add support for 'len' method, 'in' operator and iteration of enum types. --- src/embed_tests/ClassManagerTests.cs | 53 +++++++++++++++++++++++++ src/runtime/Types/MetaType.cs | 59 ++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/src/embed_tests/ClassManagerTests.cs b/src/embed_tests/ClassManagerTests.cs index 0db0d282f..b0edb860e 100644 --- a/src/embed_tests/ClassManagerTests.cs +++ b/src/embed_tests/ClassManagerTests.cs @@ -1003,6 +1003,59 @@ def call(instance): } #endregion + + public enum TestEnum + { + FirstEnumValue, + SecondEnumValue, + ThirdEnumValue + } + + [Test] + public void EnumPythonOperationsCanBePerformedOnManagedEnum() + { + using (Py.GIL()) + { + var module = PyModule.FromString("EnumPythonOperationsCanBePerformedOnManagedEnum", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +def get_enum_values(): + return [x for x in ClassManagerTests.TestEnum] + +def count_enum_values(): + return len(ClassManagerTests.TestEnum) + +def is_enum_value_defined(value): + return value in ClassManagerTests.TestEnum + "); + + using var pyEnumValues = module.InvokeMethod("get_enum_values"); + var enumValues = pyEnumValues.As>(); + + var expectedEnumValues = Enum.GetValues(); + CollectionAssert.AreEquivalent(expectedEnumValues, enumValues); + + using var pyEnumCount = module.InvokeMethod("count_enum_values"); + var enumCount = pyEnumCount.As(); + Assert.AreEqual(expectedEnumValues.Length, enumCount); + + var validEnumValues = expectedEnumValues + .SelectMany(x => new object[] { x, (int)x, Enum.GetName(x.GetType(), x) }) + .Select(x => (x, true)); + var invalidEnumValues = new object[] { 5, "INVALID_ENUM_VALUE" }.Select(x => (x, false)); + + foreach (var (enumValue, isValid) in validEnumValues.Concat(invalidEnumValues)) + { + using var pyEnumValue = enumValue.ToPython(); + using var pyIsDefined = module.InvokeMethod("is_enum_value_defined", pyEnumValue); + var isDefined = pyIsDefined.As(); + Assert.AreEqual(isValid, isDefined, $"Failed for {enumValue} ({enumValue.GetType()})"); + } + } + } } public class NestedTestParent diff --git a/src/runtime/Types/MetaType.cs b/src/runtime/Types/MetaType.cs index 1543711f6..addd464f3 100644 --- a/src/runtime/Types/MetaType.cs +++ b/src/runtime/Types/MetaType.cs @@ -359,5 +359,64 @@ public static NewReference __subclasscheck__(BorrowedReference tp, BorrowedRefer { return DoInstanceCheck(tp, args, true); } + + /// + /// Standard iteration support Enums. This allows natural interation + /// over the available values an Enum defines. + /// + public static NewReference tp_iter(BorrowedReference tp) + { + var type = CheckAndGetEnumType(tp); + var values = Enum.GetValues(type); + return new Iterator(values.GetEnumerator(), type).Alloc(); + } + + /// + /// Implements __len__ for Enum types. + /// + public static int mp_length(BorrowedReference tp) + { + var type = CheckAndGetEnumType(tp); + return Enum.GetValues(type).Length; + } + + /// + /// Implements __contains__ for Enum types. + /// + public static int sq_contains(BorrowedReference tp, BorrowedReference v) + { + var type = CheckAndGetEnumType(tp); + + if (!Converter.ToManaged(v, type, out var enumValue, false) && + !Converter.ToManaged(v, typeof(int), out enumValue, false) && + !Converter.ToManaged(v, typeof(string), out enumValue, false)) + { + Exceptions.SetError(Exceptions.TypeError, + $"invalid parameter type for sq_contains: should be {Converter.GetTypeByAlias(v)}, found {type}"); + } + return Enum.IsDefined(type, enumValue) ? 1 : 0; + } + + private static Type CheckAndGetEnumType(BorrowedReference tp) + { + var cb = GetManagedObject(tp) as ClassBase; + if (cb == null) + { + Exceptions.SetError(Exceptions.TypeError, "invalid object"); + } + + if (!cb.type.Valid) + { + Exceptions.SetError(Exceptions.TypeError, "invalid type"); + } + + var type = cb.type.Value; + if (!type.IsEnum) + { + Exceptions.SetError(Exceptions.TypeError, "uniterable object"); + } + + return type; + } } } From 2cfb4539537610335f1d8f2d267ccfaa0bc97a51 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 30 Apr 2025 15:36:53 -0400 Subject: [PATCH 2/3] Minor fixes and unit tests --- src/embed_tests/ClassManagerTests.cs | 27 ++++++++++++++++++++++++ src/runtime/Types/MetaType.cs | 31 +++++++++++++++++++++------- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/embed_tests/ClassManagerTests.cs b/src/embed_tests/ClassManagerTests.cs index b0edb860e..15da61e3b 100644 --- a/src/embed_tests/ClassManagerTests.cs +++ b/src/embed_tests/ClassManagerTests.cs @@ -1056,6 +1056,33 @@ def is_enum_value_defined(value): } } } + + [Test] + public void EnumInterableOperationsNotSupportedForManagedNonEnumTypes() + { + using (Py.GIL()) + { + var module = PyModule.FromString("EnumInterableOperationsNotSupportedForManagedNonEnumTypes", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +def get_enum_values(): + return [x for x in ClassManagerTests] + +def count_enum_values(): + return len(ClassManagerTests) + +def is_enum_value_defined(): + return 1 in ClassManagerTests + "); + + Assert.Throws(() => module.InvokeMethod("get_enum_values")); + Assert.Throws(() => module.InvokeMethod("count_enum_values")); + Assert.Throws(() => module.InvokeMethod("is_enum_value_defined")); + } + } } public class NestedTestParent diff --git a/src/runtime/Types/MetaType.cs b/src/runtime/Types/MetaType.cs index addd464f3..bfaced5f6 100644 --- a/src/runtime/Types/MetaType.cs +++ b/src/runtime/Types/MetaType.cs @@ -366,7 +366,10 @@ public static NewReference __subclasscheck__(BorrowedReference tp, BorrowedRefer /// public static NewReference tp_iter(BorrowedReference tp) { - var type = CheckAndGetEnumType(tp); + if (!TryGetEnumType(tp, out var type)) + { + return default; + } var values = Enum.GetValues(type); return new Iterator(values.GetEnumerator(), type).Alloc(); } @@ -376,7 +379,10 @@ public static NewReference tp_iter(BorrowedReference tp) /// public static int mp_length(BorrowedReference tp) { - var type = CheckAndGetEnumType(tp); + if (!TryGetEnumType(tp, out var type)) + { + return -1; + } return Enum.GetValues(type).Length; } @@ -385,7 +391,10 @@ public static int mp_length(BorrowedReference tp) /// public static int sq_contains(BorrowedReference tp, BorrowedReference v) { - var type = CheckAndGetEnumType(tp); + if (!TryGetEnumType(tp, out var type)) + { + return -1; + } if (!Converter.ToManaged(v, type, out var enumValue, false) && !Converter.ToManaged(v, typeof(int), out enumValue, false) && @@ -393,30 +402,36 @@ public static int sq_contains(BorrowedReference tp, BorrowedReference v) { Exceptions.SetError(Exceptions.TypeError, $"invalid parameter type for sq_contains: should be {Converter.GetTypeByAlias(v)}, found {type}"); + return -1; } + return Enum.IsDefined(type, enumValue) ? 1 : 0; } - private static Type CheckAndGetEnumType(BorrowedReference tp) + private static bool TryGetEnumType(BorrowedReference tp, out Type type) { + type = null; var cb = GetManagedObject(tp) as ClassBase; if (cb == null) { Exceptions.SetError(Exceptions.TypeError, "invalid object"); + return false; } if (!cb.type.Valid) { Exceptions.SetError(Exceptions.TypeError, "invalid type"); + return false; } - var type = cb.type.Value; - if (!type.IsEnum) + if (!cb.type.Value.IsEnum) { - Exceptions.SetError(Exceptions.TypeError, "uniterable object"); + Exceptions.SetError(Exceptions.TypeError, "uniterable type"); + return false; } - return type; + type = cb.type.Value; + return true; } } } From 88a607056698a4042e3a59d0ff0da7c8312160a2 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 1 May 2025 10:24:42 -0400 Subject: [PATCH 3/3] Bump version to 2.0.43 --- src/perf_tests/Python.PerformanceTests.csproj | 4 ++-- src/runtime/Properties/AssemblyInfo.cs | 4 ++-- src/runtime/Python.Runtime.csproj | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/perf_tests/Python.PerformanceTests.csproj b/src/perf_tests/Python.PerformanceTests.csproj index 540e18b66..99f447f56 100644 --- a/src/perf_tests/Python.PerformanceTests.csproj +++ b/src/perf_tests/Python.PerformanceTests.csproj @@ -13,7 +13,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + compile @@ -25,7 +25,7 @@ - + diff --git a/src/runtime/Properties/AssemblyInfo.cs b/src/runtime/Properties/AssemblyInfo.cs index 126b2f62e..c8a43c43a 100644 --- a/src/runtime/Properties/AssemblyInfo.cs +++ b/src/runtime/Properties/AssemblyInfo.cs @@ -4,5 +4,5 @@ [assembly: InternalsVisibleTo("Python.EmbeddingTest, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")] [assembly: InternalsVisibleTo("Python.Test, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")] -[assembly: AssemblyVersion("2.0.42")] -[assembly: AssemblyFileVersion("2.0.42")] +[assembly: AssemblyVersion("2.0.43")] +[assembly: AssemblyFileVersion("2.0.43")] diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index 4ab951154..f1f77f9d7 100644 --- a/src/runtime/Python.Runtime.csproj +++ b/src/runtime/Python.Runtime.csproj @@ -5,7 +5,7 @@ Python.Runtime Python.Runtime QuantConnect.pythonnet - 2.0.42 + 2.0.43 false LICENSE https://github.com/pythonnet/pythonnet