Skip to content
Draft
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
70 changes: 38 additions & 32 deletions bindings/pyroot/cppyy/CPyCppyy/src/CPPInstance.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -532,22 +532,6 @@ static inline PyObject* eqneq_binop(CPPClass* klass, PyObject* self, PyObject* o
Py_RETURN_TRUE;
}

static inline void* cast_actual(void* obj) {
void* address = ((CPPInstance*)obj)->GetObject();
if (((CPPInstance*)obj)->fFlags & CPPInstance::kIsActual)
return address;

Cppyy::TCppType_t klass = ((CPPClass*)Py_TYPE((PyObject*)obj))->fCppType;
Cppyy::TCppType_t clActual = Cppyy::GetActualClass(klass, address);
if (clActual && clActual != klass) {
intptr_t offset = Cppyy::GetBaseOffset(
clActual, klass, address, -1 /* down-cast */, true /* report errors */);
if (offset != -1) address = (void*)((intptr_t)address + offset);
}

return address;
}


#define CPYCPPYY_ORDERED_OPERATOR_STUB(op, ometh, label) \
if (!ometh) { \
Expand Down Expand Up @@ -592,25 +576,47 @@ static PyObject* op_richcompare(CPPInstance* self, PyObject* other, int op)
result = eqneq_binop((CPPClass*)Py_TYPE(other), other, (PyObject*)self, op);
if (result) return result;

// default behavior: type + held pointer value defines identity; if both are
// CPPInstance objects, perform an additional autocast if need be
bool bIsEq = false;

if ((Py_TYPE(self) == Py_TYPE(other) && \
self->GetObject() == ((CPPInstance*)other)->GetObject())) {
// direct match
bIsEq = true;
} else if (CPPInstance_Check(other)) {
// try auto-cast match
void* addr1 = cast_actual(self);
void* addr2 = cast_actual(other);
bIsEq = addr1 && addr2 && (addr1 == addr2);
// for non-CPPInstance objects, let Python handle dispatch/fallback
if (!CPPInstance_Check(other)) {
Py_INCREF(Py_NotImplemented);
return Py_NotImplemented;
}

if ((op == Py_EQ && bIsEq) || (op == Py_NE && !bIsEq))
Py_RETURN_TRUE;
// if both proxies have the same type and at least one wraps nullptr,
// comparing nullness is unambiguous
auto ptr1 = self->GetObject();
auto ptr2 = ((CPPInstance*)other)->GetObject();
if(ptr1 == nullptr || ptr2 == nullptr) {
// Take this branch only for exact proxy type matches. Broadening this
// to inheritance-related types would be allowed in C++, but it would
// silently change previous cppyy behavior, where equality comparison
// of different-typed nullptr always resulted in `False`, straying away
// from C++.
if (Py_TYPE(self) == Py_TYPE(other)) {
bool bIsEq = ptr1 == ptr2;
if ((op == Py_EQ && bIsEq) || (op == Py_NE && !bIsEq))
Py_RETURN_TRUE;
Py_RETURN_FALSE;
}
}

Py_RETURN_FALSE;
// in the remaining cases, the semantics of the comparison are ambiguous, so we raise an exception
const char* opstr = op == Py_EQ ? "==" : "!=";
const char* lhs = Py_TYPE(self)->tp_name;
const char* rhs = Py_TYPE(other)->tp_name;
const char *msg =
"\nC++ equality operator '%s' is not defined for objects of type \"%s\" and \"%s\"."
"\n\nThe Python proxy no longer falls back to comparing by type and held C++ pointer "
"address, because that can be misleading: using `%s` may look like a value comparison "
"even though no corresponding C++ operator is available."
Comment on lines +609 to +611
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wording will age very quickly, I would rephrase in such a way to just indicate the reason for the failure and provide the user with suggestions on what to do (e.g. implent a custom __eq__ for their own classes where it makes sense).

"\n\nSome comparisons that are well-formed in C++ are still rejected here to avoid "
"changing legacy cppyy behavior silently."
"\n\nDefine a suitable C++ equality operator for these operands, or compare object identity explicitly "
"through another mechanism if that is what you intend."
"\n";

PyErr_Format(PyExc_TypeError, msg, opstr, lhs, rhs, opstr);
return NULL;
}

// ordered comparison operators
Expand Down
17 changes: 8 additions & 9 deletions bindings/pyroot/cppyy/cppyy/test/test_advancedcpp.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import pytest, os

Check failure on line 1 in bindings/pyroot/cppyy/cppyy/test/test_advancedcpp.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (F401)

bindings/pyroot/cppyy/cppyy/test/test_advancedcpp.py:1:16: F401 `os` imported but unused help: Remove unused import: `os`

Check failure on line 1 in bindings/pyroot/cppyy/cppyy/test/test_advancedcpp.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (E401)

bindings/pyroot/cppyy/cppyy/test/test_advancedcpp.py:1:1: E401 Multiple imports on one line help: Split imports
from pytest import mark, raises, skip
from support import setup_make, pylong, IS_WINDOWS, ispypy

Check failure on line 3 in bindings/pyroot/cppyy/cppyy/test/test_advancedcpp.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (F401)

bindings/pyroot/cppyy/cppyy/test/test_advancedcpp.py:3:21: F401 `support.setup_make` imported but unused help: Remove unused import: `support.setup_make`

Check failure on line 3 in bindings/pyroot/cppyy/cppyy/test/test_advancedcpp.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (I001)

bindings/pyroot/cppyy/cppyy/test/test_advancedcpp.py:1:1: I001 Import block is un-sorted or un-formatted help: Organize imports

Expand Down Expand Up @@ -154,7 +154,7 @@
import cppyy
gbl = cppyy.gbl

lib2 = cppyy.load_reflection_info("advancedcpp2_cxx")

Check failure on line 157 in bindings/pyroot/cppyy/cppyy/test/test_advancedcpp.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (F841)

bindings/pyroot/cppyy/cppyy/test/test_advancedcpp.py:157:9: F841 Local variable `lib2` is assigned to but never used help: Remove assignment to unused variable `lib2`

assert gbl.a_ns is gbl.a_ns
assert gbl.a_ns.d_ns is gbl.a_ns.d_ns
Expand Down Expand Up @@ -186,8 +186,8 @@
assert gbl.T1 is gbl.T1
assert gbl.T2 is gbl.T2
assert gbl.T3 is gbl.T3
assert not gbl.T1 is gbl.T2

Check failure on line 189 in bindings/pyroot/cppyy/cppyy/test/test_advancedcpp.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (E714)

bindings/pyroot/cppyy/cppyy/test/test_advancedcpp.py:189:20: E714 Test for object identity should be `is not` help: Convert to `is not`
assert not gbl.T2 is gbl.T3

Check failure on line 190 in bindings/pyroot/cppyy/cppyy/test/test_advancedcpp.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (E714)

bindings/pyroot/cppyy/cppyy/test/test_advancedcpp.py:190:20: E714 Test for object identity should be `is not` help: Convert to `is not`

assert gbl.T1('int') is gbl.T1('int')
assert gbl.T1(int) is gbl.T1('int')
Expand Down Expand Up @@ -445,10 +445,10 @@
#assert o == cppyy.bind_object(cobj, o.__class__)
#assert o == cppyy.bind_object(cobj, "some_concrete_class")
assert cppyy.addressof(o) == cppyy.addressof(cppyy.bind_object(addr, some_concrete_class))
assert o == cppyy.bind_object(addr, some_concrete_class)
assert o == cppyy.bind_object(addr, type(o))
assert o == cppyy.bind_object(addr, o.__class__)
assert o == cppyy.bind_object(addr, "some_concrete_class")
assert o is cppyy.bind_object(addr, some_concrete_class)
assert o is cppyy.bind_object(addr, type(o))
assert o is cppyy.bind_object(addr, o.__class__)
assert o is cppyy.bind_object(addr, "some_concrete_class")
raises(TypeError, cppyy.bind_object, addr, "does_not_exist")
raises(TypeError, cppyy.bind_object, addr, 1)

Expand All @@ -466,18 +466,18 @@
assert o is o2

o3 = cppyy.bind_object(addr, some_class_with_data)
assert not o is o3

Check failure on line 469 in bindings/pyroot/cppyy/cppyy/test/test_advancedcpp.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (E714)

bindings/pyroot/cppyy/cppyy/test/test_advancedcpp.py:469:20: E714 Test for object identity should be `is not` help: Convert to `is not`

d1 = some_class_with_data()
d2 = d1.gime_copy()
assert not d1 is d2

Check failure on line 473 in bindings/pyroot/cppyy/cppyy/test/test_advancedcpp.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (E714)

bindings/pyroot/cppyy/cppyy/test/test_advancedcpp.py:473:20: E714 Test for object identity should be `is not` help: Convert to `is not`

dd1a = d1.gime_data()
dd1b = d1.gime_data()
assert dd1a is dd1b

dd2 = d2.gime_data()
assert not dd1a is dd2

Check failure on line 480 in bindings/pyroot/cppyy/cppyy/test/test_advancedcpp.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (E714)

bindings/pyroot/cppyy/cppyy/test/test_advancedcpp.py:480:20: E714 Test for object identity should be `is not` help: Convert to `is not`
assert not dd1b is dd2

d2.__destruct__()
Expand All @@ -493,7 +493,6 @@
r4 = r1.m_other
assert r3 is r4

assert r3 == r2
assert r3 is r2

r3.extra = 42
Expand Down Expand Up @@ -531,13 +530,13 @@
b = base_class()
d = derived_class()

assert b == b.cycle(b)
assert b is b.cycle(b)
assert id(b) == id(b.cycle(b))
assert b == d.cycle(b)
assert b is d.cycle(b)
assert id(b) == id(d.cycle(b))
assert d == b.cycle(d)
assert d is b.cycle(d)
assert id(d) == id(b.cycle(d))
assert d == d.cycle(d)
assert d is d.cycle(d)
assert id(d) == id(d.cycle(d))

assert isinstance(b.cycle(b), base_class)
Expand Down
6 changes: 3 additions & 3 deletions bindings/pyroot/cppyy/cppyy/test/test_datatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,7 @@ def test09_global_ptr(self):

d = gbl.get_global_pod()
assert gbl.is_global_pod(d)
assert c == d
assert c is d
assert id(c) == id(d)

e = gbl.CppyyTestPod()
Expand Down Expand Up @@ -1008,8 +1008,8 @@ def test19_object_and_pointer_comparisons(self):
l1 = cppyy.bind_object(0, gbl.FourVector)
assert not l1

assert c1 != l1
assert l1 != c1
# assert c1 != l1
# assert l1 != c1

l2 = cppyy.bind_object(0, gbl.FourVector)
assert l1 == l2
Expand Down
2 changes: 1 addition & 1 deletion bindings/pyroot/cppyy/cppyy/test/test_regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ def test11_cobject_addressing(self):
a = cppyy.gbl.CObjA()
co = cppyy.ll.as_cobject(a)

assert a == cppyy.bind_object(co, 'CObjA')
assert a is cppyy.bind_object(co, 'CObjA')
assert a.m_int == 42
assert cppyy.bind_object(co, 'CObjA').m_int == 42

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def test_eq(self):
o = ROOT.TObject()

# TObject::IsEqual compares internal address
self.assertTrue(o == o)
self.assertTrue(o.IsEqual(o))

# Test comparison with no TObject
self.assertFalse(o == 1)
Expand All @@ -31,7 +31,7 @@ def test_ne(self):
o = ROOT.TObject()

# TObject::IsEqual compares internal address
self.assertFalse(o != o)
self.assertFalse(not o.IsEqual(o))

# Test comparison with no TObject
self.assertTrue(o != 1)
Expand Down
1 change: 0 additions & 1 deletion roottest/python/basic/PyROOT_datatypetest.py
Original file line number Diff line number Diff line change
Expand Up @@ -598,7 +598,6 @@ def test10_global_ptr(self):

d = gbl.get_global_pod()
assert gbl.is_global_pod(d)
assert c == d
assert c is d

e = gbl.CppyyTestPod()
Expand Down
9 changes: 6 additions & 3 deletions roottest/python/cpp/PyROOT_cpptests.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ def test12VoidPointerPassing( self ):

pZ = Z.getZ(0)
self.assertEqual( Z.checkAddressOfZ( pZ ), True )
self.assertEqual( pZ , Z.getZ(1) )
# self.assertEqual( pZ , Z.getZ(1) )

import array
# Not supported in p2.2
Expand Down Expand Up @@ -302,8 +302,11 @@ def test15ObjectAndPointerComparisons( self ):
l1 = MakeNullPointer( TLorentzVector )
self.assertFalse( l1 )

self.assertNotEqual( c1, l1 )
self.assertNotEqual( l1, c1 )
# Forbidden by the type system
with self.assertRaises(TypeError):
self.assertNotEqual( c1, l1 )
with self.assertRaises(TypeError):
self.assertNotEqual( l1, c1 )

l2 = MakeNullPointer( TLorentzVector )
self.assertEqual( l1, l2 )
Expand Down
4 changes: 3 additions & 1 deletion roottest/python/regression/PyROOT_regressiontests.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,9 @@ def test1IterateWithBaseIterator( self ):

b = a.begin()
e = a.end()
self.assertNotEqual( a, e )
# This comparison is forbidden by the type system
with self.assertRaises(TypeError):
a == e

b.__preinc__()
self.assertEqual( b, e )
Expand Down
Loading