From 0e86e8df28972102da62ae533c8b9685ca4886f6 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Mon, 28 Jul 2025 16:50:24 -0400 Subject: [PATCH 01/10] Make C# enums work as proper enums in Python Avoid converting C# enums to long in Python --- src/embed_tests/EnumTests.cs | 353 ++++++++++++++++++++++++ src/embed_tests/TestMethodBinder.cs | 34 +++ src/runtime/Converter.cs | 5 + src/runtime/MethodBinder.cs | 11 + src/runtime/Util/OpsHelper.cs | 398 +++++++++++++++++++++++++++- 5 files changed, 799 insertions(+), 2 deletions(-) create mode 100644 src/embed_tests/EnumTests.cs diff --git a/src/embed_tests/EnumTests.cs b/src/embed_tests/EnumTests.cs new file mode 100644 index 000000000..69123ea5d --- /dev/null +++ b/src/embed_tests/EnumTests.cs @@ -0,0 +1,353 @@ +using System; +using System.Collections.Generic; + +using NUnit.Framework; + +using Python.Runtime; + +namespace Python.EmbeddingTest +{ + public class EnumTests + { + [OneTimeSetUp] + public void SetUp() + { + PythonEngine.Initialize(); + } + + [OneTimeTearDown] + public void Dispose() + { + PythonEngine.Shutdown(); + } + + public enum Direction + { + Down = -2, + Flat = 0, + Up = 2, + } + + [Test] + public void CSharpEnumsBehaveAsEnumsInPython() + { + using var _ = Py.GIL(); + var module = PyModule.FromString("CSharpEnumsBehaveAsEnumsInPython", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +def enum_is_right_type(enum_value=EnumTests.Direction.Up): + return isinstance(enum_value, EnumTests.Direction) +"); + + Assert.IsTrue(module.InvokeMethod("enum_is_right_type").As()); + + // Also test passing the enum value from C# to Python + using var pyEnumValue = Direction.Up.ToPython(); + Assert.IsTrue(module.InvokeMethod("enum_is_right_type", pyEnumValue).As()); + } + + private PyModule GetTestOperatorsModule(string @operator, Direction operand1, double operand2) + { + var operand1Str = $"{nameof(EnumTests)}.{nameof(Direction)}.{operand1}"; + return PyModule.FromString("GetTestOperatorsModule", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +def operation1(): + return {operand1Str} {@operator} {operand2} + +def operation2(): + return {operand2} {@operator} {operand1Str} +"); + } + + [TestCase(" *", Direction.Down, 2, -4)] + [TestCase("/", Direction.Down, 2, -1)] + [TestCase("+", Direction.Down, 2, 0)] + [TestCase("-", Direction.Down, 2, -4)] + [TestCase("*", Direction.Flat, 2, 0)] + [TestCase("/", Direction.Flat, 2, 0)] + [TestCase("+", Direction.Flat, 2, 2)] + [TestCase("-", Direction.Flat, 2, -2)] + [TestCase("*", Direction.Up, 2, 4)] + [TestCase("/", Direction.Up, 2, 1)] + [TestCase("+", Direction.Up, 2, 4)] + [TestCase("-", Direction.Up, 2, 0)] + public void ArithmeticOperatorsWorkWithoutExplicitCast(string @operator, Direction operand1, double operand2, double expectedResult) + { + using var _ = Py.GIL(); + var module = GetTestOperatorsModule(@operator, operand1, operand2); + + Assert.AreEqual(expectedResult, module.InvokeMethod("operation1").As()); + Assert.AreEqual(expectedResult, module.InvokeMethod("operation2").As()); + } + + [TestCase("==", Direction.Down, -2, true)] + [TestCase("==", Direction.Down, 0, false)] + [TestCase("==", Direction.Down, 2, false)] + [TestCase("==", Direction.Flat, -2, false)] + [TestCase("==", Direction.Flat, 0, true)] + [TestCase("==", Direction.Flat, 2, false)] + [TestCase("==", Direction.Up, -2, false)] + [TestCase("==", Direction.Up, 0, false)] + [TestCase("==", Direction.Up, 2, true)] + [TestCase("!=", Direction.Down, -2, false)] + [TestCase("!=", Direction.Down, 0, true)] + [TestCase("!=", Direction.Down, 2, true)] + [TestCase("!=", Direction.Flat, -2, true)] + [TestCase("!=", Direction.Flat, 0, false)] + [TestCase("!=", Direction.Flat, 2, true)] + [TestCase("!=", Direction.Up, -2, true)] + [TestCase("!=", Direction.Up, 0, true)] + [TestCase("!=", Direction.Up, 2, false)] + [TestCase("<", Direction.Down, -3, false)] + [TestCase("<", Direction.Down, -2, false)] + [TestCase("<", Direction.Down, 0, true)] + [TestCase("<", Direction.Down, 2, true)] + [TestCase("<", Direction.Flat, -2, false)] + [TestCase("<", Direction.Flat, 0, false)] + [TestCase("<", Direction.Flat, 2, true)] + [TestCase("<", Direction.Up, -2, false)] + [TestCase("<", Direction.Up, 0, false)] + [TestCase("<", Direction.Up, 2, false)] + [TestCase("<", Direction.Up, 3, true)] + [TestCase("<=", Direction.Down, -3, false)] + [TestCase("<=", Direction.Down, -2, true)] + [TestCase("<=", Direction.Down, 0, true)] + [TestCase("<=", Direction.Down, 2, true)] + [TestCase("<=", Direction.Flat, -2, false)] + [TestCase("<=", Direction.Flat, 0, true)] + [TestCase("<=", Direction.Flat, 2, true)] + [TestCase("<=", Direction.Up, -2, false)] + [TestCase("<=", Direction.Up, 0, false)] + [TestCase("<=", Direction.Up, 2, true)] + [TestCase("<=", Direction.Up, 3, true)] + [TestCase(">", Direction.Down, -3, true)] + [TestCase(">", Direction.Down, -2, false)] + [TestCase(">", Direction.Down, 0, false)] + [TestCase(">", Direction.Down, 2, false)] + [TestCase(">", Direction.Flat, -2, true)] + [TestCase(">", Direction.Flat, 0, false)] + [TestCase(">", Direction.Flat, 2, false)] + [TestCase(">", Direction.Up, -2, true)] + [TestCase(">", Direction.Up, 0, true)] + [TestCase(">", Direction.Up, 2, false)] + [TestCase(">", Direction.Up, 3, false)] + [TestCase(">=", Direction.Down, -3, true)] + [TestCase(">=", Direction.Down, -2, true)] + [TestCase(">=", Direction.Down, 0, false)] + [TestCase(">=", Direction.Down, 2, false)] + [TestCase(">=", Direction.Flat, -2, true)] + [TestCase(">=", Direction.Flat, 0, true)] + [TestCase(">=", Direction.Flat, 2, false)] + [TestCase(">=", Direction.Up, -2, true)] + [TestCase(">=", Direction.Up, 0, true)] + [TestCase(">=", Direction.Up, 2, true)] + [TestCase(">=", Direction.Up, 3, false)] + public void IntComparisonOperatorsWorkWithoutExplicitCast(string @operator, Direction operand1, int operand2, bool expectedResult) + { + using var _ = Py.GIL(); + var module = GetTestOperatorsModule(@operator, operand1, operand2); + + Assert.AreEqual(expectedResult, module.InvokeMethod("operation1").As()); + + var invertedOperationExpectedResult = (@operator.StartsWith('<') || @operator.StartsWith('>')) && Convert.ToInt64(operand1) != operand2 + ? !expectedResult + : expectedResult; + Assert.AreEqual(invertedOperationExpectedResult, module.InvokeMethod("operation2").As()); + } + + [TestCase("==", Direction.Down, -2.0, true)] + [TestCase("==", Direction.Down, -2.00001, false)] + [TestCase("==", Direction.Down, -1.99999, false)] + [TestCase("==", Direction.Down, 0.0, false)] + [TestCase("==", Direction.Down, 2.0, false)] + [TestCase("==", Direction.Flat, -2.0, false)] + [TestCase("==", Direction.Flat, 0.0, true)] + [TestCase("==", Direction.Flat, 0.00001, false)] + [TestCase("==", Direction.Flat, -0.00001, false)] + [TestCase("==", Direction.Flat, 2.0, false)] + [TestCase("==", Direction.Up, -2.0, false)] + [TestCase("==", Direction.Up, 0.0, false)] + [TestCase("==", Direction.Up, 2.0, true)] + [TestCase("==", Direction.Up, 2.00001, false)] + [TestCase("==", Direction.Up, 1.99999, false)] + [TestCase("!=", Direction.Down, -2.0, false)] + [TestCase("!=", Direction.Down, -2.00001, true)] + [TestCase("!=", Direction.Down, -1.99999, true)] + [TestCase("!=", Direction.Down, 0.0, true)] + [TestCase("!=", Direction.Down, 2.0, true)] + [TestCase("!=", Direction.Flat, -2.0, true)] + [TestCase("!=", Direction.Flat, 0.0, false)] + [TestCase("!=", Direction.Flat, 0.00001, true)] + [TestCase("!=", Direction.Flat, -0.00001, true)] + [TestCase("!=", Direction.Flat, 2.0, true)] + [TestCase("!=", Direction.Up, -2.0, true)] + [TestCase("!=", Direction.Up, 0.0, true)] + [TestCase("!=", Direction.Up, 2.0, false)] + [TestCase("!=", Direction.Up, 2.00001, true)] + [TestCase("!=", Direction.Up, 1.99999, true)] + [TestCase("<", Direction.Down, -3.0, false)] + [TestCase("<", Direction.Down, -2.00001, false)] + [TestCase("<", Direction.Down, -2.0, false)] + [TestCase("<", Direction.Down, -1.99999, true)] + [TestCase("<", Direction.Down, 0.0, true)] + [TestCase("<", Direction.Down, 2.0, true)] + [TestCase("<", Direction.Flat, -2.0, false)] + [TestCase("<", Direction.Flat, -0.00001, false)] + [TestCase("<", Direction.Flat, 0.0, false)] + [TestCase("<", Direction.Flat, 0.00001, true)] + [TestCase("<", Direction.Flat, 2.0, true)] + [TestCase("<", Direction.Up, -2.0, false)] + [TestCase("<", Direction.Up, 0.0, false)] + [TestCase("<", Direction.Up, 1.99999, false)] + [TestCase("<", Direction.Up, 2.0, false)] + [TestCase("<", Direction.Up, 2.00001, true)] + [TestCase("<", Direction.Up, 3.0, true)] + [TestCase("<=", Direction.Down, -3.0, false)] + [TestCase("<=", Direction.Down, -2.00001, false)] + [TestCase("<=", Direction.Down, -2.0, true)] + [TestCase("<=", Direction.Down, -1.99999, true)] + [TestCase("<=", Direction.Down, 0.0, true)] + [TestCase("<=", Direction.Down, 2.0, true)] + [TestCase("<=", Direction.Flat, -2.0, false)] + [TestCase("<=", Direction.Flat, -0.00001, false)] + [TestCase("<=", Direction.Flat, 0.0, true)] + [TestCase("<=", Direction.Flat, 0.00001, true)] + [TestCase("<=", Direction.Flat, 2.0, true)] + [TestCase("<=", Direction.Up, -2.0, false)] + [TestCase("<=", Direction.Up, 0.0, false)] + [TestCase("<=", Direction.Up, 1.99999, false)] + [TestCase("<=", Direction.Up, 2.0, true)] + [TestCase("<=", Direction.Up, 2.00001, true)] + [TestCase("<=", Direction.Up, 3.0, true)] + [TestCase(">", Direction.Down, -3.0, true)] + [TestCase(">", Direction.Down, -2.00001, true)] + [TestCase(">", Direction.Down, -2.0, false)] + [TestCase(">", Direction.Down, -1.99999, false)] + [TestCase(">", Direction.Down, 0.0, false)] + [TestCase(">", Direction.Down, 2.0, false)] + [TestCase(">", Direction.Flat, -2.0, true)] + [TestCase(">", Direction.Flat, -0.00001, true)] + [TestCase(">", Direction.Flat, 0.0, false)] + [TestCase(">", Direction.Flat, 0.00001, false)] + [TestCase(">", Direction.Flat, 2.0, false)] + [TestCase(">", Direction.Up, -2.0, true)] + [TestCase(">", Direction.Up, 0.0, true)] + [TestCase(">", Direction.Up, 1.99999, true)] + [TestCase(">", Direction.Up, 2.0, false)] + [TestCase(">", Direction.Up, 2.00001, false)] + [TestCase(">", Direction.Up, 3.0, false)] + [TestCase(">=", Direction.Down, -3.0, true)] + [TestCase(">=", Direction.Down, -2.00001, true)] + [TestCase(">=", Direction.Down, -2.0, true)] + [TestCase(">=", Direction.Down, -1.99999, false)] + [TestCase(">=", Direction.Down, 0.0, false)] + [TestCase(">=", Direction.Down, 2.0, false)] + [TestCase(">=", Direction.Flat, -2.0, true)] + [TestCase(">=", Direction.Flat, -0.00001, true)] + [TestCase(">=", Direction.Flat, 0.0, true)] + [TestCase(">=", Direction.Flat, 0.00001, false)] + [TestCase(">=", Direction.Flat, 2.0, false)] + [TestCase(">=", Direction.Up, -2.0, true)] + [TestCase(">=", Direction.Up, 0.0, true)] + [TestCase(">=", Direction.Up, 1.99999, true)] + [TestCase(">=", Direction.Up, 2.0, true)] + [TestCase(">=", Direction.Up, 2.00001, false)] + [TestCase(">=", Direction.Up, 3.0, false)] + public void FloatComparisonOperatorsWorkWithoutExplicitCast(string @operator, Direction operand1, double operand2, bool expectedResult) + { + using var _ = Py.GIL(); + var module = GetTestOperatorsModule(@operator, operand1, operand2); + + Assert.AreEqual(expectedResult, module.InvokeMethod("operation1").As()); + + var invertedOperationExpectedResult = (@operator.StartsWith('<') || @operator.StartsWith('>')) && Convert.ToInt64(operand1) != operand2 + ? !expectedResult + : expectedResult; + Assert.AreEqual(invertedOperationExpectedResult, module.InvokeMethod("operation2").As()); + } + + public static IEnumerable SameEnumTypeComparisonOperatorsTestCases + { + get + { + var operators = new[] { "==", "!=", "<", "<=", ">", ">=" }; + var enumValues = Enum.GetValues(); + + foreach (var enumValue in enumValues) + { + foreach (var enumValue2 in enumValues) + { + yield return new TestCaseData("==", enumValue, enumValue2, enumValue == enumValue2); + yield return new TestCaseData("!=", enumValue, enumValue2, enumValue != enumValue2); + yield return new TestCaseData("<", enumValue, enumValue2, enumValue < enumValue2); + yield return new TestCaseData("<=", enumValue, enumValue2, enumValue <= enumValue2); + yield return new TestCaseData(">", enumValue, enumValue2, enumValue > enumValue2); + yield return new TestCaseData(">=", enumValue, enumValue2, enumValue >= enumValue2); + } + } + } + } + + [TestCaseSource(nameof(SameEnumTypeComparisonOperatorsTestCases))] + public void SameEnumTypeComparisonOperatorsWorkWithoutExplicitCast(string @operator, Direction operand1, Direction operand2, bool expectedResult) + { + using var _ = Py.GIL(); + var module = PyModule.FromString("SameEnumTypeComparisonOperatorsWorkWithoutExplicitCast", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +def operation(): + return {nameof(EnumTests)}.{nameof(Direction)}.{operand1} {@operator} {nameof(EnumTests)}.{nameof(Direction)}.{operand2} +"); + + Assert.AreEqual(expectedResult, module.InvokeMethod("operation").As()); + } + + [TestCase("==", Direction.Down, "Down", true)] + [TestCase("==", Direction.Down, "Flat", false)] + [TestCase("==", Direction.Down, "Up", false)] + [TestCase("==", Direction.Flat, "Down", false)] + [TestCase("==", Direction.Flat, "Flat", true)] + [TestCase("==", Direction.Flat, "Up", false)] + [TestCase("==", Direction.Up, "Down", false)] + [TestCase("==", Direction.Up, "Flat", false)] + [TestCase("==", Direction.Up, "Up", true)] + [TestCase("!=", Direction.Down, "Down", false)] + [TestCase("!=", Direction.Down, "Flat", true)] + [TestCase("!=", Direction.Down, "Up", true)] + [TestCase("!=", Direction.Flat, "Down", true)] + [TestCase("!=", Direction.Flat, "Flat", false)] + [TestCase("!=", Direction.Flat, "Up", true)] + [TestCase("!=", Direction.Up, "Down", true)] + [TestCase("!=", Direction.Up, "Flat", true)] + [TestCase("!=", Direction.Up, "Up", false)] + public void EnumComparisonOperatorsWorkWithString(string @operator, Direction operand1, string operand2, bool expectedResult) + { + using var _ = Py.GIL(); + var module = PyModule.FromString("EnumComparisonOperatorsWorkWithString", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +def operation1(): + return {nameof(EnumTests)}.{nameof(Direction)}.{operand1} {@operator} ""{operand2}"" + +def operation2(): + return ""{operand2}"" {@operator} {nameof(EnumTests)}.{nameof(Direction)}.{operand1} +"); + + Assert.AreEqual(expectedResult, module.InvokeMethod("operation1").As()); + Assert.AreEqual(expectedResult, module.InvokeMethod("operation2").As()); + } + } +} diff --git a/src/embed_tests/TestMethodBinder.cs b/src/embed_tests/TestMethodBinder.cs index 7f4c58d7e..0b3f6497c 100644 --- a/src/embed_tests/TestMethodBinder.cs +++ b/src/embed_tests/TestMethodBinder.cs @@ -815,6 +815,20 @@ public string VariableArgumentsMethod(params PyObject[] paramsParams) return "VariableArgumentsMethod(PyObject[])"; } + // ---- + + public string MethodWithEnumParam(SomeEnu enumValue, string symbol) + { + return $"MethodWithEnumParam With Enum"; + } + + public string MethodWithEnumParam(PyObject pyObject, string symbol) + { + return $"MethodWithEnumParam With PyObject"; + } + + // ---- + public string ConstructorMessage { get; set; } public OverloadsTestClass(params CSharpModel[] paramsParams) @@ -1117,6 +1131,26 @@ def get_instance(): Assert.AreEqual("OverloadsTestClass(PyObject[])", instance.GetAttr("ConstructorMessage").As()); } + [Test] + public void EnumHasPrecedenceOverPyObject() + { + using var _ = Py.GIL(); + + var module = PyModule.FromString("EnumHasPrecedenceOverPyObject", @$" +from clr import AddReference +AddReference(""System"") +from Python.EmbeddingTest import * + +class PythonModel(TestMethodBinder.CSharpModel): + pass + +def call_method(): + return TestMethodBinder.OverloadsTestClass().MethodWithEnumParam(TestMethodBinder.SomeEnu.A, ""Some string"") +"); + + var result = module.GetAttr("call_method").Invoke(); + Assert.AreEqual("MethodWithEnumParam With Enum", result.As()); + } // Used to test that we match this function with Py DateTime & Date Objects public static int GetMonth(DateTime test) diff --git a/src/runtime/Converter.cs b/src/runtime/Converter.cs index 19fb1c883..d0e97362a 100644 --- a/src/runtime/Converter.cs +++ b/src/runtime/Converter.cs @@ -226,6 +226,11 @@ internal static NewReference ToPython(object? value, Type type) return resultlist.NewReferenceOrNull(); } + if (type.IsEnum) + { + return CLRObject.GetReference(value, type); + } + // it the type is a python subclass of a managed type then return the // underlying python object rather than construct a new wrapper object. var pyderived = value as IPythonDerivedType; diff --git a/src/runtime/MethodBinder.cs b/src/runtime/MethodBinder.cs index 8c8bac65d..42fe0ba91 100644 --- a/src/runtime/MethodBinder.cs +++ b/src/runtime/MethodBinder.cs @@ -383,6 +383,17 @@ internal static int ArgPrecedence(Type t, bool isOperatorMethod) return 3000; } + if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + // Nullable is a special case, we treat it as the underlying type + return ArgPrecedence(Nullable.GetUnderlyingType(t), isOperatorMethod); + } + + if (t.IsEnum) + { + return -2; + } + if (t.IsAssignableFrom(typeof(PyObject)) && !isOperatorMethod) { return -1; diff --git a/src/runtime/Util/OpsHelper.cs b/src/runtime/Util/OpsHelper.cs index ab623f3de..93e8146e4 100644 --- a/src/runtime/Util/OpsHelper.cs +++ b/src/runtime/Util/OpsHelper.cs @@ -1,7 +1,6 @@ using System; using System.Linq.Expressions; using System.Reflection; - using static Python.Runtime.OpsHelper; namespace Python.Runtime @@ -35,7 +34,7 @@ public static Expression EnumUnderlyingValue(Expression enumValue) } [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - internal class OpsAttribute: Attribute { } + internal class OpsAttribute : Attribute { } [Ops] internal static class FlagEnumOps where T : Enum @@ -85,5 +84,400 @@ public static PyInt __int__(T value) => typeof(T).GetEnumUnderlyingType() == typeof(UInt64) ? new PyInt(Convert.ToUInt64(value)) : new PyInt(Convert.ToInt64(value)); + + #region Arithmetic operators + + public static double op_Addition(T a, double b) + { + if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + { + return Convert.ToUInt64(a) + b; + } + return Convert.ToInt64(a) + b; + } + + public static double op_Addition(double a, T b) + { + return op_Addition(b, a); + } + + public static double op_Subtraction(T a, double b) + { + if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + { + return Convert.ToUInt64(a) - b; + } + return Convert.ToInt64(a) - b; + } + + public static double op_Subtraction(double a, T b) + { + return op_Subtraction(b, a); + } + + public static double op_Multiply(T a, double b) + { + if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + { + return Convert.ToUInt64(a) * b; + } + return Convert.ToInt64(a) * b; + } + + public static double op_Multiply(double a, T b) + { + return op_Multiply(b, a); + } + + public static double op_Division(T a, double b) + { + if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + { + return Convert.ToUInt64(a) / b; + } + return Convert.ToInt64(a) / b; + } + + public static double op_Division(double a, T b) + { + return op_Division(b, a); + } + + #endregion + + #region Int comparison operators + + public static bool op_Equality(T a, long b) + { + if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + { + var uvalue = Convert.ToUInt64(a); + return b >= 0 && ((ulong)b) == uvalue; + } + return Convert.ToInt64(a) == b; + } + + public static bool op_Equality(T a, ulong b) + { + if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + { + var uvalue = Convert.ToUInt64(a); + return b == uvalue; + } + var ivalue = Convert.ToInt64(a); + return ivalue >= 0 && ((ulong)ivalue) == b; + } + + public static bool op_Equality(long a, T b) + { + return op_Equality(b, a); + } + + public static bool op_Equality(ulong a, T b) + { + return op_Equality(b, a); + } + + public static bool op_Inequality(T a, long b) + { + return !op_Equality(a, b); + } + + public static bool op_Inequality(T a, ulong b) + { + return !op_Equality(a, b); + } + + public static bool op_Inequality(long a, T b) + { + return !op_Equality(b, a); + } + + public static bool op_Inequality(ulong a, T b) + { + return !op_Equality(b, a); + } + + public static bool op_LessThan(T a, long b) + { + if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + { + var uvalue = Convert.ToUInt64(a); + return b >= 0 && ((ulong)b) > uvalue; + } + return Convert.ToInt64(a) < b; + } + + public static bool op_LessThan(T a, ulong b) + { + if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + { + var uvalue = Convert.ToUInt64(a); + return b > uvalue; + } + var ivalue = Convert.ToInt64(a); + return ivalue >= 0 && ((ulong)ivalue) < b; + } + + public static bool op_LessThan(long a, T b) + { + return op_GreaterThan(b, a); + } + + public static bool op_LessThan(ulong a, T b) + { + return op_GreaterThan(b, a); + } + + public static bool op_GreaterThan(T a, long b) + { + if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + { + var uvalue = Convert.ToUInt64(a); + return b >= 0 && ((ulong)b) < uvalue; + } + return Convert.ToInt64(a) > b; + } + + public static bool op_GreaterThan(T a, ulong b) + { + if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + { + var uvalue = Convert.ToUInt64(a); + return b < uvalue; + } + var ivalue = Convert.ToInt64(a); + return ivalue >= 0 && ((ulong)ivalue) > b; + } + + public static bool op_GreaterThan(long a, T b) + { + return op_LessThan(b, a); + } + + public static bool op_GreaterThan(ulong a, T b) + { + return op_LessThan(b, a); + } + + public static bool op_LessThanOrEqual(T a, long b) + { + if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + { + var uvalue = Convert.ToUInt64(a); + return b >= 0 && ((ulong)b) >= uvalue; + } + return Convert.ToInt64(a) <= b; + } + + public static bool op_LessThanOrEqual(T a, ulong b) + { + if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + { + var uvalue = Convert.ToUInt64(a); + return b >= uvalue; + } + var ivalue = Convert.ToInt64(a); + return ivalue >= 0 && ((ulong)ivalue) <= b; + } + + public static bool op_LessThanOrEqual(long a, T b) + { + return op_GreaterThanOrEqual(b, a); + } + + public static bool op_LessThanOrEqual(ulong a, T b) + { + return op_GreaterThanOrEqual(b, a); + } + + public static bool op_GreaterThanOrEqual(T a, long b) + { + if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + { + var uvalue = Convert.ToUInt64(a); + return b >= 0 && ((ulong)b) <= uvalue; + } + return Convert.ToInt64(a) >= b; + } + + public static bool op_GreaterThanOrEqual(T a, ulong b) + { + if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + { + var uvalue = Convert.ToUInt64(a); + return b <= uvalue; + } + var ivalue = Convert.ToInt64(a); + return ivalue >= 0 && ((ulong)ivalue) >= b; + } + + public static bool op_GreaterThanOrEqual(long a, T b) + { + return op_LessThanOrEqual(b, a); + } + + public static bool op_GreaterThanOrEqual(ulong a, T b) + { + return op_LessThanOrEqual(b, a); + } + + #endregion + + #region Double comparison operators + + public static bool op_Equality(T a, double b) + { + if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + { + return Convert.ToUInt64(a) == b; + } + return Convert.ToInt64(a) == b; + } + + public static bool op_Equality(double a, T b) + { + return op_Equality(b, a); + } + + public static bool op_Inequality(T a, double b) + { + return !op_Equality(a, b); + } + + public static bool op_Inequality(double a, T b) + { + return !op_Equality(b, a); + } + + public static bool op_LessThan(T a, double b) + { + if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + { + return Convert.ToUInt64(a) < b; + } + return Convert.ToInt64(a) < b; + } + + public static bool op_LessThan(double a, T b) + { + return op_GreaterThan(b, a); + } + + public static bool op_GreaterThan(T a, double b) + { + if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + { + return Convert.ToUInt64(a) > b; + } + return Convert.ToInt64(a) > b; + } + + public static bool op_GreaterThan(double a, T b) + { + return op_LessThan(b, a); + } + + public static bool op_LessThanOrEqual(T a, double b) + { + if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + { + return Convert.ToUInt64(a) <= b; + } + return Convert.ToInt64(a) <= b; + } + + public static bool op_LessThanOrEqual(double a, T b) + { + return op_GreaterThanOrEqual(b, a); + } + + public static bool op_GreaterThanOrEqual(T a, double b) + { + if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + { + return Convert.ToUInt64(a) >= b; + } + return Convert.ToInt64(a) >= b; + } + + public static bool op_GreaterThanOrEqual(double a, T b) + { + return op_LessThanOrEqual(b, a); + } + + #endregion + + #region Same type comparison operators + + public static bool op_Equality(T a, T b) + { + return a.Equals(b); + } + + public static bool op_Inequality(T a, T b) + { + return !a.Equals(b); + } + + public static bool op_LessThan(T a, T b) + { + if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + { + return Convert.ToUInt64(a) < Convert.ToUInt64(b); + } + return Convert.ToInt64(a) < Convert.ToInt64(b); + } + + public static bool op_GreaterThan(T a, T b) + { + if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + { + return Convert.ToUInt64(a) > Convert.ToUInt64(b); + } + return Convert.ToInt64(a) > Convert.ToInt64(b); + } + + public static bool op_LessThanOrEqual(T a, T b) + { + if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + { + return Convert.ToUInt64(a) <= Convert.ToUInt64(b); + } + return Convert.ToInt64(a) <= Convert.ToInt64(b); + } + + public static bool op_GreaterThanOrEqual(T a, T b) + { + if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + { + return Convert.ToUInt64(a) >= Convert.ToUInt64(b); + } + return Convert.ToInt64(a) >= Convert.ToInt64(b); + } + + #endregion + + #region String comparison operators + public static bool op_Equality(T a, string b) + { + return a.ToString().Equals(b, StringComparison.InvariantCultureIgnoreCase); + } + public static bool op_Equality(string a, T b) + { + return op_Equality(b, a); + } + + public static bool op_Inequality(T a, string b) + { + return !op_Equality(a, b); + } + + public static bool op_Inequality(string a, T b) + { + return !op_Equality(b, a); + } + + #endregion } } From f45f92d188b6a86a85ea1aae174a2131fc80d677 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 29 Jul 2025 08:59:54 -0400 Subject: [PATCH 02/10] Minor fixes in substraction and division operators --- src/embed_tests/EnumTests.cs | 44 +++++++++++++++++-------- src/runtime/Util/OpsHelper.cs | 62 ++++++++++++++++++++--------------- 2 files changed, 66 insertions(+), 40 deletions(-) diff --git a/src/embed_tests/EnumTests.cs b/src/embed_tests/EnumTests.cs index 69123ea5d..fd1974ae0 100644 --- a/src/embed_tests/EnumTests.cs +++ b/src/embed_tests/EnumTests.cs @@ -66,25 +66,41 @@ def operation2(): "); } - [TestCase(" *", Direction.Down, 2, -4)] - [TestCase("/", Direction.Down, 2, -1)] - [TestCase("+", Direction.Down, 2, 0)] - [TestCase("-", Direction.Down, 2, -4)] - [TestCase("*", Direction.Flat, 2, 0)] - [TestCase("/", Direction.Flat, 2, 0)] - [TestCase("+", Direction.Flat, 2, 2)] - [TestCase("-", Direction.Flat, 2, -2)] - [TestCase("*", Direction.Up, 2, 4)] - [TestCase("/", Direction.Up, 2, 1)] - [TestCase("+", Direction.Up, 2, 4)] - [TestCase("-", Direction.Up, 2, 0)] - public void ArithmeticOperatorsWorkWithoutExplicitCast(string @operator, Direction operand1, double operand2, double expectedResult) + [TestCase("*", Direction.Down, 2, -4, -4)] + [TestCase("/", Direction.Down, 2, -1, -1)] + [TestCase("+", Direction.Down, 2, 0, 0)] + [TestCase("-", Direction.Down, 2, -4, 4)] + [TestCase("*", Direction.Flat, 2, 0, 0)] + [TestCase("/", Direction.Flat, 2, 0, 0)] + [TestCase("+", Direction.Flat, 2, 2, 2)] + [TestCase("-", Direction.Flat, 2, -2, 2)] + [TestCase("*", Direction.Up, 2, 4, 4)] + [TestCase("/", Direction.Up, 2, 1, 1)] + [TestCase("+", Direction.Up, 2, 4, 4)] + [TestCase("-", Direction.Up, 2, 0, 0)] + [TestCase("*", Direction.Down, -2, 4, 4)] + [TestCase("/", Direction.Down, -2, 1, 1)] + [TestCase("+", Direction.Down, -2, -4, -4)] + [TestCase("-", Direction.Down, -2, 0, 0)] + [TestCase("*", Direction.Flat, -2, 0, 0)] + [TestCase("/", Direction.Flat, -2, 0, 0)] + [TestCase("+", Direction.Flat, -2, -2, -2)] + [TestCase("-", Direction.Flat, -2, 2, -2)] + [TestCase("*", Direction.Up, -2, -4, -4)] + [TestCase("/", Direction.Up, -2, -1, -1)] + [TestCase("+", Direction.Up, -2, 0, 0)] + [TestCase("-", Direction.Up, -2, 4, -4)] + public void ArithmeticOperatorsWorkWithoutExplicitCast(string @operator, Direction operand1, double operand2, double expectedResult, double invertedOperationExpectedResult) { using var _ = Py.GIL(); var module = GetTestOperatorsModule(@operator, operand1, operand2); Assert.AreEqual(expectedResult, module.InvokeMethod("operation1").As()); - Assert.AreEqual(expectedResult, module.InvokeMethod("operation2").As()); + + if (Convert.ToInt64(operand1) != 0 || @operator != "/") + { + Assert.AreEqual(invertedOperationExpectedResult, module.InvokeMethod("operation2").As()); + } } [TestCase("==", Direction.Down, -2, true)] diff --git a/src/runtime/Util/OpsHelper.cs b/src/runtime/Util/OpsHelper.cs index 93e8146e4..3d628d3de 100644 --- a/src/runtime/Util/OpsHelper.cs +++ b/src/runtime/Util/OpsHelper.cs @@ -77,11 +77,13 @@ static Func UnaryOp(Func op) [Ops] internal static class EnumOps where T : Enum { + private static bool IsUnsigned = typeof(T).GetEnumUnderlyingType() == typeof(UInt64); + [ForbidPythonThreads] #pragma warning disable IDE1006 // Naming Styles - must match Python public static PyInt __int__(T value) #pragma warning restore IDE1006 // Naming Styles - => typeof(T).GetEnumUnderlyingType() == typeof(UInt64) + => IsUnsigned ? new PyInt(Convert.ToUInt64(value)) : new PyInt(Convert.ToInt64(value)); @@ -89,7 +91,7 @@ public static PyInt __int__(T value) public static double op_Addition(T a, double b) { - if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + if (IsUnsigned) { return Convert.ToUInt64(a) + b; } @@ -103,7 +105,7 @@ public static double op_Addition(double a, T b) public static double op_Subtraction(T a, double b) { - if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + if (IsUnsigned) { return Convert.ToUInt64(a) - b; } @@ -112,12 +114,16 @@ public static double op_Subtraction(T a, double b) public static double op_Subtraction(double a, T b) { - return op_Subtraction(b, a); + if (IsUnsigned) + { + return a - Convert.ToUInt64(b); + } + return a - Convert.ToInt64(b); } public static double op_Multiply(T a, double b) { - if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + if (IsUnsigned) { return Convert.ToUInt64(a) * b; } @@ -131,7 +137,7 @@ public static double op_Multiply(double a, T b) public static double op_Division(T a, double b) { - if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + if (IsUnsigned) { return Convert.ToUInt64(a) / b; } @@ -140,7 +146,11 @@ public static double op_Division(T a, double b) public static double op_Division(double a, T b) { - return op_Division(b, a); + if (IsUnsigned) + { + return a / Convert.ToUInt64(b); + } + return a / Convert.ToInt64(b); } #endregion @@ -149,7 +159,7 @@ public static double op_Division(double a, T b) public static bool op_Equality(T a, long b) { - if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + if (IsUnsigned) { var uvalue = Convert.ToUInt64(a); return b >= 0 && ((ulong)b) == uvalue; @@ -159,7 +169,7 @@ public static bool op_Equality(T a, long b) public static bool op_Equality(T a, ulong b) { - if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + if (IsUnsigned) { var uvalue = Convert.ToUInt64(a); return b == uvalue; @@ -200,7 +210,7 @@ public static bool op_Inequality(ulong a, T b) public static bool op_LessThan(T a, long b) { - if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + if (IsUnsigned) { var uvalue = Convert.ToUInt64(a); return b >= 0 && ((ulong)b) > uvalue; @@ -210,7 +220,7 @@ public static bool op_LessThan(T a, long b) public static bool op_LessThan(T a, ulong b) { - if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + if (IsUnsigned) { var uvalue = Convert.ToUInt64(a); return b > uvalue; @@ -231,7 +241,7 @@ public static bool op_LessThan(ulong a, T b) public static bool op_GreaterThan(T a, long b) { - if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + if (IsUnsigned) { var uvalue = Convert.ToUInt64(a); return b >= 0 && ((ulong)b) < uvalue; @@ -241,7 +251,7 @@ public static bool op_GreaterThan(T a, long b) public static bool op_GreaterThan(T a, ulong b) { - if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + if (IsUnsigned) { var uvalue = Convert.ToUInt64(a); return b < uvalue; @@ -262,7 +272,7 @@ public static bool op_GreaterThan(ulong a, T b) public static bool op_LessThanOrEqual(T a, long b) { - if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + if (IsUnsigned) { var uvalue = Convert.ToUInt64(a); return b >= 0 && ((ulong)b) >= uvalue; @@ -272,7 +282,7 @@ public static bool op_LessThanOrEqual(T a, long b) public static bool op_LessThanOrEqual(T a, ulong b) { - if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + if (IsUnsigned) { var uvalue = Convert.ToUInt64(a); return b >= uvalue; @@ -293,7 +303,7 @@ public static bool op_LessThanOrEqual(ulong a, T b) public static bool op_GreaterThanOrEqual(T a, long b) { - if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + if (IsUnsigned) { var uvalue = Convert.ToUInt64(a); return b >= 0 && ((ulong)b) <= uvalue; @@ -303,7 +313,7 @@ public static bool op_GreaterThanOrEqual(T a, long b) public static bool op_GreaterThanOrEqual(T a, ulong b) { - if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + if (IsUnsigned) { var uvalue = Convert.ToUInt64(a); return b <= uvalue; @@ -328,7 +338,7 @@ public static bool op_GreaterThanOrEqual(ulong a, T b) public static bool op_Equality(T a, double b) { - if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + if (IsUnsigned) { return Convert.ToUInt64(a) == b; } @@ -352,7 +362,7 @@ public static bool op_Inequality(double a, T b) public static bool op_LessThan(T a, double b) { - if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + if (IsUnsigned) { return Convert.ToUInt64(a) < b; } @@ -366,7 +376,7 @@ public static bool op_LessThan(double a, T b) public static bool op_GreaterThan(T a, double b) { - if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + if (IsUnsigned) { return Convert.ToUInt64(a) > b; } @@ -380,7 +390,7 @@ public static bool op_GreaterThan(double a, T b) public static bool op_LessThanOrEqual(T a, double b) { - if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + if (IsUnsigned) { return Convert.ToUInt64(a) <= b; } @@ -394,7 +404,7 @@ public static bool op_LessThanOrEqual(double a, T b) public static bool op_GreaterThanOrEqual(T a, double b) { - if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + if (IsUnsigned) { return Convert.ToUInt64(a) >= b; } @@ -422,7 +432,7 @@ public static bool op_Inequality(T a, T b) public static bool op_LessThan(T a, T b) { - if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + if (IsUnsigned) { return Convert.ToUInt64(a) < Convert.ToUInt64(b); } @@ -431,7 +441,7 @@ public static bool op_LessThan(T a, T b) public static bool op_GreaterThan(T a, T b) { - if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + if (IsUnsigned) { return Convert.ToUInt64(a) > Convert.ToUInt64(b); } @@ -440,7 +450,7 @@ public static bool op_GreaterThan(T a, T b) public static bool op_LessThanOrEqual(T a, T b) { - if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + if (IsUnsigned) { return Convert.ToUInt64(a) <= Convert.ToUInt64(b); } @@ -449,7 +459,7 @@ public static bool op_LessThanOrEqual(T a, T b) public static bool op_GreaterThanOrEqual(T a, T b) { - if (typeof(T).GetEnumUnderlyingType() == typeof(UInt64)) + if (IsUnsigned) { return Convert.ToUInt64(a) >= Convert.ToUInt64(b); } From c5be5f20af43d7f53ac8d58e8e31801378ba34a4 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 29 Jul 2025 09:15:26 -0400 Subject: [PATCH 03/10] Bump version to 2.0.45 --- 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 ee239ff12..aa3a04adb 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 c3e7c304f..6941d1ac1 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.44")] -[assembly: AssemblyFileVersion("2.0.44")] +[assembly: AssemblyVersion("2.0.45")] +[assembly: AssemblyFileVersion("2.0.45")] diff --git a/src/runtime/Python.Runtime.csproj b/src/runtime/Python.Runtime.csproj index 9b870ed44..035bc6214 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.44 + 2.0.45 false LICENSE https://github.com/pythonnet/pythonnet From 84a1be3deebd86407a601d2c5aa74c3537804a43 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Tue, 29 Jul 2025 13:15:48 -0400 Subject: [PATCH 04/10] Support enum comparison to other enum types Compare based on the underlying int value --- src/embed_tests/EnumTests.cs | 489 +++++++++++++++++++--------------- src/runtime/Util/OpsHelper.cs | 158 +++++++++++ 2 files changed, 430 insertions(+), 217 deletions(-) diff --git a/src/embed_tests/EnumTests.cs b/src/embed_tests/EnumTests.cs index fd1974ae0..56732e2ea 100644 --- a/src/embed_tests/EnumTests.cs +++ b/src/embed_tests/EnumTests.cs @@ -21,13 +21,20 @@ public void Dispose() PythonEngine.Shutdown(); } - public enum Direction + public enum VerticalDirection { Down = -2, Flat = 0, Up = 2, } + public enum HorizontalDirection + { + Left = -2, + Flat = 0, + Right = 2, + } + [Test] public void CSharpEnumsBehaveAsEnumsInPython() { @@ -38,20 +45,20 @@ from clr import AddReference from Python.EmbeddingTest import * -def enum_is_right_type(enum_value=EnumTests.Direction.Up): - return isinstance(enum_value, EnumTests.Direction) +def enum_is_right_type(enum_value={nameof(EnumTests)}.{nameof(VerticalDirection)}.{nameof(VerticalDirection.Up)}): + return isinstance(enum_value, {nameof(EnumTests)}.{nameof(VerticalDirection)}) "); Assert.IsTrue(module.InvokeMethod("enum_is_right_type").As()); // Also test passing the enum value from C# to Python - using var pyEnumValue = Direction.Up.ToPython(); + using var pyEnumValue = VerticalDirection.Up.ToPython(); Assert.IsTrue(module.InvokeMethod("enum_is_right_type", pyEnumValue).As()); } - private PyModule GetTestOperatorsModule(string @operator, Direction operand1, double operand2) + private PyModule GetTestOperatorsModule(string @operator, VerticalDirection operand1, double operand2) { - var operand1Str = $"{nameof(EnumTests)}.{nameof(Direction)}.{operand1}"; + var operand1Str = $"{nameof(EnumTests)}.{nameof(VerticalDirection)}.{operand1}"; return PyModule.FromString("GetTestOperatorsModule", $@" from clr import AddReference AddReference(""Python.EmbeddingTest"") @@ -66,31 +73,31 @@ def operation2(): "); } - [TestCase("*", Direction.Down, 2, -4, -4)] - [TestCase("/", Direction.Down, 2, -1, -1)] - [TestCase("+", Direction.Down, 2, 0, 0)] - [TestCase("-", Direction.Down, 2, -4, 4)] - [TestCase("*", Direction.Flat, 2, 0, 0)] - [TestCase("/", Direction.Flat, 2, 0, 0)] - [TestCase("+", Direction.Flat, 2, 2, 2)] - [TestCase("-", Direction.Flat, 2, -2, 2)] - [TestCase("*", Direction.Up, 2, 4, 4)] - [TestCase("/", Direction.Up, 2, 1, 1)] - [TestCase("+", Direction.Up, 2, 4, 4)] - [TestCase("-", Direction.Up, 2, 0, 0)] - [TestCase("*", Direction.Down, -2, 4, 4)] - [TestCase("/", Direction.Down, -2, 1, 1)] - [TestCase("+", Direction.Down, -2, -4, -4)] - [TestCase("-", Direction.Down, -2, 0, 0)] - [TestCase("*", Direction.Flat, -2, 0, 0)] - [TestCase("/", Direction.Flat, -2, 0, 0)] - [TestCase("+", Direction.Flat, -2, -2, -2)] - [TestCase("-", Direction.Flat, -2, 2, -2)] - [TestCase("*", Direction.Up, -2, -4, -4)] - [TestCase("/", Direction.Up, -2, -1, -1)] - [TestCase("+", Direction.Up, -2, 0, 0)] - [TestCase("-", Direction.Up, -2, 4, -4)] - public void ArithmeticOperatorsWorkWithoutExplicitCast(string @operator, Direction operand1, double operand2, double expectedResult, double invertedOperationExpectedResult) + [TestCase("*", VerticalDirection.Down, 2, -4, -4)] + [TestCase("/", VerticalDirection.Down, 2, -1, -1)] + [TestCase("+", VerticalDirection.Down, 2, 0, 0)] + [TestCase("-", VerticalDirection.Down, 2, -4, 4)] + [TestCase("*", VerticalDirection.Flat, 2, 0, 0)] + [TestCase("/", VerticalDirection.Flat, 2, 0, 0)] + [TestCase("+", VerticalDirection.Flat, 2, 2, 2)] + [TestCase("-", VerticalDirection.Flat, 2, -2, 2)] + [TestCase("*", VerticalDirection.Up, 2, 4, 4)] + [TestCase("/", VerticalDirection.Up, 2, 1, 1)] + [TestCase("+", VerticalDirection.Up, 2, 4, 4)] + [TestCase("-", VerticalDirection.Up, 2, 0, 0)] + [TestCase("*", VerticalDirection.Down, -2, 4, 4)] + [TestCase("/", VerticalDirection.Down, -2, 1, 1)] + [TestCase("+", VerticalDirection.Down, -2, -4, -4)] + [TestCase("-", VerticalDirection.Down, -2, 0, 0)] + [TestCase("*", VerticalDirection.Flat, -2, 0, 0)] + [TestCase("/", VerticalDirection.Flat, -2, 0, 0)] + [TestCase("+", VerticalDirection.Flat, -2, -2, -2)] + [TestCase("-", VerticalDirection.Flat, -2, 2, -2)] + [TestCase("*", VerticalDirection.Up, -2, -4, -4)] + [TestCase("/", VerticalDirection.Up, -2, -1, -1)] + [TestCase("+", VerticalDirection.Up, -2, 0, 0)] + [TestCase("-", VerticalDirection.Up, -2, 4, -4)] + public void ArithmeticOperatorsWorkWithoutExplicitCast(string @operator, VerticalDirection operand1, double operand2, double expectedResult, double invertedOperationExpectedResult) { using var _ = Py.GIL(); var module = GetTestOperatorsModule(@operator, operand1, operand2); @@ -103,69 +110,69 @@ public void ArithmeticOperatorsWorkWithoutExplicitCast(string @operator, Directi } } - [TestCase("==", Direction.Down, -2, true)] - [TestCase("==", Direction.Down, 0, false)] - [TestCase("==", Direction.Down, 2, false)] - [TestCase("==", Direction.Flat, -2, false)] - [TestCase("==", Direction.Flat, 0, true)] - [TestCase("==", Direction.Flat, 2, false)] - [TestCase("==", Direction.Up, -2, false)] - [TestCase("==", Direction.Up, 0, false)] - [TestCase("==", Direction.Up, 2, true)] - [TestCase("!=", Direction.Down, -2, false)] - [TestCase("!=", Direction.Down, 0, true)] - [TestCase("!=", Direction.Down, 2, true)] - [TestCase("!=", Direction.Flat, -2, true)] - [TestCase("!=", Direction.Flat, 0, false)] - [TestCase("!=", Direction.Flat, 2, true)] - [TestCase("!=", Direction.Up, -2, true)] - [TestCase("!=", Direction.Up, 0, true)] - [TestCase("!=", Direction.Up, 2, false)] - [TestCase("<", Direction.Down, -3, false)] - [TestCase("<", Direction.Down, -2, false)] - [TestCase("<", Direction.Down, 0, true)] - [TestCase("<", Direction.Down, 2, true)] - [TestCase("<", Direction.Flat, -2, false)] - [TestCase("<", Direction.Flat, 0, false)] - [TestCase("<", Direction.Flat, 2, true)] - [TestCase("<", Direction.Up, -2, false)] - [TestCase("<", Direction.Up, 0, false)] - [TestCase("<", Direction.Up, 2, false)] - [TestCase("<", Direction.Up, 3, true)] - [TestCase("<=", Direction.Down, -3, false)] - [TestCase("<=", Direction.Down, -2, true)] - [TestCase("<=", Direction.Down, 0, true)] - [TestCase("<=", Direction.Down, 2, true)] - [TestCase("<=", Direction.Flat, -2, false)] - [TestCase("<=", Direction.Flat, 0, true)] - [TestCase("<=", Direction.Flat, 2, true)] - [TestCase("<=", Direction.Up, -2, false)] - [TestCase("<=", Direction.Up, 0, false)] - [TestCase("<=", Direction.Up, 2, true)] - [TestCase("<=", Direction.Up, 3, true)] - [TestCase(">", Direction.Down, -3, true)] - [TestCase(">", Direction.Down, -2, false)] - [TestCase(">", Direction.Down, 0, false)] - [TestCase(">", Direction.Down, 2, false)] - [TestCase(">", Direction.Flat, -2, true)] - [TestCase(">", Direction.Flat, 0, false)] - [TestCase(">", Direction.Flat, 2, false)] - [TestCase(">", Direction.Up, -2, true)] - [TestCase(">", Direction.Up, 0, true)] - [TestCase(">", Direction.Up, 2, false)] - [TestCase(">", Direction.Up, 3, false)] - [TestCase(">=", Direction.Down, -3, true)] - [TestCase(">=", Direction.Down, -2, true)] - [TestCase(">=", Direction.Down, 0, false)] - [TestCase(">=", Direction.Down, 2, false)] - [TestCase(">=", Direction.Flat, -2, true)] - [TestCase(">=", Direction.Flat, 0, true)] - [TestCase(">=", Direction.Flat, 2, false)] - [TestCase(">=", Direction.Up, -2, true)] - [TestCase(">=", Direction.Up, 0, true)] - [TestCase(">=", Direction.Up, 2, true)] - [TestCase(">=", Direction.Up, 3, false)] - public void IntComparisonOperatorsWorkWithoutExplicitCast(string @operator, Direction operand1, int operand2, bool expectedResult) + [TestCase("==", VerticalDirection.Down, -2, true)] + [TestCase("==", VerticalDirection.Down, 0, false)] + [TestCase("==", VerticalDirection.Down, 2, false)] + [TestCase("==", VerticalDirection.Flat, -2, false)] + [TestCase("==", VerticalDirection.Flat, 0, true)] + [TestCase("==", VerticalDirection.Flat, 2, false)] + [TestCase("==", VerticalDirection.Up, -2, false)] + [TestCase("==", VerticalDirection.Up, 0, false)] + [TestCase("==", VerticalDirection.Up, 2, true)] + [TestCase("!=", VerticalDirection.Down, -2, false)] + [TestCase("!=", VerticalDirection.Down, 0, true)] + [TestCase("!=", VerticalDirection.Down, 2, true)] + [TestCase("!=", VerticalDirection.Flat, -2, true)] + [TestCase("!=", VerticalDirection.Flat, 0, false)] + [TestCase("!=", VerticalDirection.Flat, 2, true)] + [TestCase("!=", VerticalDirection.Up, -2, true)] + [TestCase("!=", VerticalDirection.Up, 0, true)] + [TestCase("!=", VerticalDirection.Up, 2, false)] + [TestCase("<", VerticalDirection.Down, -3, false)] + [TestCase("<", VerticalDirection.Down, -2, false)] + [TestCase("<", VerticalDirection.Down, 0, true)] + [TestCase("<", VerticalDirection.Down, 2, true)] + [TestCase("<", VerticalDirection.Flat, -2, false)] + [TestCase("<", VerticalDirection.Flat, 0, false)] + [TestCase("<", VerticalDirection.Flat, 2, true)] + [TestCase("<", VerticalDirection.Up, -2, false)] + [TestCase("<", VerticalDirection.Up, 0, false)] + [TestCase("<", VerticalDirection.Up, 2, false)] + [TestCase("<", VerticalDirection.Up, 3, true)] + [TestCase("<=", VerticalDirection.Down, -3, false)] + [TestCase("<=", VerticalDirection.Down, -2, true)] + [TestCase("<=", VerticalDirection.Down, 0, true)] + [TestCase("<=", VerticalDirection.Down, 2, true)] + [TestCase("<=", VerticalDirection.Flat, -2, false)] + [TestCase("<=", VerticalDirection.Flat, 0, true)] + [TestCase("<=", VerticalDirection.Flat, 2, true)] + [TestCase("<=", VerticalDirection.Up, -2, false)] + [TestCase("<=", VerticalDirection.Up, 0, false)] + [TestCase("<=", VerticalDirection.Up, 2, true)] + [TestCase("<=", VerticalDirection.Up, 3, true)] + [TestCase(">", VerticalDirection.Down, -3, true)] + [TestCase(">", VerticalDirection.Down, -2, false)] + [TestCase(">", VerticalDirection.Down, 0, false)] + [TestCase(">", VerticalDirection.Down, 2, false)] + [TestCase(">", VerticalDirection.Flat, -2, true)] + [TestCase(">", VerticalDirection.Flat, 0, false)] + [TestCase(">", VerticalDirection.Flat, 2, false)] + [TestCase(">", VerticalDirection.Up, -2, true)] + [TestCase(">", VerticalDirection.Up, 0, true)] + [TestCase(">", VerticalDirection.Up, 2, false)] + [TestCase(">", VerticalDirection.Up, 3, false)] + [TestCase(">=", VerticalDirection.Down, -3, true)] + [TestCase(">=", VerticalDirection.Down, -2, true)] + [TestCase(">=", VerticalDirection.Down, 0, false)] + [TestCase(">=", VerticalDirection.Down, 2, false)] + [TestCase(">=", VerticalDirection.Flat, -2, true)] + [TestCase(">=", VerticalDirection.Flat, 0, true)] + [TestCase(">=", VerticalDirection.Flat, 2, false)] + [TestCase(">=", VerticalDirection.Up, -2, true)] + [TestCase(">=", VerticalDirection.Up, 0, true)] + [TestCase(">=", VerticalDirection.Up, 2, true)] + [TestCase(">=", VerticalDirection.Up, 3, false)] + public void IntComparisonOperatorsWorkWithoutExplicitCast(string @operator, VerticalDirection operand1, int operand2, bool expectedResult) { using var _ = Py.GIL(); var module = GetTestOperatorsModule(@operator, operand1, operand2); @@ -178,105 +185,105 @@ public void IntComparisonOperatorsWorkWithoutExplicitCast(string @operator, Dire Assert.AreEqual(invertedOperationExpectedResult, module.InvokeMethod("operation2").As()); } - [TestCase("==", Direction.Down, -2.0, true)] - [TestCase("==", Direction.Down, -2.00001, false)] - [TestCase("==", Direction.Down, -1.99999, false)] - [TestCase("==", Direction.Down, 0.0, false)] - [TestCase("==", Direction.Down, 2.0, false)] - [TestCase("==", Direction.Flat, -2.0, false)] - [TestCase("==", Direction.Flat, 0.0, true)] - [TestCase("==", Direction.Flat, 0.00001, false)] - [TestCase("==", Direction.Flat, -0.00001, false)] - [TestCase("==", Direction.Flat, 2.0, false)] - [TestCase("==", Direction.Up, -2.0, false)] - [TestCase("==", Direction.Up, 0.0, false)] - [TestCase("==", Direction.Up, 2.0, true)] - [TestCase("==", Direction.Up, 2.00001, false)] - [TestCase("==", Direction.Up, 1.99999, false)] - [TestCase("!=", Direction.Down, -2.0, false)] - [TestCase("!=", Direction.Down, -2.00001, true)] - [TestCase("!=", Direction.Down, -1.99999, true)] - [TestCase("!=", Direction.Down, 0.0, true)] - [TestCase("!=", Direction.Down, 2.0, true)] - [TestCase("!=", Direction.Flat, -2.0, true)] - [TestCase("!=", Direction.Flat, 0.0, false)] - [TestCase("!=", Direction.Flat, 0.00001, true)] - [TestCase("!=", Direction.Flat, -0.00001, true)] - [TestCase("!=", Direction.Flat, 2.0, true)] - [TestCase("!=", Direction.Up, -2.0, true)] - [TestCase("!=", Direction.Up, 0.0, true)] - [TestCase("!=", Direction.Up, 2.0, false)] - [TestCase("!=", Direction.Up, 2.00001, true)] - [TestCase("!=", Direction.Up, 1.99999, true)] - [TestCase("<", Direction.Down, -3.0, false)] - [TestCase("<", Direction.Down, -2.00001, false)] - [TestCase("<", Direction.Down, -2.0, false)] - [TestCase("<", Direction.Down, -1.99999, true)] - [TestCase("<", Direction.Down, 0.0, true)] - [TestCase("<", Direction.Down, 2.0, true)] - [TestCase("<", Direction.Flat, -2.0, false)] - [TestCase("<", Direction.Flat, -0.00001, false)] - [TestCase("<", Direction.Flat, 0.0, false)] - [TestCase("<", Direction.Flat, 0.00001, true)] - [TestCase("<", Direction.Flat, 2.0, true)] - [TestCase("<", Direction.Up, -2.0, false)] - [TestCase("<", Direction.Up, 0.0, false)] - [TestCase("<", Direction.Up, 1.99999, false)] - [TestCase("<", Direction.Up, 2.0, false)] - [TestCase("<", Direction.Up, 2.00001, true)] - [TestCase("<", Direction.Up, 3.0, true)] - [TestCase("<=", Direction.Down, -3.0, false)] - [TestCase("<=", Direction.Down, -2.00001, false)] - [TestCase("<=", Direction.Down, -2.0, true)] - [TestCase("<=", Direction.Down, -1.99999, true)] - [TestCase("<=", Direction.Down, 0.0, true)] - [TestCase("<=", Direction.Down, 2.0, true)] - [TestCase("<=", Direction.Flat, -2.0, false)] - [TestCase("<=", Direction.Flat, -0.00001, false)] - [TestCase("<=", Direction.Flat, 0.0, true)] - [TestCase("<=", Direction.Flat, 0.00001, true)] - [TestCase("<=", Direction.Flat, 2.0, true)] - [TestCase("<=", Direction.Up, -2.0, false)] - [TestCase("<=", Direction.Up, 0.0, false)] - [TestCase("<=", Direction.Up, 1.99999, false)] - [TestCase("<=", Direction.Up, 2.0, true)] - [TestCase("<=", Direction.Up, 2.00001, true)] - [TestCase("<=", Direction.Up, 3.0, true)] - [TestCase(">", Direction.Down, -3.0, true)] - [TestCase(">", Direction.Down, -2.00001, true)] - [TestCase(">", Direction.Down, -2.0, false)] - [TestCase(">", Direction.Down, -1.99999, false)] - [TestCase(">", Direction.Down, 0.0, false)] - [TestCase(">", Direction.Down, 2.0, false)] - [TestCase(">", Direction.Flat, -2.0, true)] - [TestCase(">", Direction.Flat, -0.00001, true)] - [TestCase(">", Direction.Flat, 0.0, false)] - [TestCase(">", Direction.Flat, 0.00001, false)] - [TestCase(">", Direction.Flat, 2.0, false)] - [TestCase(">", Direction.Up, -2.0, true)] - [TestCase(">", Direction.Up, 0.0, true)] - [TestCase(">", Direction.Up, 1.99999, true)] - [TestCase(">", Direction.Up, 2.0, false)] - [TestCase(">", Direction.Up, 2.00001, false)] - [TestCase(">", Direction.Up, 3.0, false)] - [TestCase(">=", Direction.Down, -3.0, true)] - [TestCase(">=", Direction.Down, -2.00001, true)] - [TestCase(">=", Direction.Down, -2.0, true)] - [TestCase(">=", Direction.Down, -1.99999, false)] - [TestCase(">=", Direction.Down, 0.0, false)] - [TestCase(">=", Direction.Down, 2.0, false)] - [TestCase(">=", Direction.Flat, -2.0, true)] - [TestCase(">=", Direction.Flat, -0.00001, true)] - [TestCase(">=", Direction.Flat, 0.0, true)] - [TestCase(">=", Direction.Flat, 0.00001, false)] - [TestCase(">=", Direction.Flat, 2.0, false)] - [TestCase(">=", Direction.Up, -2.0, true)] - [TestCase(">=", Direction.Up, 0.0, true)] - [TestCase(">=", Direction.Up, 1.99999, true)] - [TestCase(">=", Direction.Up, 2.0, true)] - [TestCase(">=", Direction.Up, 2.00001, false)] - [TestCase(">=", Direction.Up, 3.0, false)] - public void FloatComparisonOperatorsWorkWithoutExplicitCast(string @operator, Direction operand1, double operand2, bool expectedResult) + [TestCase("==", VerticalDirection.Down, -2.0, true)] + [TestCase("==", VerticalDirection.Down, -2.00001, false)] + [TestCase("==", VerticalDirection.Down, -1.99999, false)] + [TestCase("==", VerticalDirection.Down, 0.0, false)] + [TestCase("==", VerticalDirection.Down, 2.0, false)] + [TestCase("==", VerticalDirection.Flat, -2.0, false)] + [TestCase("==", VerticalDirection.Flat, 0.0, true)] + [TestCase("==", VerticalDirection.Flat, 0.00001, false)] + [TestCase("==", VerticalDirection.Flat, -0.00001, false)] + [TestCase("==", VerticalDirection.Flat, 2.0, false)] + [TestCase("==", VerticalDirection.Up, -2.0, false)] + [TestCase("==", VerticalDirection.Up, 0.0, false)] + [TestCase("==", VerticalDirection.Up, 2.0, true)] + [TestCase("==", VerticalDirection.Up, 2.00001, false)] + [TestCase("==", VerticalDirection.Up, 1.99999, false)] + [TestCase("!=", VerticalDirection.Down, -2.0, false)] + [TestCase("!=", VerticalDirection.Down, -2.00001, true)] + [TestCase("!=", VerticalDirection.Down, -1.99999, true)] + [TestCase("!=", VerticalDirection.Down, 0.0, true)] + [TestCase("!=", VerticalDirection.Down, 2.0, true)] + [TestCase("!=", VerticalDirection.Flat, -2.0, true)] + [TestCase("!=", VerticalDirection.Flat, 0.0, false)] + [TestCase("!=", VerticalDirection.Flat, 0.00001, true)] + [TestCase("!=", VerticalDirection.Flat, -0.00001, true)] + [TestCase("!=", VerticalDirection.Flat, 2.0, true)] + [TestCase("!=", VerticalDirection.Up, -2.0, true)] + [TestCase("!=", VerticalDirection.Up, 0.0, true)] + [TestCase("!=", VerticalDirection.Up, 2.0, false)] + [TestCase("!=", VerticalDirection.Up, 2.00001, true)] + [TestCase("!=", VerticalDirection.Up, 1.99999, true)] + [TestCase("<", VerticalDirection.Down, -3.0, false)] + [TestCase("<", VerticalDirection.Down, -2.00001, false)] + [TestCase("<", VerticalDirection.Down, -2.0, false)] + [TestCase("<", VerticalDirection.Down, -1.99999, true)] + [TestCase("<", VerticalDirection.Down, 0.0, true)] + [TestCase("<", VerticalDirection.Down, 2.0, true)] + [TestCase("<", VerticalDirection.Flat, -2.0, false)] + [TestCase("<", VerticalDirection.Flat, -0.00001, false)] + [TestCase("<", VerticalDirection.Flat, 0.0, false)] + [TestCase("<", VerticalDirection.Flat, 0.00001, true)] + [TestCase("<", VerticalDirection.Flat, 2.0, true)] + [TestCase("<", VerticalDirection.Up, -2.0, false)] + [TestCase("<", VerticalDirection.Up, 0.0, false)] + [TestCase("<", VerticalDirection.Up, 1.99999, false)] + [TestCase("<", VerticalDirection.Up, 2.0, false)] + [TestCase("<", VerticalDirection.Up, 2.00001, true)] + [TestCase("<", VerticalDirection.Up, 3.0, true)] + [TestCase("<=", VerticalDirection.Down, -3.0, false)] + [TestCase("<=", VerticalDirection.Down, -2.00001, false)] + [TestCase("<=", VerticalDirection.Down, -2.0, true)] + [TestCase("<=", VerticalDirection.Down, -1.99999, true)] + [TestCase("<=", VerticalDirection.Down, 0.0, true)] + [TestCase("<=", VerticalDirection.Down, 2.0, true)] + [TestCase("<=", VerticalDirection.Flat, -2.0, false)] + [TestCase("<=", VerticalDirection.Flat, -0.00001, false)] + [TestCase("<=", VerticalDirection.Flat, 0.0, true)] + [TestCase("<=", VerticalDirection.Flat, 0.00001, true)] + [TestCase("<=", VerticalDirection.Flat, 2.0, true)] + [TestCase("<=", VerticalDirection.Up, -2.0, false)] + [TestCase("<=", VerticalDirection.Up, 0.0, false)] + [TestCase("<=", VerticalDirection.Up, 1.99999, false)] + [TestCase("<=", VerticalDirection.Up, 2.0, true)] + [TestCase("<=", VerticalDirection.Up, 2.00001, true)] + [TestCase("<=", VerticalDirection.Up, 3.0, true)] + [TestCase(">", VerticalDirection.Down, -3.0, true)] + [TestCase(">", VerticalDirection.Down, -2.00001, true)] + [TestCase(">", VerticalDirection.Down, -2.0, false)] + [TestCase(">", VerticalDirection.Down, -1.99999, false)] + [TestCase(">", VerticalDirection.Down, 0.0, false)] + [TestCase(">", VerticalDirection.Down, 2.0, false)] + [TestCase(">", VerticalDirection.Flat, -2.0, true)] + [TestCase(">", VerticalDirection.Flat, -0.00001, true)] + [TestCase(">", VerticalDirection.Flat, 0.0, false)] + [TestCase(">", VerticalDirection.Flat, 0.00001, false)] + [TestCase(">", VerticalDirection.Flat, 2.0, false)] + [TestCase(">", VerticalDirection.Up, -2.0, true)] + [TestCase(">", VerticalDirection.Up, 0.0, true)] + [TestCase(">", VerticalDirection.Up, 1.99999, true)] + [TestCase(">", VerticalDirection.Up, 2.0, false)] + [TestCase(">", VerticalDirection.Up, 2.00001, false)] + [TestCase(">", VerticalDirection.Up, 3.0, false)] + [TestCase(">=", VerticalDirection.Down, -3.0, true)] + [TestCase(">=", VerticalDirection.Down, -2.00001, true)] + [TestCase(">=", VerticalDirection.Down, -2.0, true)] + [TestCase(">=", VerticalDirection.Down, -1.99999, false)] + [TestCase(">=", VerticalDirection.Down, 0.0, false)] + [TestCase(">=", VerticalDirection.Down, 2.0, false)] + [TestCase(">=", VerticalDirection.Flat, -2.0, true)] + [TestCase(">=", VerticalDirection.Flat, -0.00001, true)] + [TestCase(">=", VerticalDirection.Flat, 0.0, true)] + [TestCase(">=", VerticalDirection.Flat, 0.00001, false)] + [TestCase(">=", VerticalDirection.Flat, 2.0, false)] + [TestCase(">=", VerticalDirection.Up, -2.0, true)] + [TestCase(">=", VerticalDirection.Up, 0.0, true)] + [TestCase(">=", VerticalDirection.Up, 1.99999, true)] + [TestCase(">=", VerticalDirection.Up, 2.0, true)] + [TestCase(">=", VerticalDirection.Up, 2.00001, false)] + [TestCase(">=", VerticalDirection.Up, 3.0, false)] + public void FloatComparisonOperatorsWorkWithoutExplicitCast(string @operator, VerticalDirection operand1, double operand2, bool expectedResult) { using var _ = Py.GIL(); var module = GetTestOperatorsModule(@operator, operand1, operand2); @@ -294,7 +301,7 @@ public static IEnumerable SameEnumTypeComparisonOperatorsTestCases get { var operators = new[] { "==", "!=", "<", "<=", ">", ">=" }; - var enumValues = Enum.GetValues(); + var enumValues = Enum.GetValues(); foreach (var enumValue in enumValues) { @@ -312,7 +319,7 @@ public static IEnumerable SameEnumTypeComparisonOperatorsTestCases } [TestCaseSource(nameof(SameEnumTypeComparisonOperatorsTestCases))] - public void SameEnumTypeComparisonOperatorsWorkWithoutExplicitCast(string @operator, Direction operand1, Direction operand2, bool expectedResult) + public void SameEnumTypeComparisonOperatorsWorkWithoutExplicitCast(string @operator, VerticalDirection operand1, VerticalDirection operand2, bool expectedResult) { using var _ = Py.GIL(); var module = PyModule.FromString("SameEnumTypeComparisonOperatorsWorkWithoutExplicitCast", $@" @@ -322,31 +329,31 @@ from clr import AddReference from Python.EmbeddingTest import * def operation(): - return {nameof(EnumTests)}.{nameof(Direction)}.{operand1} {@operator} {nameof(EnumTests)}.{nameof(Direction)}.{operand2} + return {nameof(EnumTests)}.{nameof(VerticalDirection)}.{operand1} {@operator} {nameof(EnumTests)}.{nameof(VerticalDirection)}.{operand2} "); Assert.AreEqual(expectedResult, module.InvokeMethod("operation").As()); } - [TestCase("==", Direction.Down, "Down", true)] - [TestCase("==", Direction.Down, "Flat", false)] - [TestCase("==", Direction.Down, "Up", false)] - [TestCase("==", Direction.Flat, "Down", false)] - [TestCase("==", Direction.Flat, "Flat", true)] - [TestCase("==", Direction.Flat, "Up", false)] - [TestCase("==", Direction.Up, "Down", false)] - [TestCase("==", Direction.Up, "Flat", false)] - [TestCase("==", Direction.Up, "Up", true)] - [TestCase("!=", Direction.Down, "Down", false)] - [TestCase("!=", Direction.Down, "Flat", true)] - [TestCase("!=", Direction.Down, "Up", true)] - [TestCase("!=", Direction.Flat, "Down", true)] - [TestCase("!=", Direction.Flat, "Flat", false)] - [TestCase("!=", Direction.Flat, "Up", true)] - [TestCase("!=", Direction.Up, "Down", true)] - [TestCase("!=", Direction.Up, "Flat", true)] - [TestCase("!=", Direction.Up, "Up", false)] - public void EnumComparisonOperatorsWorkWithString(string @operator, Direction operand1, string operand2, bool expectedResult) + [TestCase("==", VerticalDirection.Down, "Down", true)] + [TestCase("==", VerticalDirection.Down, "Flat", false)] + [TestCase("==", VerticalDirection.Down, "Up", false)] + [TestCase("==", VerticalDirection.Flat, "Down", false)] + [TestCase("==", VerticalDirection.Flat, "Flat", true)] + [TestCase("==", VerticalDirection.Flat, "Up", false)] + [TestCase("==", VerticalDirection.Up, "Down", false)] + [TestCase("==", VerticalDirection.Up, "Flat", false)] + [TestCase("==", VerticalDirection.Up, "Up", true)] + [TestCase("!=", VerticalDirection.Down, "Down", false)] + [TestCase("!=", VerticalDirection.Down, "Flat", true)] + [TestCase("!=", VerticalDirection.Down, "Up", true)] + [TestCase("!=", VerticalDirection.Flat, "Down", true)] + [TestCase("!=", VerticalDirection.Flat, "Flat", false)] + [TestCase("!=", VerticalDirection.Flat, "Up", true)] + [TestCase("!=", VerticalDirection.Up, "Down", true)] + [TestCase("!=", VerticalDirection.Up, "Flat", true)] + [TestCase("!=", VerticalDirection.Up, "Up", false)] + public void EnumComparisonOperatorsWorkWithString(string @operator, VerticalDirection operand1, string operand2, bool expectedResult) { using var _ = Py.GIL(); var module = PyModule.FromString("EnumComparisonOperatorsWorkWithString", $@" @@ -356,14 +363,62 @@ from clr import AddReference from Python.EmbeddingTest import * def operation1(): - return {nameof(EnumTests)}.{nameof(Direction)}.{operand1} {@operator} ""{operand2}"" + return {nameof(EnumTests)}.{nameof(VerticalDirection)}.{operand1} {@operator} ""{operand2}"" def operation2(): - return ""{operand2}"" {@operator} {nameof(EnumTests)}.{nameof(Direction)}.{operand1} + return ""{operand2}"" {@operator} {nameof(EnumTests)}.{nameof(VerticalDirection)}.{operand1} "); Assert.AreEqual(expectedResult, module.InvokeMethod("operation1").As()); Assert.AreEqual(expectedResult, module.InvokeMethod("operation2").As()); } + + public static IEnumerable OtherEnumsComparisonOperatorsTestCases + { + get + { + var operators = new[] { "==", "!=", "<", "<=", ">", ">=" }; + var enumValues = Enum.GetValues(); + var enum2Values = Enum.GetValues(); + + foreach (var enumValue in enumValues) + { + foreach (var enum2Value in enum2Values) + { + var intEnumValue = Convert.ToInt64(enumValue); + var intEnum2Value = Convert.ToInt64(enum2Value); + + yield return new TestCaseData("==", enumValue, enum2Value, intEnumValue == intEnum2Value, intEnum2Value == intEnumValue); + yield return new TestCaseData("!=", enumValue, enum2Value, intEnumValue != intEnum2Value, intEnum2Value != intEnumValue); + yield return new TestCaseData("<", enumValue, enum2Value, intEnumValue < intEnum2Value, intEnum2Value < intEnumValue); + yield return new TestCaseData("<=", enumValue, enum2Value, intEnumValue <= intEnum2Value, intEnum2Value <= intEnumValue); + yield return new TestCaseData(">", enumValue, enum2Value, intEnumValue > intEnum2Value, intEnum2Value > intEnumValue); + yield return new TestCaseData(">=", enumValue, enum2Value, intEnumValue >= intEnum2Value, intEnum2Value >= intEnumValue); + } + } + } + } + + [TestCaseSource(nameof(OtherEnumsComparisonOperatorsTestCases))] + public void OtherEnumsComparisonOperatorsWorkWithoutExplicitCast(string @operator, VerticalDirection operand1, HorizontalDirection operand2, bool expectedResult, bool invertedOperationExpectedResult) + { + using var _ = Py.GIL(); + var module = PyModule.FromString("OtherEnumsComparisonOperatorsWorkWithoutExplicitCast", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +def operation1(): + return {nameof(EnumTests)}.{nameof(VerticalDirection)}.{operand1} {@operator} {nameof(EnumTests)}.{nameof(HorizontalDirection)}.{operand2} + +def operation2(): + return {nameof(EnumTests)}.{nameof(HorizontalDirection)}.{operand2} {@operator} {nameof(EnumTests)}.{nameof(VerticalDirection)}.{operand1} +"); + + Assert.AreEqual(expectedResult, module.InvokeMethod("operation1").As()); + Assert.AreEqual(invertedOperationExpectedResult, module.InvokeMethod("operation2").As()); + + } } } diff --git a/src/runtime/Util/OpsHelper.cs b/src/runtime/Util/OpsHelper.cs index 3d628d3de..f6de71c5f 100644 --- a/src/runtime/Util/OpsHelper.cs +++ b/src/runtime/Util/OpsHelper.cs @@ -489,5 +489,163 @@ public static bool op_Inequality(string a, T b) } #endregion + + #region Other Enum comparison operators + + private static bool IsEnum(object b, out Type type) + { + type = b.GetType(); + if (type.IsEnum) + { + return true; + } + using var _ = Py.GIL(); + Exceptions.RaiseTypeError($"No method matched to compare {typeof(T).Name} and {type.Name}"); + + return false; + } + + public static bool op_Equality(T a, object b) + { + if (!IsEnum(b, out var bType)) + { + return false; + } + if (bType.GetEnumUnderlyingType() == typeof(UInt64)) + { + return op_Equality(a, Convert.ToUInt64(b)); + } + return op_Equality(a, Convert.ToInt64(b)); + } + + public static bool op_Equality(object a, T b) + { + if (!IsEnum(a, out var aType)) + { + return false; + } + + if (aType.GetEnumUnderlyingType() == typeof(UInt64)) + { + return op_Equality(b, Convert.ToUInt64(a)); + } + return op_Equality(b, Convert.ToInt64(a)); + } + + public static bool op_Inequality(T a, object b) + { + return !op_Equality(a, b); + } + + public static bool op_Inequality(object a, T b) + { + return !op_Equality(a, b); + } + + public static bool op_LessThan(T a, object b) + { + if (!IsEnum(b, out var bType)) + { + return false; + } + if (bType.GetEnumUnderlyingType() == typeof(UInt64)) + { + return op_LessThan(a, Convert.ToUInt64(b)); + } + return op_LessThan(a, Convert.ToInt64(b)); + } + + public static bool op_LessThan(object a, T b) + { + if (!IsEnum(a, out var aType)) + { + return false; + } + if (aType.GetEnumUnderlyingType() == typeof(UInt64)) + { + return op_LessThan(Convert.ToUInt64(a), b); + } + return op_LessThan(Convert.ToInt64(a), b); + } + + public static bool op_GreaterThan(T a, object b) + { + if (!IsEnum(b, out var bType)) + { + return false; + } + if (bType.GetEnumUnderlyingType() == typeof(UInt64)) + { + return op_GreaterThan(a, Convert.ToUInt64(b)); + } + return op_GreaterThan(a, Convert.ToInt64(b)); + } + + public static bool op_GreaterThan(object a, T b) + { + if (!IsEnum(a, out var aType)) + { + return false; + } + if (aType.GetEnumUnderlyingType() == typeof(UInt64)) + { + return op_GreaterThan(Convert.ToUInt64(a), b); + } + return op_GreaterThan(Convert.ToInt64(a), b); + } + + public static bool op_LessThanOrEqual(T a, object b) + { + if (!IsEnum(b, out var bType)) + { + return false; + } + if (bType.GetEnumUnderlyingType() == typeof(UInt64)) + { + return op_LessThanOrEqual(a, Convert.ToUInt64(b)); + } + return op_LessThanOrEqual(a, Convert.ToInt64(b)); + } + + public static bool op_LessThanOrEqual(object a, T b) + { + if (!IsEnum(a, out var aType)) + { + return false; + } + if (aType.GetEnumUnderlyingType() == typeof(UInt64)) + { + return op_LessThanOrEqual(Convert.ToUInt64(a), b); + } + return op_LessThanOrEqual(Convert.ToInt64(a), b); + } + + public static bool op_GreaterThanOrEqual(T a, object b) + { + if (!IsEnum(b, out var bType)) + { + return false; + } + if (bType.GetEnumUnderlyingType() == typeof(UInt64)) + { + return op_GreaterThanOrEqual(a, Convert.ToUInt64(b)); + } + return op_GreaterThanOrEqual(a, Convert.ToInt64(b)); + } + + public static bool op_GreaterThanOrEqual(object a, T b) + { + if (!IsEnum(a, out var aType)) + { + return false; + } + if (aType.GetEnumUnderlyingType() == typeof(UInt64)) + { + return op_GreaterThanOrEqual(Convert.ToUInt64(a), b); + } + return op_GreaterThanOrEqual(Convert.ToInt64(a), b); + } + + #endregion } } From 009e9cf67a8561a11ab3c83df05c6a66013b5ea6 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 30 Jul 2025 09:24:40 -0400 Subject: [PATCH 05/10] 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. --- src/embed_tests/EnumTests.cs | 75 ++++++++++++++++++++++++++++++++++++ src/runtime/Converter.cs | 20 +++++++++- 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/embed_tests/EnumTests.cs b/src/embed_tests/EnumTests.cs index 56732e2ea..619a85ee3 100644 --- a/src/embed_tests/EnumTests.cs +++ b/src/embed_tests/EnumTests.cs @@ -418,7 +418,82 @@ def operation2(): Assert.AreEqual(expectedResult, module.InvokeMethod("operation1").As()); Assert.AreEqual(invertedOperationExpectedResult, module.InvokeMethod("operation2").As()); + } + + private static IEnumerable IdentityComparisonTestCases + { + get + { + var enumValues = Enum.GetValues(); + foreach (var enumValue1 in enumValues) + { + foreach (var enumValue2 in enumValues) + { + if (enumValue2 != enumValue1) + { + yield return new TestCaseData(enumValue1, enumValue2); + } + } + } + } + } + + [TestCaseSource(nameof(IdentityComparisonTestCases))] + public void CSharpEnumsAreSingletonsInPthonAndIdentityComparisonWorks(VerticalDirection enumValue1, VerticalDirection enumValue2) + { + var enumValue1Str = $"{nameof(EnumTests)}.{nameof(VerticalDirection)}.{enumValue1}"; + var enumValue2Str = $"{nameof(EnumTests)}.{nameof(VerticalDirection)}.{enumValue2}"; + using var _ = Py.GIL(); + var module = PyModule.FromString("TESTTTT", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +def are_same1(): + return {enumValue1Str} is {enumValue1Str} + +def are_same2(): + enum_value = {enumValue1Str} + return enum_value is {enumValue1Str} + +def are_same3(): + enum_value = {enumValue1Str} + return {enumValue1Str} is enum_value + +def are_same4(): + enum_value1 = {enumValue1Str} + enum_value2 = {enumValue1Str} + return enum_value1 is enum_value2 + +def are_not_same1(): + return {enumValue1Str} is not {enumValue2Str} + +def are_not_same2(): + enum_value = {enumValue1Str} + return enum_value is not {enumValue2Str} + +def are_not_same3(): + enum_value = {enumValue2Str} + return {enumValue1Str} is not enum_value + +def are_not_same4(): + enum_value1 = {enumValue1Str} + enum_value2 = {enumValue2Str} + return enum_value1 is not enum_value2 + + +"); + + Assert.IsTrue(module.InvokeMethod("are_same1").As()); + Assert.IsTrue(module.InvokeMethod("are_same2").As()); + Assert.IsTrue(module.InvokeMethod("are_same3").As()); + Assert.IsTrue(module.InvokeMethod("are_same4").As()); + Assert.IsTrue(module.InvokeMethod("are_not_same1").As()); + Assert.IsTrue(module.InvokeMethod("are_not_same2").As()); + Assert.IsTrue(module.InvokeMethod("are_not_same3").As()); + Assert.IsTrue(module.InvokeMethod("are_not_same4").As()); } } } diff --git a/src/runtime/Converter.cs b/src/runtime/Converter.cs index d0e97362a..be4d5fb78 100644 --- a/src/runtime/Converter.cs +++ b/src/runtime/Converter.cs @@ -18,6 +18,13 @@ namespace Python.Runtime [SuppressUnmanagedCodeSecurity] internal class Converter { + /// + /// We use a cache of the enum values references so that we treat them as singletons in Python. + /// We just try to mimic Python enums behavior, since Python enum values are singletons, + /// so the `is` identity comparison operator works for C# enums as well. + /// + + private static readonly Dictionary> _enumCache = new(); private Converter() { } @@ -228,7 +235,18 @@ internal static NewReference ToPython(object? value, Type type) if (type.IsEnum) { - return CLRObject.GetReference(value, type); + if (!_enumCache.TryGetValue(type, out var cache)) + { + cache = new(); + _enumCache[type] = cache; + } + + if (!cache.TryGetValue(value, out var cachedValue)) + { + cache[value] = cachedValue = CLRObject.GetReference(value, type).MoveToPyObject(); + } + + return cachedValue.NewReferenceOrNull(); } // it the type is a python subclass of a managed type then return the From cb232a0244b5b4d26d759b71c3b3f0cbe124cfe8 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 30 Jul 2025 10:23:40 -0400 Subject: [PATCH 06/10] Minor fix --- src/embed_tests/EnumTests.cs | 71 ++++++++++++++++++++++++++++++------ src/runtime/Converter.cs | 12 ++---- 2 files changed, 63 insertions(+), 20 deletions(-) diff --git a/src/embed_tests/EnumTests.cs b/src/embed_tests/EnumTests.cs index 619a85ee3..857f2a1c9 100644 --- a/src/embed_tests/EnumTests.cs +++ b/src/embed_tests/EnumTests.cs @@ -9,6 +9,9 @@ namespace Python.EmbeddingTest { public class EnumTests { + private static VerticalDirection[] VerticalDirectionEnumValues = Enum.GetValues(); + private static HorizontalDirection[] HorizontalDirectionEnumValues = Enum.GetValues(); + [OneTimeSetUp] public void SetUp() { @@ -301,11 +304,10 @@ public static IEnumerable SameEnumTypeComparisonOperatorsTestCases get { var operators = new[] { "==", "!=", "<", "<=", ">", ">=" }; - var enumValues = Enum.GetValues(); - foreach (var enumValue in enumValues) + foreach (var enumValue in VerticalDirectionEnumValues) { - foreach (var enumValue2 in enumValues) + foreach (var enumValue2 in VerticalDirectionEnumValues) { yield return new TestCaseData("==", enumValue, enumValue2, enumValue == enumValue2); yield return new TestCaseData("!=", enumValue, enumValue2, enumValue != enumValue2); @@ -378,12 +380,10 @@ public static IEnumerable OtherEnumsComparisonOperatorsTestCases get { var operators = new[] { "==", "!=", "<", "<=", ">", ">=" }; - var enumValues = Enum.GetValues(); - var enum2Values = Enum.GetValues(); - foreach (var enumValue in enumValues) + foreach (var enumValue in VerticalDirectionEnumValues) { - foreach (var enum2Value in enum2Values) + foreach (var enum2Value in HorizontalDirectionEnumValues) { var intEnumValue = Convert.ToInt64(enumValue); var intEnum2Value = Convert.ToInt64(enum2Value); @@ -424,10 +424,9 @@ private static IEnumerable IdentityComparisonTestCases { get { - var enumValues = Enum.GetValues(); - foreach (var enumValue1 in enumValues) + foreach (var enumValue1 in VerticalDirectionEnumValues) { - foreach (var enumValue2 in enumValues) + foreach (var enumValue2 in VerticalDirectionEnumValues) { if (enumValue2 != enumValue1) { @@ -443,8 +442,9 @@ public void CSharpEnumsAreSingletonsInPthonAndIdentityComparisonWorks(VerticalDi { var enumValue1Str = $"{nameof(EnumTests)}.{nameof(VerticalDirection)}.{enumValue1}"; var enumValue2Str = $"{nameof(EnumTests)}.{nameof(VerticalDirection)}.{enumValue2}"; + using var _ = Py.GIL(); - var module = PyModule.FromString("TESTTTT", $@" + var module = PyModule.FromString("CSharpEnumsAreSingletonsInPthonAndIdentityComparisonWorks", $@" from clr import AddReference AddReference(""Python.EmbeddingTest"") @@ -495,5 +495,54 @@ def are_not_same4(): Assert.IsTrue(module.InvokeMethod("are_not_same3").As()); Assert.IsTrue(module.InvokeMethod("are_not_same4").As()); } + + [Test] + public void IdentityComparisonBetweenDifferentEnumTypesIsNeverTrue( + [ValueSource(nameof(VerticalDirectionEnumValues))] VerticalDirection enumValue1, + [ValueSource(nameof(HorizontalDirectionEnumValues))] HorizontalDirection enumValue2) + { + var enumValue1Str = $"{nameof(EnumTests)}.{nameof(VerticalDirection)}.{enumValue1}"; + var enumValue2Str = $"{nameof(EnumTests)}.{nameof(HorizontalDirection)}.{enumValue2}"; + + using var _ = Py.GIL(); + var module = PyModule.FromString("IdentityComparisonBetweenDifferentEnumTypesIsNeverTrue", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +enum_value1 = {enumValue1Str} +enum_value2 = {enumValue2Str} + +def are_same1(): + return {enumValue1Str} is {enumValue2Str} + +def are_same2(): + return enum_value1 is {enumValue2Str} + +def are_same3(): + return {enumValue2Str} is enum_value1 + +def are_same4(): + return enum_value2 is {enumValue1Str} + +def are_same5(): + return {enumValue1Str} is enum_value2 + +def are_same6(): + return enum_value1 is enum_value2 + +def are_same7(): + return enum_value2 is enum_value1 +"); + + Assert.IsFalse(module.InvokeMethod("are_same1").As()); + Assert.IsFalse(module.InvokeMethod("are_same2").As()); + Assert.IsFalse(module.InvokeMethod("are_same3").As()); + Assert.IsFalse(module.InvokeMethod("are_same4").As()); + Assert.IsFalse(module.InvokeMethod("are_same5").As()); + Assert.IsFalse(module.InvokeMethod("are_same6").As()); + Assert.IsFalse(module.InvokeMethod("are_same7").As()); + } } } diff --git a/src/runtime/Converter.cs b/src/runtime/Converter.cs index be4d5fb78..fc6437bc1 100644 --- a/src/runtime/Converter.cs +++ b/src/runtime/Converter.cs @@ -24,7 +24,7 @@ internal class Converter /// so the `is` identity comparison operator works for C# enums as well. /// - private static readonly Dictionary> _enumCache = new(); + private static readonly Dictionary _enumCache = new(); private Converter() { } @@ -235,15 +235,9 @@ internal static NewReference ToPython(object? value, Type type) if (type.IsEnum) { - if (!_enumCache.TryGetValue(type, out var cache)) + if (!_enumCache.TryGetValue(value, out var cachedValue)) { - cache = new(); - _enumCache[type] = cache; - } - - if (!cache.TryGetValue(value, out var cachedValue)) - { - cache[value] = cachedValue = CLRObject.GetReference(value, type).MoveToPyObject(); + _enumCache[value] = cachedValue = CLRObject.GetReference(value, type).MoveToPyObject(); } return cachedValue.NewReferenceOrNull(); From 57c2299a728490e37656a8f766b9103cd9b11088 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 30 Jul 2025 13:02:55 -0400 Subject: [PATCH 07/10] More tests and cleanup --- src/embed_tests/EnumTests.cs | 102 +++++++++++++++++++++++++++++++--- src/runtime/Util/OpsHelper.cs | 98 +++++++++++++------------------- 2 files changed, 132 insertions(+), 68 deletions(-) diff --git a/src/embed_tests/EnumTests.cs b/src/embed_tests/EnumTests.cs index 857f2a1c9..f44def664 100644 --- a/src/embed_tests/EnumTests.cs +++ b/src/embed_tests/EnumTests.cs @@ -42,7 +42,7 @@ public enum HorizontalDirection public void CSharpEnumsBehaveAsEnumsInPython() { using var _ = Py.GIL(); - var module = PyModule.FromString("CSharpEnumsBehaveAsEnumsInPython", $@" + using var module = PyModule.FromString("CSharpEnumsBehaveAsEnumsInPython", $@" from clr import AddReference AddReference(""Python.EmbeddingTest"") @@ -103,7 +103,7 @@ def operation2(): public void ArithmeticOperatorsWorkWithoutExplicitCast(string @operator, VerticalDirection operand1, double operand2, double expectedResult, double invertedOperationExpectedResult) { using var _ = Py.GIL(); - var module = GetTestOperatorsModule(@operator, operand1, operand2); + using var module = GetTestOperatorsModule(@operator, operand1, operand2); Assert.AreEqual(expectedResult, module.InvokeMethod("operation1").As()); @@ -178,7 +178,7 @@ public void ArithmeticOperatorsWorkWithoutExplicitCast(string @operator, Vertica public void IntComparisonOperatorsWorkWithoutExplicitCast(string @operator, VerticalDirection operand1, int operand2, bool expectedResult) { using var _ = Py.GIL(); - var module = GetTestOperatorsModule(@operator, operand1, operand2); + using var module = GetTestOperatorsModule(@operator, operand1, operand2); Assert.AreEqual(expectedResult, module.InvokeMethod("operation1").As()); @@ -289,7 +289,7 @@ public void IntComparisonOperatorsWorkWithoutExplicitCast(string @operator, Vert public void FloatComparisonOperatorsWorkWithoutExplicitCast(string @operator, VerticalDirection operand1, double operand2, bool expectedResult) { using var _ = Py.GIL(); - var module = GetTestOperatorsModule(@operator, operand1, operand2); + using var module = GetTestOperatorsModule(@operator, operand1, operand2); Assert.AreEqual(expectedResult, module.InvokeMethod("operation1").As()); @@ -324,7 +324,7 @@ public static IEnumerable SameEnumTypeComparisonOperatorsTestCases public void SameEnumTypeComparisonOperatorsWorkWithoutExplicitCast(string @operator, VerticalDirection operand1, VerticalDirection operand2, bool expectedResult) { using var _ = Py.GIL(); - var module = PyModule.FromString("SameEnumTypeComparisonOperatorsWorkWithoutExplicitCast", $@" + using var module = PyModule.FromString("SameEnumTypeComparisonOperatorsWorkWithoutExplicitCast", $@" from clr import AddReference AddReference(""Python.EmbeddingTest"") @@ -358,7 +358,7 @@ def operation(): public void EnumComparisonOperatorsWorkWithString(string @operator, VerticalDirection operand1, string operand2, bool expectedResult) { using var _ = Py.GIL(); - var module = PyModule.FromString("EnumComparisonOperatorsWorkWithString", $@" + using var module = PyModule.FromString("EnumComparisonOperatorsWorkWithString", $@" from clr import AddReference AddReference(""Python.EmbeddingTest"") @@ -403,7 +403,7 @@ public static IEnumerable OtherEnumsComparisonOperatorsTestCases public void OtherEnumsComparisonOperatorsWorkWithoutExplicitCast(string @operator, VerticalDirection operand1, HorizontalDirection operand2, bool expectedResult, bool invertedOperationExpectedResult) { using var _ = Py.GIL(); - var module = PyModule.FromString("OtherEnumsComparisonOperatorsWorkWithoutExplicitCast", $@" + using var module = PyModule.FromString("OtherEnumsComparisonOperatorsWorkWithoutExplicitCast", $@" from clr import AddReference AddReference(""Python.EmbeddingTest"") @@ -444,7 +444,7 @@ public void CSharpEnumsAreSingletonsInPthonAndIdentityComparisonWorks(VerticalDi var enumValue2Str = $"{nameof(EnumTests)}.{nameof(VerticalDirection)}.{enumValue2}"; using var _ = Py.GIL(); - var module = PyModule.FromString("CSharpEnumsAreSingletonsInPthonAndIdentityComparisonWorks", $@" + using var module = PyModule.FromString("CSharpEnumsAreSingletonsInPthonAndIdentityComparisonWorks", $@" from clr import AddReference AddReference(""Python.EmbeddingTest"") @@ -505,7 +505,7 @@ public void IdentityComparisonBetweenDifferentEnumTypesIsNeverTrue( var enumValue2Str = $"{nameof(EnumTests)}.{nameof(HorizontalDirection)}.{enumValue2}"; using var _ = Py.GIL(); - var module = PyModule.FromString("IdentityComparisonBetweenDifferentEnumTypesIsNeverTrue", $@" + using var module = PyModule.FromString("IdentityComparisonBetweenDifferentEnumTypesIsNeverTrue", $@" from clr import AddReference AddReference(""Python.EmbeddingTest"") @@ -544,5 +544,89 @@ def are_same7(): Assert.IsFalse(module.InvokeMethod("are_same6").As()); Assert.IsFalse(module.InvokeMethod("are_same7").As()); } + + private PyModule GetCSharpObjectsComparisonTestModule(string @operator) + { + return PyModule.FromString("GetCSharpObjectsComparisonTestModule", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +enum_value = {nameof(EnumTests)}.{nameof(VerticalDirection)}.{VerticalDirection.Up} + +def compare_with_none1(): + return enum_value {@operator} None + +def compare_with_none2(): + return None {@operator} enum_value + +def compare_with_csharp_object1(csharp_object): + return enum_value {@operator} csharp_object + +def compare_with_csharp_object2(csharp_object): + return csharp_object {@operator} enum_value +"); + } + + [TestCase("==", false)] + [TestCase("!=", true)] + public void EqualityComparisonWithNull(string @operator, bool expectedResult) + { + using var _ = Py.GIL(); + using var module = GetCSharpObjectsComparisonTestModule(@operator); + + Assert.AreEqual(expectedResult, module.InvokeMethod("compare_with_none1").As()); + Assert.AreEqual(expectedResult, module.InvokeMethod("compare_with_none2").As()); + + using var pyNull = ((TestClass)null).ToPython(); + Assert.AreEqual(expectedResult, module.InvokeMethod("compare_with_csharp_object1", pyNull).As()); + Assert.AreEqual(expectedResult, module.InvokeMethod("compare_with_csharp_object2", pyNull).As()); + } + + [Test] + public void SortingComparisonWithNullThrows([Values("<", "<=", ">", ">=")] string @operator) + { + using var _ = Py.GIL(); + using var module = GetCSharpObjectsComparisonTestModule(@operator); + + using var pyNull = ((TestClass)null).ToPython(); + + var exception = Assert.Throws(() => module.InvokeMethod("compare_with_csharp_object1", pyNull)); + Assert.IsTrue(exception.Message.Contains("Cannot compare")); + Assert.IsTrue(exception.Message.Contains("with null")); + } + + private static IEnumerable ComparisonWithNonEnumObjectsTestCases + { + get + { + foreach (var op in new[] { "==", "!=" }) + { + yield return new TestCaseData(op, new[] { "No method matched to compare" }); + } + + foreach (var op in new[] { "<", "<=", ">", ">=" }) + { + yield return new TestCaseData(op, new[] { "Cannot compare", "with null" }); + } + } + } + + [Test] + public void ComparisonOperatorsWithNonEnumObjectsThrows([Values("==", "!=", "<", "<=", ">", ">=")] string @operator) + { + using var _ = Py.GIL(); + using var module = GetCSharpObjectsComparisonTestModule(@operator); + + using var pyCSharpObject = new TestClass().ToPython(); + + var exception = Assert.Throws(() => module.InvokeMethod("compare_with_csharp_object1", pyCSharpObject)); + Assert.IsTrue(exception.Message.Contains("No method matched"), $"Expected exception message to contain 'No method matched' but got: {exception.Message}"); + } + + public class TestClass + { + } } } diff --git a/src/runtime/Util/OpsHelper.cs b/src/runtime/Util/OpsHelper.cs index f6de71c5f..89d278de0 100644 --- a/src/runtime/Util/OpsHelper.cs +++ b/src/runtime/Util/OpsHelper.cs @@ -492,26 +492,43 @@ public static bool op_Inequality(string a, T b) #region Other Enum comparison operators - private static bool IsEnum(object b, out Type type) - { + private static bool IsEnum(object b, out Type type, out Type underlyingType, bool throwOnNull = false) + { + type = null; + underlyingType = null; + if (b == null) + { + if (throwOnNull) + { + using (Py.GIL()) + { + Exceptions.RaiseTypeError($"Cannot compare {typeof(T).Name} with null"); + PythonException.ThrowLastAsClrException(); + } + } + return false; + } + type = b.GetType(); if (type.IsEnum) { + underlyingType = type.GetEnumUnderlyingType(); return true; } using var _ = Py.GIL(); Exceptions.RaiseTypeError($"No method matched to compare {typeof(T).Name} and {type.Name}"); + PythonException.ThrowLastAsClrException(); return false; } public static bool op_Equality(T a, object b) { - if (!IsEnum(b, out var bType)) + if (!IsEnum(b, out var bType, out var underlyingType)) { return false; } - if (bType.GetEnumUnderlyingType() == typeof(UInt64)) + if (underlyingType == typeof(UInt64)) { return op_Equality(a, Convert.ToUInt64(b)); } @@ -520,16 +537,7 @@ public static bool op_Equality(T a, object b) public static bool op_Equality(object a, T b) { - if (!IsEnum(a, out var aType)) - { - return false; - } - - if (aType.GetEnumUnderlyingType() == typeof(UInt64)) - { - return op_Equality(b, Convert.ToUInt64(a)); - } - return op_Equality(b, Convert.ToInt64(a)); + return op_Equality(b, a); } public static bool op_Inequality(T a, object b) @@ -539,16 +547,17 @@ public static bool op_Inequality(T a, object b) public static bool op_Inequality(object a, T b) { - return !op_Equality(a, b); + return !op_Equality(b, a); } public static bool op_LessThan(T a, object b) { - if (!IsEnum(b, out var bType)) + if (!IsEnum(b, out var bType, out var underlyingType, throwOnNull: true)) { + // False although it means nothing: an exception will be raised return false; } - if (bType.GetEnumUnderlyingType() == typeof(UInt64)) + if (underlyingType == typeof(UInt64)) { return op_LessThan(a, Convert.ToUInt64(b)); } @@ -557,24 +566,17 @@ public static bool op_LessThan(T a, object b) public static bool op_LessThan(object a, T b) { - if (!IsEnum(a, out var aType)) - { - return false; - } - if (aType.GetEnumUnderlyingType() == typeof(UInt64)) - { - return op_LessThan(Convert.ToUInt64(a), b); - } - return op_LessThan(Convert.ToInt64(a), b); + return op_GreaterThan(b, a); } public static bool op_GreaterThan(T a, object b) { - if (!IsEnum(b, out var bType)) + if (!IsEnum(b, out var bType, out var underlyingType, throwOnNull: true)) { + // False although it means nothing: an exception will be raised return false; } - if (bType.GetEnumUnderlyingType() == typeof(UInt64)) + if (underlyingType == typeof(UInt64)) { return op_GreaterThan(a, Convert.ToUInt64(b)); } @@ -583,24 +585,17 @@ public static bool op_GreaterThan(T a, object b) public static bool op_GreaterThan(object a, T b) { - if (!IsEnum(a, out var aType)) - { - return false; - } - if (aType.GetEnumUnderlyingType() == typeof(UInt64)) - { - return op_GreaterThan(Convert.ToUInt64(a), b); - } - return op_GreaterThan(Convert.ToInt64(a), b); + return op_LessThan(b, a); } public static bool op_LessThanOrEqual(T a, object b) { - if (!IsEnum(b, out var bType)) + if (!IsEnum(b, out var bType, out var underlyingType, throwOnNull: true)) { + // False although it means nothing: an exception will be raised return false; } - if (bType.GetEnumUnderlyingType() == typeof(UInt64)) + if (underlyingType == typeof(UInt64)) { return op_LessThanOrEqual(a, Convert.ToUInt64(b)); } @@ -609,24 +604,17 @@ public static bool op_LessThanOrEqual(T a, object b) public static bool op_LessThanOrEqual(object a, T b) { - if (!IsEnum(a, out var aType)) - { - return false; - } - if (aType.GetEnumUnderlyingType() == typeof(UInt64)) - { - return op_LessThanOrEqual(Convert.ToUInt64(a), b); - } - return op_LessThanOrEqual(Convert.ToInt64(a), b); + return op_GreaterThanOrEqual(b, a); } public static bool op_GreaterThanOrEqual(T a, object b) { - if (!IsEnum(b, out var bType)) + if (!IsEnum(b, out var bType, out var underlyingType, throwOnNull: true)) { + // False although it means nothing: an exception will be raised return false; } - if (bType.GetEnumUnderlyingType() == typeof(UInt64)) + if (underlyingType == typeof(UInt64)) { return op_GreaterThanOrEqual(a, Convert.ToUInt64(b)); } @@ -635,15 +623,7 @@ public static bool op_GreaterThanOrEqual(T a, object b) public static bool op_GreaterThanOrEqual(object a, T b) { - if (!IsEnum(a, out var aType)) - { - return false; - } - if (aType.GetEnumUnderlyingType() == typeof(UInt64)) - { - return op_GreaterThanOrEqual(Convert.ToUInt64(a), b); - } - return op_GreaterThanOrEqual(Convert.ToInt64(a), b); + return op_LessThanOrEqual(b, a); } #endregion From 98828a15b996e87c6852814f0eb3eaa3c2ded932 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Wed, 30 Jul 2025 17:34:29 -0400 Subject: [PATCH 08/10] Reduce enum operators overloads --- src/embed_tests/EnumTests.cs | 84 -------------------- src/runtime/Util/OpsHelper.cs | 140 +++++----------------------------- 2 files changed, 18 insertions(+), 206 deletions(-) diff --git a/src/embed_tests/EnumTests.cs b/src/embed_tests/EnumTests.cs index f44def664..52c40a56b 100644 --- a/src/embed_tests/EnumTests.cs +++ b/src/embed_tests/EnumTests.cs @@ -544,89 +544,5 @@ def are_same7(): Assert.IsFalse(module.InvokeMethod("are_same6").As()); Assert.IsFalse(module.InvokeMethod("are_same7").As()); } - - private PyModule GetCSharpObjectsComparisonTestModule(string @operator) - { - return PyModule.FromString("GetCSharpObjectsComparisonTestModule", $@" -from clr import AddReference -AddReference(""Python.EmbeddingTest"") - -from Python.EmbeddingTest import * - -enum_value = {nameof(EnumTests)}.{nameof(VerticalDirection)}.{VerticalDirection.Up} - -def compare_with_none1(): - return enum_value {@operator} None - -def compare_with_none2(): - return None {@operator} enum_value - -def compare_with_csharp_object1(csharp_object): - return enum_value {@operator} csharp_object - -def compare_with_csharp_object2(csharp_object): - return csharp_object {@operator} enum_value -"); - } - - [TestCase("==", false)] - [TestCase("!=", true)] - public void EqualityComparisonWithNull(string @operator, bool expectedResult) - { - using var _ = Py.GIL(); - using var module = GetCSharpObjectsComparisonTestModule(@operator); - - Assert.AreEqual(expectedResult, module.InvokeMethod("compare_with_none1").As()); - Assert.AreEqual(expectedResult, module.InvokeMethod("compare_with_none2").As()); - - using var pyNull = ((TestClass)null).ToPython(); - Assert.AreEqual(expectedResult, module.InvokeMethod("compare_with_csharp_object1", pyNull).As()); - Assert.AreEqual(expectedResult, module.InvokeMethod("compare_with_csharp_object2", pyNull).As()); - } - - [Test] - public void SortingComparisonWithNullThrows([Values("<", "<=", ">", ">=")] string @operator) - { - using var _ = Py.GIL(); - using var module = GetCSharpObjectsComparisonTestModule(@operator); - - using var pyNull = ((TestClass)null).ToPython(); - - var exception = Assert.Throws(() => module.InvokeMethod("compare_with_csharp_object1", pyNull)); - Assert.IsTrue(exception.Message.Contains("Cannot compare")); - Assert.IsTrue(exception.Message.Contains("with null")); - } - - private static IEnumerable ComparisonWithNonEnumObjectsTestCases - { - get - { - foreach (var op in new[] { "==", "!=" }) - { - yield return new TestCaseData(op, new[] { "No method matched to compare" }); - } - - foreach (var op in new[] { "<", "<=", ">", ">=" }) - { - yield return new TestCaseData(op, new[] { "Cannot compare", "with null" }); - } - } - } - - [Test] - public void ComparisonOperatorsWithNonEnumObjectsThrows([Values("==", "!=", "<", "<=", ">", ">=")] string @operator) - { - using var _ = Py.GIL(); - using var module = GetCSharpObjectsComparisonTestModule(@operator); - - using var pyCSharpObject = new TestClass().ToPython(); - - var exception = Assert.Throws(() => module.InvokeMethod("compare_with_csharp_object1", pyCSharpObject)); - Assert.IsTrue(exception.Message.Contains("No method matched"), $"Expected exception message to contain 'No method matched' but got: {exception.Message}"); - } - - public class TestClass - { - } } } diff --git a/src/runtime/Util/OpsHelper.cs b/src/runtime/Util/OpsHelper.cs index 89d278de0..8d2b8b8ef 100644 --- a/src/runtime/Util/OpsHelper.cs +++ b/src/runtime/Util/OpsHelper.cs @@ -418,56 +418,6 @@ public static bool op_GreaterThanOrEqual(double a, T b) #endregion - #region Same type comparison operators - - public static bool op_Equality(T a, T b) - { - return a.Equals(b); - } - - public static bool op_Inequality(T a, T b) - { - return !a.Equals(b); - } - - public static bool op_LessThan(T a, T b) - { - if (IsUnsigned) - { - return Convert.ToUInt64(a) < Convert.ToUInt64(b); - } - return Convert.ToInt64(a) < Convert.ToInt64(b); - } - - public static bool op_GreaterThan(T a, T b) - { - if (IsUnsigned) - { - return Convert.ToUInt64(a) > Convert.ToUInt64(b); - } - return Convert.ToInt64(a) > Convert.ToInt64(b); - } - - public static bool op_LessThanOrEqual(T a, T b) - { - if (IsUnsigned) - { - return Convert.ToUInt64(a) <= Convert.ToUInt64(b); - } - return Convert.ToInt64(a) <= Convert.ToInt64(b); - } - - public static bool op_GreaterThanOrEqual(T a, T b) - { - if (IsUnsigned) - { - return Convert.ToUInt64(a) >= Convert.ToUInt64(b); - } - return Convert.ToInt64(a) >= Convert.ToInt64(b); - } - - #endregion - #region String comparison operators public static bool op_Equality(T a, string b) { @@ -490,138 +440,84 @@ public static bool op_Inequality(string a, T b) #endregion - #region Other Enum comparison operators - - private static bool IsEnum(object b, out Type type, out Type underlyingType, bool throwOnNull = false) - { - type = null; - underlyingType = null; - if (b == null) - { - if (throwOnNull) - { - using (Py.GIL()) - { - Exceptions.RaiseTypeError($"Cannot compare {typeof(T).Name} with null"); - PythonException.ThrowLastAsClrException(); - } - } - return false; - } - - type = b.GetType(); - if (type.IsEnum) - { - underlyingType = type.GetEnumUnderlyingType(); - return true; - } - using var _ = Py.GIL(); - Exceptions.RaiseTypeError($"No method matched to compare {typeof(T).Name} and {type.Name}"); - PythonException.ThrowLastAsClrException(); - - return false; - } + #region Enum comparison operators - public static bool op_Equality(T a, object b) + public static bool op_Equality(T a, Enum b) { - if (!IsEnum(b, out var bType, out var underlyingType)) - { - return false; - } - if (underlyingType == typeof(UInt64)) + if (b.GetType().GetEnumUnderlyingType() == typeof(UInt64)) { return op_Equality(a, Convert.ToUInt64(b)); } return op_Equality(a, Convert.ToInt64(b)); } - public static bool op_Equality(object a, T b) + public static bool op_Equality(Enum a, T b) { return op_Equality(b, a); } - public static bool op_Inequality(T a, object b) + public static bool op_Inequality(T a, Enum b) { return !op_Equality(a, b); } - public static bool op_Inequality(object a, T b) + public static bool op_Inequality(Enum a, T b) { return !op_Equality(b, a); } - public static bool op_LessThan(T a, object b) + public static bool op_LessThan(T a, Enum b) { - if (!IsEnum(b, out var bType, out var underlyingType, throwOnNull: true)) - { - // False although it means nothing: an exception will be raised - return false; - } - if (underlyingType == typeof(UInt64)) + if (b.GetType().GetEnumUnderlyingType() == typeof(UInt64)) { return op_LessThan(a, Convert.ToUInt64(b)); } return op_LessThan(a, Convert.ToInt64(b)); } - public static bool op_LessThan(object a, T b) + public static bool op_LessThan(Enum a, T b) { return op_GreaterThan(b, a); } - public static bool op_GreaterThan(T a, object b) + public static bool op_GreaterThan(T a, Enum b) { - if (!IsEnum(b, out var bType, out var underlyingType, throwOnNull: true)) - { - // False although it means nothing: an exception will be raised - return false; - } - if (underlyingType == typeof(UInt64)) + if (b.GetType().GetEnumUnderlyingType() == typeof(UInt64)) { return op_GreaterThan(a, Convert.ToUInt64(b)); } return op_GreaterThan(a, Convert.ToInt64(b)); } - public static bool op_GreaterThan(object a, T b) + public static bool op_GreaterThan(Enum a, T b) { return op_LessThan(b, a); } - public static bool op_LessThanOrEqual(T a, object b) + public static bool op_LessThanOrEqual(T a, Enum b) { - if (!IsEnum(b, out var bType, out var underlyingType, throwOnNull: true)) - { - // False although it means nothing: an exception will be raised - return false; - } - if (underlyingType == typeof(UInt64)) + if (b.GetType().GetEnumUnderlyingType() == typeof(UInt64)) { return op_LessThanOrEqual(a, Convert.ToUInt64(b)); } return op_LessThanOrEqual(a, Convert.ToInt64(b)); } - public static bool op_LessThanOrEqual(object a, T b) + public static bool op_LessThanOrEqual(Enum a, T b) { return op_GreaterThanOrEqual(b, a); } - public static bool op_GreaterThanOrEqual(T a, object b) + public static bool op_GreaterThanOrEqual(T a, Enum b) { - if (!IsEnum(b, out var bType, out var underlyingType, throwOnNull: true)) - { - // False although it means nothing: an exception will be raised - return false; - } - if (underlyingType == typeof(UInt64)) + if (b.GetType().GetEnumUnderlyingType() == typeof(UInt64)) { return op_GreaterThanOrEqual(a, Convert.ToUInt64(b)); } return op_GreaterThanOrEqual(a, Convert.ToInt64(b)); } - public static bool op_GreaterThanOrEqual(object a, T b) + public static bool op_GreaterThanOrEqual(Enum a, T b) { return op_LessThanOrEqual(b, a); } From 4b5b7a1fbf9114db3b9163a824864f5c00f9f707 Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 31 Jul 2025 10:15:56 -0400 Subject: [PATCH 09/10] Fix comparison to null/None --- src/embed_tests/EnumTests.cs | 80 +++++++++++++++++++++++++++++++++++ src/runtime/Util/OpsHelper.cs | 56 ++++++++++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/src/embed_tests/EnumTests.cs b/src/embed_tests/EnumTests.cs index 52c40a56b..095944f59 100644 --- a/src/embed_tests/EnumTests.cs +++ b/src/embed_tests/EnumTests.cs @@ -544,5 +544,85 @@ def are_same7(): Assert.IsFalse(module.InvokeMethod("are_same6").As()); Assert.IsFalse(module.InvokeMethod("are_same7").As()); } + + private PyModule GetCSharpObjectsComparisonTestModule(string @operator) + { + return PyModule.FromString("GetCSharpObjectsComparisonTestModule", $@" +from clr import AddReference +AddReference(""Python.EmbeddingTest"") + +from Python.EmbeddingTest import * + +enum_value = {nameof(EnumTests)}.{nameof(VerticalDirection)}.{VerticalDirection.Up} + +def compare_with_none1(): + return enum_value {@operator} None + +def compare_with_none2(): + return None {@operator} enum_value + +def compare_with_csharp_object1(csharp_object): + return enum_value {@operator} csharp_object + +def compare_with_csharp_object2(csharp_object): + return csharp_object {@operator} enum_value +"); + } + + [TestCase("==", false)] + [TestCase("!=", true)] + public void EqualityComparisonWithNull(string @operator, bool expectedResult) + { + using var _ = Py.GIL(); + using var module = GetCSharpObjectsComparisonTestModule(@operator); + + Assert.AreEqual(expectedResult, module.InvokeMethod("compare_with_none1").As()); + Assert.AreEqual(expectedResult, module.InvokeMethod("compare_with_none2").As()); + + using var pyNull = ((TestClass)null).ToPython(); + Assert.AreEqual(expectedResult, module.InvokeMethod("compare_with_csharp_object1", pyNull).As()); + Assert.AreEqual(expectedResult, module.InvokeMethod("compare_with_csharp_object2", pyNull).As()); + } + + [TestCase("==", false)] + [TestCase("!=", true)] + public void ComparisonOperatorsWithNonEnumObjectsThrows(string @operator, bool expectedResult) + { + using var _ = Py.GIL(); + using var module = GetCSharpObjectsComparisonTestModule(@operator); + + using var pyCSharpObject = new TestClass().ToPython(); + Assert.AreEqual(expectedResult, module.InvokeMethod("compare_with_csharp_object1", pyCSharpObject).As()); + Assert.AreEqual(expectedResult, module.InvokeMethod("compare_with_csharp_object2", pyCSharpObject).As()); + } + + [Test] + public void ThrowsOnObjectComparisonOperators([Values("<", "<=", ">", ">=")] string @operator) + { + using var _ = Py.GIL(); + using var module = GetCSharpObjectsComparisonTestModule(@operator); + + using var pyCSharpObject = new TestClass().ToPython(); + Assert.Throws(() => module.InvokeMethod("compare_with_csharp_object1", pyCSharpObject)); + Assert.Throws(() => module.InvokeMethod("compare_with_csharp_object2", pyCSharpObject)); + } + + [Test] + public void ThrowsOnNullComparisonOperators([Values("<", "<=", ">", ">=")] string @operator) + { + using var _ = Py.GIL(); + using var module = GetCSharpObjectsComparisonTestModule(@operator); + + Assert.Throws(() => module.InvokeMethod("compare_with_none1").As()); + Assert.Throws(() => module.InvokeMethod("compare_with_none2").As()); + + using var pyNull = ((TestClass)null).ToPython(); + Assert.Throws(() => module.InvokeMethod("compare_with_csharp_object1", pyNull)); + Assert.Throws(() => module.InvokeMethod("compare_with_csharp_object2", pyNull)); + } + + public class TestClass + { + } } } diff --git a/src/runtime/Util/OpsHelper.cs b/src/runtime/Util/OpsHelper.cs index 8d2b8b8ef..89ce79e20 100644 --- a/src/runtime/Util/OpsHelper.cs +++ b/src/runtime/Util/OpsHelper.cs @@ -1,6 +1,8 @@ using System; using System.Linq.Expressions; using System.Reflection; +using System.Runtime.CompilerServices; + using static Python.Runtime.OpsHelper; namespace Python.Runtime @@ -444,6 +446,11 @@ public static bool op_Inequality(string a, T b) public static bool op_Equality(T a, Enum b) { + if (b == null) + { + return false; + } + if (b.GetType().GetEnumUnderlyingType() == typeof(UInt64)) { return op_Equality(a, Convert.ToUInt64(b)); @@ -466,8 +473,23 @@ public static bool op_Inequality(Enum a, T b) return !op_Equality(b, a); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ThrowOnNull(object obj, string @operator) + { + if (obj == null) + { + using (Py.GIL()) + { + Exceptions.RaiseTypeError($"'{@operator}' not supported between instances of '{typeof(T).Name}' and null/None"); + PythonException.ThrowLastAsClrException(); + } + } + } + public static bool op_LessThan(T a, Enum b) { + ThrowOnNull(b, "<"); + if (b.GetType().GetEnumUnderlyingType() == typeof(UInt64)) { return op_LessThan(a, Convert.ToUInt64(b)); @@ -477,11 +499,14 @@ public static bool op_LessThan(T a, Enum b) public static bool op_LessThan(Enum a, T b) { + ThrowOnNull(a, "<"); return op_GreaterThan(b, a); } public static bool op_GreaterThan(T a, Enum b) { + ThrowOnNull(b, ">"); + if (b.GetType().GetEnumUnderlyingType() == typeof(UInt64)) { return op_GreaterThan(a, Convert.ToUInt64(b)); @@ -491,11 +516,14 @@ public static bool op_GreaterThan(T a, Enum b) public static bool op_GreaterThan(Enum a, T b) { + ThrowOnNull(a, ">"); return op_LessThan(b, a); } public static bool op_LessThanOrEqual(T a, Enum b) { + ThrowOnNull(b, "<="); + if (b.GetType().GetEnumUnderlyingType() == typeof(UInt64)) { return op_LessThanOrEqual(a, Convert.ToUInt64(b)); @@ -505,11 +533,14 @@ public static bool op_LessThanOrEqual(T a, Enum b) public static bool op_LessThanOrEqual(Enum a, T b) { + ThrowOnNull(a, "<="); return op_GreaterThanOrEqual(b, a); } public static bool op_GreaterThanOrEqual(T a, Enum b) { + ThrowOnNull(b, ">="); + if (b.GetType().GetEnumUnderlyingType() == typeof(UInt64)) { return op_GreaterThanOrEqual(a, Convert.ToUInt64(b)); @@ -519,9 +550,34 @@ public static bool op_GreaterThanOrEqual(T a, Enum b) public static bool op_GreaterThanOrEqual(Enum a, T b) { + ThrowOnNull(a, ">="); return op_LessThanOrEqual(b, a); } #endregion + + #region Object equality operators + + public static bool op_Equality(T a, object b) + { + return false; + } + + public static bool op_Equality(object a, T b) + { + return false; + } + + public static bool op_Inequality(T a, object b) + { + return true; + } + + public static bool op_Inequality(object a, T b) + { + return true; + } + + #endregion } } From dc5eb2bd62503b68240e11d7e8b6899b4369a6cb Mon Sep 17 00:00:00 2001 From: Jhonathan Abreu Date: Thu, 31 Jul 2025 11:37:17 -0400 Subject: [PATCH 10/10] Minor change --- src/embed_tests/EnumTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/embed_tests/EnumTests.cs b/src/embed_tests/EnumTests.cs index 095944f59..f8f1789d2 100644 --- a/src/embed_tests/EnumTests.cs +++ b/src/embed_tests/EnumTests.cs @@ -586,7 +586,7 @@ public void EqualityComparisonWithNull(string @operator, bool expectedResult) [TestCase("==", false)] [TestCase("!=", true)] - public void ComparisonOperatorsWithNonEnumObjectsThrows(string @operator, bool expectedResult) + public void EqualityOperatorsWithNonEnumObjects(string @operator, bool expectedResult) { using var _ = Py.GIL(); using var module = GetCSharpObjectsComparisonTestModule(@operator);