commit:     a02ebef1465c64b16b8a5dc8c6c55524ac8e89ed
Author:     Brian Harring <ferringb <AT> gmail <DOT> com>
AuthorDate: Mon Oct 27 15:17:57 2025 +0000
Commit:     Arthur Zamarin <arthurzam <AT> gentoo <DOT> org>
CommitDate: Mon Oct 27 21:54:07 2025 +0000
URL:        
https://gitweb.gentoo.org/proj/pkgcore/snakeoil.git/commit/?id=a02ebef1

feat: Add GenericRichComparison

This is to help w/ migration away from inject_richcmp_from_cmp
for instances that use __attr_comparison__ logic.

Signed-off-by: Brian Harring <ferringb <AT> gmail.com>

 src/snakeoil/klass/__init__.py | 69 ++++++++++++++++++++++++++++++++++++++----
 tests/klass/test_init.py       | 40 ++++++++++++++++++++++++
 2 files changed, 103 insertions(+), 6 deletions(-)

diff --git a/src/snakeoil/klass/__init__.py b/src/snakeoil/klass/__init__.py
index 9691dd9..68d610a 100644
--- a/src/snakeoil/klass/__init__.py
+++ b/src/snakeoil/klass/__init__.py
@@ -42,7 +42,6 @@ from collections import deque
 from functools import wraps
 from importlib import import_module
 from operator import attrgetter
-from typing import Any
 
 from snakeoil.deprecation import deprecated as warn_deprecated
 
@@ -167,14 +166,22 @@ class GenericEquality(abc.ABC):
 
     __attr_comparison__: tuple[str, ...]
 
-    def __eq__(self, other: Any) -> bool:
+    def __eq__(
+        self, value, /, attr_comparison_override: tuple[str, ...] | None = None
+    ) -> bool:
         """
-        Comparison is down via comparing attributes listed in 
self.__attr_comparison__
+        Comparison is down via comparing attributes listed in 
self.__attr_comparison__,
+        or via the passed in attr_comparison_override.  That exists 
specifically to
+        simplify subclass partial reuse of the class when logic gets complex.
         """
-        if self is other:
+        if self is value:
             return True
-        for attr in self.__attr_comparison__:
-            if getattr(self, attr, sentinel) != getattr(other, attr, sentinel):
+        for attr in (
+            self.__attr_comparison__
+            if attr_comparison_override is None
+            else attr_comparison_override
+        ):
+            if getattr(self, attr, sentinel) != getattr(value, attr, sentinel):
                 return False
         return True
 
@@ -195,6 +202,56 @@ class GenericEquality(abc.ABC):
         return super().__init_subclass__()
 
 
+class GenericRichComparison(GenericEquality):
+    __slots__ = ()
+
+    def __lt__(self, value, attr_comparison_override: tuple[str, ...] | None = 
None):
+        if self is value:
+            return False
+        attrlist = (
+            self.__attr_comparison__
+            if attr_comparison_override is None
+            else attr_comparison_override
+        )
+        for attr in attrlist:
+            obj1, obj2 = getattr(self, attr, sentinel), getattr(value, attr, 
sentinel)
+            if obj1 is sentinel:
+                if obj2 is sentinel:
+                    continue
+                return True
+            elif obj2 is sentinel:
+                return False
+            if not (obj1 >= obj2):  # pyright: ignore[reportOperatorIssue]
+                return True
+        return False
+
+    def __le__(self, value, attr_comparison_override: tuple[str, ...] | None = 
None):
+        if self is value:
+            return True
+        attrlist = (
+            self.__attr_comparison__
+            if attr_comparison_override is None
+            else attr_comparison_override
+        )
+        for attr in attrlist:
+            obj1, obj2 = getattr(self, attr, sentinel), getattr(value, attr, 
sentinel)
+            if obj1 is sentinel:
+                if obj2 is sentinel:
+                    continue
+                return True
+            elif obj2 is sentinel:
+                return False
+            if not (obj1 > obj2):  # pyright: ignore[reportOperatorIssue]
+                return True
+        return False
+
+    def __gt__(self, value, attr_comparison_override: tuple[str, ...] | None = 
None):
+        return not self.__le__(value, 
attr_comparison_override=attr_comparison_override)
+
+    def __ge__(self, value, attr_comparison_override: tuple[str, ...] | None = 
None):
+        return not self.__lt__(value, 
attr_comparison_override=attr_comparison_override)
+
+
 @warn_deprecated(
     "generic_equality metaclass usage is deprecated; inherit from 
snakeoil.klass.GenericEquality instead."
 )

diff --git a/tests/klass/test_init.py b/tests/klass/test_init.py
index 32500dd..d39cabe 100644
--- a/tests/klass/test_init.py
+++ b/tests/klass/test_init.py
@@ -661,3 +661,43 @@ class TestGenericEquality:
             
subclass_disabling_must_be_allowed.__annotations__["__attr_comparison__"]
             is None
         ), "annotations weren't updated"
+
+
+class TestGenericRichComparison:
+    def test_it(self):
+        class kls(klass.GenericRichComparison):
+            __attr_comparison__ = ("x", "y")
+
+            def __init__(self, x=1, y=2):
+                self.x, self.y = x, y
+
+        obj1 = kls()
+        # these use identity check shortcuts, validate how it handles
+        # the same instance.
+        assert obj1 == obj1
+        assert obj1 <= obj1
+        assert obj1 >= obj1
+        assert not (obj1 < obj1)
+        assert not (obj1 > obj1)
+        assert not (obj1 != obj1)
+
+        # validate the usual scenarios
+        obj2 = kls()
+        assert not (obj1 < obj2)
+        assert obj1 <= obj2
+        assert not (obj1 != obj2)
+        assert obj1 >= obj2
+        assert not (obj1 > obj2)
+
+        obj1.x = 0
+        assert obj1 < obj2
+        assert obj1 <= obj2
+        assert not (obj1 > obj2)
+        assert not (obj1 >= obj2)
+
+        del obj1.x
+        assert obj1 != obj2
+        assert obj1 < obj2
+        assert obj1 <= obj2
+        assert not (obj1 > obj2)
+        assert not (obj1 >= obj2)

Reply via email to