Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add methods to PyObject for Is, Equals and NotEquals #151

Merged
merged 6 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
112 changes: 80 additions & 32 deletions src/CSnakes.Runtime.Tests/Python/PyObjectTests.cs
Original file line number Diff line number Diff line change
@@ -1,69 +1,117 @@
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("<class 'str'>", pyObj!.GetPythonType().ToString());
}
using PyObject? pyObj = PyObject.From("Hello, World!");
Assert.NotNull(pyObj);
Assert.Equal("<class 'str'>", 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]
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<ObjectDisposedException>(() => 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<IEnumerable<string>>(["Hello!", "World!"]);
using var obj2 = PyObject.From<IEnumerable<string>>(["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<IEnumerable<string>>(["Hello!", "World!"]);
using var obj2 = PyObject.From<IEnumerable<string>>(["Hello?", "World?"]);
Assert.True(obj1!.NotEquals(obj2));
Assert.True(obj1 != obj2);
}
}
26 changes: 26 additions & 0 deletions src/CSnakes.Runtime/CPython/Object.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -154,4 +164,20 @@ internal static bool HasAttr(PyObject ob, string name)
/// <returns></returns>
[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);
}
65 changes: 65 additions & 0 deletions src/CSnakes.Runtime/Python/PyObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,71 @@ public virtual string GetRepr()
/// <returns>true if None, else false</returns>
public virtual bool IsNone() => CPythonAPI.IsNone(this);

/// <summary>
/// Are the objects the same instance, equivalent to the `is` operator in Python
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public virtual bool Is(PyObject other)
{
return DangerousGetHandle() == other.DangerousGetHandle();
tonybaloney marked this conversation as resolved.
Show resolved Hide resolved
}

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();

/// <summary>
Expand Down
Loading