Skip to content

Commit 5f36aa7

Browse files
authored
C# enums to work as proper enums in Python (#103)
* Make C# enums work as proper enums in Python Avoid converting C# enums to long in Python * Minor fixes in substraction and division operators * Bump version to 2.0.45 * Support enum comparison to other enum types Compare based on the underlying int value * Use single cached reference for C# enum values in Python Make C# enums work as singletons in Python so that the `is` identity comparison operator works for C# enums as well. * Minor fix * More tests and cleanup * Reduce enum operators overloads * Fix comparison to null/None * Minor change
1 parent 68a2183 commit 5f36aa7

File tree

8 files changed

+1191
-7
lines changed

8 files changed

+1191
-7
lines changed

src/embed_tests/EnumTests.cs

Lines changed: 628 additions & 0 deletions
Large diffs are not rendered by default.

src/embed_tests/TestMethodBinder.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,20 @@ public string VariableArgumentsMethod(params PyObject[] paramsParams)
815815
return "VariableArgumentsMethod(PyObject[])";
816816
}
817817

818+
// ----
819+
820+
public string MethodWithEnumParam(SomeEnu enumValue, string symbol)
821+
{
822+
return $"MethodWithEnumParam With Enum";
823+
}
824+
825+
public string MethodWithEnumParam(PyObject pyObject, string symbol)
826+
{
827+
return $"MethodWithEnumParam With PyObject";
828+
}
829+
830+
// ----
831+
818832
public string ConstructorMessage { get; set; }
819833

820834
public OverloadsTestClass(params CSharpModel[] paramsParams)
@@ -1117,6 +1131,26 @@ def get_instance():
11171131
Assert.AreEqual("OverloadsTestClass(PyObject[])", instance.GetAttr("ConstructorMessage").As<string>());
11181132
}
11191133

1134+
[Test]
1135+
public void EnumHasPrecedenceOverPyObject()
1136+
{
1137+
using var _ = Py.GIL();
1138+
1139+
var module = PyModule.FromString("EnumHasPrecedenceOverPyObject", @$"
1140+
from clr import AddReference
1141+
AddReference(""System"")
1142+
from Python.EmbeddingTest import *
1143+
1144+
class PythonModel(TestMethodBinder.CSharpModel):
1145+
pass
1146+
1147+
def call_method():
1148+
return TestMethodBinder.OverloadsTestClass().MethodWithEnumParam(TestMethodBinder.SomeEnu.A, ""Some string"")
1149+
");
1150+
1151+
var result = module.GetAttr("call_method").Invoke();
1152+
Assert.AreEqual("MethodWithEnumParam With Enum", result.As<string>());
1153+
}
11201154

11211155
// Used to test that we match this function with Py DateTime & Date Objects
11221156
public static int GetMonth(DateTime test)

src/perf_tests/Python.PerformanceTests.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1414
</PackageReference>
1515
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.*" />
16-
<PackageReference Include="quantconnect.pythonnet" Version="2.0.44" GeneratePathProperty="true">
16+
<PackageReference Include="quantconnect.pythonnet" Version="2.0.45" GeneratePathProperty="true">
1717
<IncludeAssets>compile</IncludeAssets>
1818
</PackageReference>
1919
</ItemGroup>
@@ -25,7 +25,7 @@
2525
</Target>
2626

2727
<Target Name="CopyBaseline" AfterTargets="Build">
28-
<Copy SourceFiles="$(NuGetPackageRoot)quantconnect.pythonnet\2.0.44\lib\net9.0\Python.Runtime.dll" DestinationFolder="$(OutDir)baseline" />
28+
<Copy SourceFiles="$(NuGetPackageRoot)quantconnect.pythonnet\2.0.45\lib\net9.0\Python.Runtime.dll" DestinationFolder="$(OutDir)baseline" />
2929
</Target>
3030

3131
<Target Name="CopyNewBuild" AfterTargets="Build">

src/runtime/Converter.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ namespace Python.Runtime
1818
[SuppressUnmanagedCodeSecurity]
1919
internal class Converter
2020
{
21+
/// <summary>
22+
/// We use a cache of the enum values references so that we treat them as singletons in Python.
23+
/// We just try to mimic Python enums behavior, since Python enum values are singletons,
24+
/// so the `is` identity comparison operator works for C# enums as well.
25+
/// </summary>
26+
27+
private static readonly Dictionary<object, PyObject> _enumCache = new();
2128
private Converter()
2229
{
2330
}
@@ -226,6 +233,16 @@ internal static NewReference ToPython(object? value, Type type)
226233
return resultlist.NewReferenceOrNull();
227234
}
228235

236+
if (type.IsEnum)
237+
{
238+
if (!_enumCache.TryGetValue(value, out var cachedValue))
239+
{
240+
_enumCache[value] = cachedValue = CLRObject.GetReference(value, type).MoveToPyObject();
241+
}
242+
243+
return cachedValue.NewReferenceOrNull();
244+
}
245+
229246
// it the type is a python subclass of a managed type then return the
230247
// underlying python object rather than construct a new wrapper object.
231248
var pyderived = value as IPythonDerivedType;

src/runtime/MethodBinder.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,17 @@ internal static int ArgPrecedence(Type t, bool isOperatorMethod)
383383
return 3000;
384384
}
385385

386+
if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>))
387+
{
388+
// Nullable<T> is a special case, we treat it as the underlying type
389+
return ArgPrecedence(Nullable.GetUnderlyingType(t), isOperatorMethod);
390+
}
391+
392+
if (t.IsEnum)
393+
{
394+
return -2;
395+
}
396+
386397
if (t.IsAssignableFrom(typeof(PyObject)) && !isOperatorMethod)
387398
{
388399
return -1;

src/runtime/Properties/AssemblyInfo.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
[assembly: InternalsVisibleTo("Python.EmbeddingTest, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")]
55
[assembly: InternalsVisibleTo("Python.Test, PublicKey=00240000048000009400000006020000002400005253413100040000110000005ffd8f49fb44ab0641b3fd8d55e749f716e6dd901032295db641eb98ee46063cbe0d4a1d121ef0bc2af95f8a7438d7a80a3531316e6b75c2dae92fb05a99f03bf7e0c03980e1c3cfb74ba690aca2f3339ef329313bcc5dccced125a4ffdc4531dcef914602cd5878dc5fbb4d4c73ddfbc133f840231343e013762884d6143189")]
66

7-
[assembly: AssemblyVersion("2.0.44")]
8-
[assembly: AssemblyFileVersion("2.0.44")]
7+
[assembly: AssemblyVersion("2.0.45")]
8+
[assembly: AssemblyFileVersion("2.0.45")]

src/runtime/Python.Runtime.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<RootNamespace>Python.Runtime</RootNamespace>
66
<AssemblyName>Python.Runtime</AssemblyName>
77
<PackageId>QuantConnect.pythonnet</PackageId>
8-
<Version>2.0.44</Version>
8+
<Version>2.0.45</Version>
99
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
1010
<PackageLicenseFile>LICENSE</PackageLicenseFile>
1111
<RepositoryUrl>https://github.com/pythonnet/pythonnet</RepositoryUrl>

0 commit comments

Comments
 (0)