diff --git a/docs/reference.md b/docs/reference.md index 3057a0ea..99c5ef94 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -63,6 +63,38 @@ if (obj.IsNone()) } ``` +## Object comparisons + +The `PyObject` types can be compared with one another using the `is`, `==` and `!=` operators from Python. + +The equivalent to the `x is y` operator in Python is : + +```csharp +// Small numbers are the same object in Python (weird implementation detail) +PyObject obj1 = PyObject.From(true); +PyObject obj2 = PyObject.From(true); +if (obj1!.Is(obj2)) + Console.WriteLine("Objects are the same!"); +``` + +Equality can be accessed from the `.Equals` method or using the `==` operators in C#: + +```csharp +PyObject obj1 = PyObject.From(3.0); +PyObject obj2 = PyObject.From(3); +if (obj1 == obj2) + Console.WriteLine("Objects are equal!"); +``` + +Inequality can be accessed from the `.NotEquals` method or using the `!=` operators in C#: + +```csharp +PyObject obj1 = PyObject.From(3.0); +PyObject obj2 = PyObject.From(3); +if (obj1 != obj2) + Console.WriteLine("Objects are not equal!"); +``` + ## Python Locators CSnakes uses a `PythonLocator` to find the Python runtime on the host machine. The `PythonLocator` is a service that is registered with the dependency injection container and is used to find the Python runtime on the host machine. diff --git a/src/CSnakes.Runtime.Tests/Python/PyObjectTests.cs b/src/CSnakes.Runtime.Tests/Python/PyObjectTests.cs index e11d5963..c33dae76 100644 --- a/src/CSnakes.Runtime.Tests/Python/PyObjectTests.cs +++ b/src/CSnakes.Runtime.Tests/Python/PyObjectTests.cs @@ -1,58 +1,44 @@ using CSnakes.Runtime.Python; -using System.ComponentModel; namespace CSnakes.Runtime.Tests.Python; public class PyObjectTests : RuntimeTestBase { - private TypeConverter td = TypeDescriptor.GetConverter(typeof(PyObject)); [Fact] public void TestToString() { - using (GIL.Acquire()) - { - using PyObject? pyObj = td.ConvertFromString("Hello, World!") as PyObject; - Assert.NotNull(pyObj); - Assert.Equal("Hello, World!", pyObj!.ToString()); - } + using PyObject? pyObj = PyObject.From("Hello, World!"); + Assert.NotNull(pyObj); + Assert.Equal("Hello, World!", pyObj!.ToString()); } [Fact] public void TestObjectType() { - using (GIL.Acquire()) - { - using PyObject? pyObj = td.ConvertFromString("Hello, World!") as PyObject; - Assert.NotNull(pyObj); - Assert.Equal("", pyObj!.GetPythonType().ToString()); - } + using PyObject? pyObj = PyObject.From("Hello, World!"); + Assert.NotNull(pyObj); + Assert.Equal("", pyObj!.GetPythonType().ToString()); } [Fact] public void TestObjectGetAttr() { - using (GIL.Acquire()) - { - using PyObject? pyObj = td.ConvertFromString("Hello, World!") as PyObject; - Assert.NotNull(pyObj); - Assert.True(pyObj!.HasAttr("__doc__")); - using PyObject? pyObjDoc = pyObj!.GetAttr("__doc__"); - Assert.NotNull(pyObjDoc); - Assert.Contains("Create a new string ", pyObjDoc!.ToString()); - } + using PyObject? pyObj = PyObject.From("Hello, World!"); + Assert.NotNull(pyObj); + Assert.True(pyObj!.HasAttr("__doc__")); + using PyObject? pyObjDoc = pyObj!.GetAttr("__doc__"); + Assert.NotNull(pyObjDoc); + Assert.Contains("Create a new string ", pyObjDoc!.ToString()); } [Fact] public void TestObjectGetRepr() { - using (GIL.Acquire()) - { - using PyObject? pyObj = td.ConvertFromString("hello") as PyObject; - Assert.NotNull(pyObj); - string pyObjDoc = pyObj!.GetRepr(); - Assert.False(string.IsNullOrEmpty(pyObjDoc)); - Assert.Contains("'hello'", pyObjDoc); - } + using PyObject? pyObj = PyObject.From("hello"); + Assert.NotNull(pyObj); + string pyObjDoc = pyObj!.GetRepr(); + Assert.False(string.IsNullOrEmpty(pyObjDoc)); + Assert.Contains("'hello'", pyObjDoc); } [Fact] @@ -60,10 +46,72 @@ public void CannotUnsafelyGetHandleFromDisposedPyObject() { using (GIL.Acquire()) { - using PyObject? pyObj = td.ConvertFromString("hello") as PyObject; + using PyObject? pyObj = PyObject.From("hello"); Assert.NotNull(pyObj); pyObj.Dispose(); Assert.Throws(() => pyObj!.ToString()); } } + + [Fact] + public void TestObjectIsNone() + { + var obj1 = PyObject.None; + var obj2 = PyObject.None; + Assert.True(obj2.Is(obj2)); + } + + [Fact] + public void TestObjectIsSmallIntegers() { + // Small numbers are the same object in Python, weird implementation detail. + var obj1 = PyObject.From(42); + var obj2 = PyObject.From(42); + Assert.True(obj1!.Is(obj2!)); + } + + [InlineData(null, null)] + [InlineData(42, 42)] + [InlineData(42123434, 42123434)] + [InlineData("Hello!", "Hello!")] + [InlineData(3, 3.0)] + [Theory] + public void TestObjectEquals(object? o1, object? o2) + { + using var obj1 = PyObject.From(o1); + using var obj2 = PyObject.From(o2); + Assert.True(obj1!.Equals(obj2)); + Assert.True(obj1 == obj2); + } + + [Fact] + public void TestObjectEqualsCollection() + { + using var obj1 = PyObject.From>(["Hello!", "World!"]); + using var obj2 = PyObject.From>(["Hello!", "World!"]); + Assert.True(obj1!.Equals(obj2)); + Assert.True(obj1 == obj2); + } + + [InlineData(null, true)] + [InlineData(42, 44)] + [InlineData(42123434, 421234)] + [InlineData("Hello!", "Hello?")] + [InlineData(3, 3.2)] + [Theory] + public void TestObjectNotEquals(object? o1, object? o2) + { + using var obj1 = PyObject.From(o1); + using var obj2 = PyObject.From(o2); + Assert.True(obj1!.NotEquals(obj2)); + Assert.True(obj1 != obj2); + } + + [Fact] + public void TestObjectNotEqualsCollection() + { + using var obj1 = PyObject.From>(["Hello!", "World!"]); + using var obj2 = PyObject.From>(["Hello?", "World?"]); + Assert.True(obj1!.NotEquals(obj2)); + Assert.True(obj1 != obj2); + } } \ No newline at end of file diff --git a/src/CSnakes.Runtime/CPython/Object.cs b/src/CSnakes.Runtime/CPython/Object.cs index 3fcc9a6e..3a6d9d4a 100644 --- a/src/CSnakes.Runtime/CPython/Object.cs +++ b/src/CSnakes.Runtime/CPython/Object.cs @@ -4,6 +4,16 @@ namespace CSnakes.Runtime.CPython; internal unsafe partial class CPythonAPI { + internal enum RichComparisonType : int + { + LessThan = 0, + LessThanEqual = 1, + Equal = 2, + NotEqual = 3, + GreaterThan = 4, + GreaterThanEqual = 5 + } + [LibraryImport(PythonLibraryName)] internal static partial IntPtr PyObject_Repr(PyObject ob); @@ -154,4 +164,20 @@ internal static bool HasAttr(PyObject ob, string name) /// [LibraryImport(PythonLibraryName)] internal static partial int PyObject_SetItem(PyObject ob, PyObject key, PyObject value); + + [LibraryImport(PythonLibraryName)] + internal static partial int PyObject_Hash(PyObject ob); + + internal static bool PyObject_RichCompare(PyObject ob1, PyObject ob2, RichComparisonType comparisonType) + { + int result = PyObject_RichCompareBool(ob1, ob2, comparisonType); + if (result == -1) + { + PyObject.ThrowPythonExceptionAsClrException(); + } + return result == 1; + } + + [LibraryImport(PythonLibraryName)] + internal static partial int PyObject_RichCompareBool(PyObject ob1, PyObject ob2, RichComparisonType comparisonType); } diff --git a/src/CSnakes.Runtime/Python/PyObject.cs b/src/CSnakes.Runtime/Python/PyObject.cs index 1f5e26dc..98bfd07f 100644 --- a/src/CSnakes.Runtime/Python/PyObject.cs +++ b/src/CSnakes.Runtime/Python/PyObject.cs @@ -171,6 +171,71 @@ public virtual string GetRepr() /// true if None, else false public virtual bool IsNone() => CPythonAPI.IsNone(this); + /// + /// Are the objects the same instance, equivalent to the `is` operator in Python + /// + /// + /// + public virtual bool Is(PyObject other) + { + return DangerousGetHandle() == other.DangerousGetHandle(); + } + + public override bool Equals(object? obj) + { + if (obj is PyObject pyObj1) { + if (Is(pyObj1)) + return true; + + using (GIL.Acquire()) + { + return CPythonAPI.PyObject_RichCompare(this, pyObj1, CPythonAPI.RichComparisonType.Equal); + } + } + return base.Equals(obj); + } + + public bool NotEquals(object? obj) + { + if (obj is PyObject pyObj1) + { + if (Is(pyObj1)) + return false; + + using (GIL.Acquire()) + { + return CPythonAPI.PyObject_RichCompare(this, pyObj1, CPythonAPI.RichComparisonType.NotEqual); + } + } + return !base.Equals(obj); + } + + public static bool operator ==(PyObject? left, PyObject? right) + { + if (left is null) + return right is null; + return left.Equals(right); + } + public static bool operator !=(PyObject? left, PyObject? right) + { + if (left is null) + return right is not null; + return left.NotEquals(right); + } + + public override int GetHashCode() + { + using (GIL.Acquire()) + { + int hash = CPythonAPI.PyObject_Hash(this); + if (hash == -1) + { + ThrowPythonExceptionAsClrException(); + } + return hash; + } + } + public static PyObject None { get; } = new PyNoneObject(); ///