Repository: python-attrs/attrs
Pull request #787 (2021-04-11 17:04:13)

New attr.cmp_using helper function
# Pull Request Check List

This is just a friendly reminder about the most common mistakes.  Please make sure that you tick all boxes.  But please read our [contribution guide](https://www.attrs.org/en/latest/contributing.html) at least once, it will save you unnecessary review cycles!

If an item doesn't apply to your pull request, **check it anyway** to make it apparent that there's nothing left to do.  If your pull request is a documentation fix or a trivial typo, feel free to delete the whole thing.

- [X] Added **tests** for changed code.
- [ ] New features have been added to our [Hypothesis testing strategy](https://github.com/python-attrs/attrs/blob/main/tests/strategies.py).
- [X] Changes or additions to public APIs are reflected in our type stubs (files ending in ``.pyi``).
    - [?] ...and used in the stub test file `tests/typing_example.py`.
- [ ] Updated **documentation** for changed code.
    - [ ] New functions/classes have to be added to `docs/api.rst` by hand.
    - [ ] Changes to the signature of `@attr.s()` have to be added by hand too.
    - [ ] Changed/added classes/methods/functions have appropriate `versionadded`, `versionchanged`, or `deprecated` [directives](http://www.sphinx-doc.org/en/stable/markup/para.html#directive-versionadded).  Find the appropriate next version in our [``__init__.py``](https://github.com/python-attrs/attrs/blob/main/src/attr/__init__.py) file.
- [ ] Documentation in `.rst` files is written using [semantic newlines](https://rhodesmill.org/brandon/2012/one-sentence-per-line/).
- [ ] Changes (and possible deprecations) have news fragments in [`changelog.d`](https://github.com/python-attrs/attrs/blob/main/changelog.d).

If you have *any* questions to *any* of the points above, just **submit and ask**!  This checklist is here to *help* you, not to deter you from contributing!


botant: Hello @hynek, I've added the function `cmp_using` in this pull request.

It is still draft for one reason.  I've used `functools.total_ordering` so complete the ordering functions, and I found that the functions such as `_ge_from_lt` are defined directly in the `functools` module, and because of that I can't set their `__qualname__` to get well-behaved methods.

My question therefore is "how much does this matter"? If this this matters, then we need an alternative to total_ordering.

Let me know what you think.

Thanks 
botant: Example of numpy usage:

```{python}
Python 3.8.5 (default, Jan 27 2021, 15:41:15) 
Type 'copyright', 'credits' or 'license' for more information
IPython 7.20.0 -- An enhanced Interactive Python. Type '?' for help.
PyDev console: using IPython 7.20.0
Python 3.8.5 (default, Jan 27 2021, 15:41:15) 
[GCC 9.3.0] on linux
import numpy as np
import attr
@attr.s(eq=True, order=False)
class A:
    value = attr.ib()
    
a1 = A(np.array([[1,2],[3,4]]))
a2 = A(np.array([[1,2],[3,4]]))
a1 == a2
Traceback (most recent call last):
  File "/home/antonio/.venv/attrs/lib/python3.8/site-packages/IPython/core/interactiveshell.py", line 3427, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-7-74942f9c531d>", line 1, in <module>
    a1 == a2
  File "<attrs generated eq __main__.A>", line 4, in __eq__
    return  (
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()

@attr.s(eq=True, order=False)
class B:
    value = attr.ib(eq=attr.cmp_using(eq=np.array_equal))
    
<ipython-input-8-6aa555853f34>:3: SyntaxWarning: You have customized the behavior of `eq` but not of `order`.  This is probably a bug.
  value = attr.ib(eq=attr.cmp_using(eq=np.array_equal))
b1 = B(np.array([[1,2],[3,4]]))
b2 = B(np.array([[1,2],[3,4]]))
b1 == b2
Out[11]: True
```
botant: Finally, I believe we should update the following warning: 

```{python}
@attr.s(eq=True, order=False)
class B:
    value = attr.ib(eq=attr.cmp_using(eq=np.array_equal))
    
<ipython-input-8-6aa555853f34>:3: SyntaxWarning: You have customized the behavior of `eq` but not of `order`.  This is probably a bug.
  value = attr.ib(eq=attr.cmp_using(eq=np.array_equal))
```

If it is possible to tell that `order=False` for the entire class, then we can skip the warning altogether. If not, we can suggest setting `order=False`.
hynek: Oh god that's a lot more code than I hoped for. 😅

Firstly, I don't think the qualname stuff matters at all, but it seems like your Comparable creation is more complicated than it needs to be.

Have you considered building a dict and then using `new_class()` (we have a helper in _compat for Python 2) instead of patching an existing class?

Also you broke almost all builds. :D
botant: Got it, I'll simplify it then.  :-)

PS: the builds are breaking intentionally, as I committed a failing test.
That is why I marked the PR as draft.

On Wed, 14 Apr 2021 at 12:10, Hynek Schlawack ***@***.***>
wrote:

> Oh god that's a lot more code than I hoped for. 😅
>
> Firstly, I don't think the qualname stuff matters at all, but it seems
> like your Comparable creation is more complicated than it needs to be.
>
> Have you considered building a dict and then using new_class() (we have a
> helper in _compat for Python 2) instead of patching an existing class?
>
> Also you broke almost all builds. :D
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/python-attrs/attrs/pull/787#issuecomment-819436506>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ABMJFPAEFEZHLO57VV6L2HTTIVZZ5ANCNFSM42X3QSBQ>
> .
>

hynek: _cough_ 😇
botant: So... here is a version using `new_class`, passing tests and 100% coverage.

I even installed Python 2.7 to be sure everything worked. :smile:  
hynek: @botant  thanks! would you mind removing the stray white space  edits from the pyi file? Or are they on purpose?

@Julian since IIRC this is implementing an idea of yours, would you mind having a look if it’s what you had in mind?
botant: From `__init__`? I found that weird as well, but I assumed it was okay since it was formatted by `black`.

Happy to rollback if you want.
hynek: Yeah, `__init__.pyi`. I think we might be still excluding pyi files from black? I’ll need to re-investigate.  Anyhow, the removed empty lines are nice, please give them back. :)
botant: Sure thing.

On Sat, Apr 24, 2021, 14:19 Hynek Schlawack ***@***.***>
wrote:

> Yeah, __init__.pyi. I think we might be still excluding pyi files from
> black? I’ll need to re-investigate. Anyhow, the removed empty lines are
> nice, please give them back. :)
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/python-attrs/attrs/pull/787#issuecomment-826091946>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ABMJFPHWCVBHPQJSEFQQAZTTKLANPANCNFSM42X3QSBQ>
> .
>

Julian: Nice! This looks great to me yeah!
botant: > Yeah, `__init__.pyi`. I think we might be still excluding pyi files from black? I’ll need to re-investigate. Anyhow, the removed empty lines are nice, please give them back. :)

Done!

Patch:
diff --git a/src/attr/__init__.py b/src/attr/__init__.py
index 88f544b..b575347 100644
--- a/src/attr/__init__.py
+++ b/src/attr/__init__.py
@@ -5,6 +5,7 @@ import sys
 from functools import partial
 
 from . import converters, exceptions, filters, setters, validators
+from ._cmp import cmp_using
 from ._config import get_run_validators, set_run_validators
 from ._funcs import asdict, assoc, astuple, evolve, has, resolve_types
 from ._make import (
@@ -52,6 +53,7 @@ __all__ = [
     "attrib",
     "attributes",
     "attrs",
+    "cmp_using",
     "converters",
     "evolve",
     "exceptions",
diff --git a/src/attr/_cmp.py b/src/attr/_cmp.py
new file mode 100644
index 0000000..89670bb
--- /dev/null
+++ b/src/attr/_cmp.py
@@ -0,0 +1,150 @@
+from __future__ import absolute_import, division, print_function
+
+import functools
+
+from ._compat import new_class
+from ._make import _make_ne
+
+
+_operation_names = {"eq": "==", "lt": "<", "le": "<=", "gt": ">", "ge": ">="}
+
+
+def cmp_using(
+    eq=None,
+    lt=None,
+    le=None,
+    gt=None,
+    ge=None,
+    require_same_type=True,
+    class_name="Comparable",
+):
+    """
+    Utility function that creates a class with customized equality and
+    ordering methods.
+
+    The resulting class will have a full set of ordering methods if
+    at least one of ``{lt, le, gt, ge}`` and ``eq``  are provided.
+
+    :param Optional[callable] eq: `callable` used to evaluate equality
+        of two objects.
+    :param Optional[callable] lt: `callable` used to evaluate whether
+        one object is less than another object.
+    :param Optional[callable] le: `callable` used to evaluate whether
+        one object is less than or equal to another object.
+    :param Optional[callable] gt: `callable` used to evaluate whether
+        one object is greater than another object.
+    :param Optional[callable] ge: `callable` used to evaluate whether
+        one object is greater than or equal to another object.
+
+    :param bool require_same_type: When `True`, equality and ordering methods
+        will return `NotImplemented` if objects are not of the same type.
+
+    :param Optional[str] class_name: Name of class. Defaults to 'Comparable'.
+
+    .. versionadded:: 21.1.0
+    """
+
+    body = {
+        "__slots__": ["value"],
+        "__init__": _make_init(),
+        "_requirements": [],
+        "_is_comparable_to": _is_comparable_to,
+    }
+
+    # Add operations.
+    num_order_functions = 0
+    has_eq_function = False
+
+    if eq is not None:
+        has_eq_function = True
+        body["__eq__"] = _make_operator("eq", eq)
+        body["__ne__"] = _make_ne()
+
+    if lt is not None:
+        num_order_functions += 1
+        body["__lt__"] = _make_operator("lt", lt)
+
+    if le is not None:
+        num_order_functions += 1
+        body["__le__"] = _make_operator("le", le)
+
+    if gt is not None:
+        num_order_functions += 1
+        body["__gt__"] = _make_operator("gt", gt)
+
+    if ge is not None:
+        num_order_functions += 1
+        body["__ge__"] = _make_operator("ge", ge)
+
+    type_ = new_class(class_name, (object,), {}, lambda ns: ns.update(body))
+
+    # Add same type requirement.
+    if require_same_type:
+        type_._requirements.append(_check_same_type)
+
+    # Add total ordering if at least one operation was defined.
+    if 0 < num_order_functions < 4:
+        if not has_eq_function:
+            # functools.total_ordering requires __eq__ to be defined,
+            # so raise early error here to keep a nice stack.
+            raise ValueError(
+                "eq must be define is order to complete ordering from "
+                "lt, le, gt, ge."
+            )
+        type_ = functools.total_ordering(type_)
+
+    return type_
+
+
+def _make_init():
+    """
+    Create __init__ method.
+    """
+
+    def __init__(self, value):
+        """
+        Initialize object with *value*.
+        """
+        self.value = value
+
+    return __init__
+
+
+def _make_operator(name, func):
+    """
+    Create operator method.
+    """
+
+    def method(self, other):
+        if not self._is_comparable_to(other):
+            return NotImplemented
+
+        result = func(self.value, other.value)
+        if result is NotImplemented:
+            return NotImplemented
+
+        return result
+
+    method.__name__ = "__%s__" % (name,)
+    method.__doc__ = "Return a %s b.  Computed by attrs." % (
+        _operation_names[name],
+    )
+
+    return method
+
+
+def _is_comparable_to(self, other):
+    """
+    Check whether `other` is comparable to `self`.
+    """
+    for func in self._requirements:
+        if not func(self, other):
+            return False
+    return True
+
+
+def _check_same_type(self, other):
+    """
+    Return True if *self* and *other* are of the same type, False otherwise.
+    """
+    return other.value.__class__ is self.value.__class__
diff --git a/src/attr/_cmp.pyi b/src/attr/_cmp.pyi
new file mode 100644
index 0000000..7093550
--- /dev/null
+++ b/src/attr/_cmp.pyi
@@ -0,0 +1,14 @@
+from typing import Type
+
+from . import _CompareWithType
+
+
+def cmp_using(
+    eq: Optional[_CompareWithType],
+    lt: Optional[_CompareWithType],
+    le: Optional[_CompareWithType],
+    gt: Optional[_CompareWithType],
+    ge: Optional[_CompareWithType],
+    require_same_type: bool,
+    class_name: str,
+) -> Type: ...


Test patch:
diff --git a/tests/test_cmp.py b/tests/test_cmp.py
new file mode 100644
index 0000000..fa6b318
--- /dev/null
+++ b/tests/test_cmp.py
@@ -0,0 +1,508 @@
+"""
+Tests for methods from `attrib._cmp`.
+"""
+
+from __future__ import absolute_import, division, print_function
+
+import pytest
+
+from attr._cmp import cmp_using
+from attr._compat import PY2
+
+
+# Test parameters.
+EqCSameType = cmp_using(eq=lambda a, b: a == b, class_name="EqCSameType")
+PartialOrderCSameType = cmp_using(
+    eq=lambda a, b: a == b,
+    lt=lambda a, b: a < b,
+    class_name="PartialOrderCSameType",
+)
+FullOrderCSameType = cmp_using(
+    eq=lambda a, b: a == b,
+    lt=lambda a, b: a < b,
+    le=lambda a, b: a <= b,
+    gt=lambda a, b: a > b,
+    ge=lambda a, b: a >= b,
+    class_name="FullOrderCSameType",
+)
+
+EqCAnyType = cmp_using(
+    eq=lambda a, b: a == b, require_same_type=False, class_name="EqCAnyType"
+)
+PartialOrderCAnyType = cmp_using(
+    eq=lambda a, b: a == b,
+    lt=lambda a, b: a < b,
+    require_same_type=False,
+    class_name="PartialOrderCAnyType",
+)
+
+
+eq_data = [
+    (EqCSameType, True),
+    (EqCAnyType, False),
+]
+
+order_data = [
+    (PartialOrderCSameType, True),
+    (PartialOrderCAnyType, False),
+    (FullOrderCSameType, True),
+]
+
+eq_ids = [c[0].__name__ for c in eq_data]
+order_ids = [c[0].__name__ for c in order_data]
+
+cmp_data = eq_data + order_data
+cmp_ids = eq_ids + order_ids
+
+
+class TestEqOrder(object):
+    """
+    Tests for eq and order related methods.
+    """
+
+    #########
+    # eq
+    #########
+    @pytest.mark.parametrize("cls, requires_same_type", cmp_data, ids=cmp_ids)
+    def test_equal_same_type(self, cls, requires_same_type):
+        """
+        Equal objects are detected as equal.
+        """
+        assert cls(1) == cls(1)
+        assert not (cls(1) != cls(1))
+
+    @pytest.mark.parametrize("cls, requires_same_type", cmp_data, ids=cmp_ids)
+    def test_unequal_same_type(self, cls, requires_same_type):
+        """
+        Unequal objects of correct type are detected as unequal.
+        """
+        assert cls(1) != cls(2)
+        assert not (cls(1) == cls(2))
+
+    @pytest.mark.parametrize("cls, requires_same_type", cmp_data, ids=cmp_ids)
+    def test_equal_different_type(self, cls, requires_same_type):
+        """
+        Equal values of different types are detected appropriately.
+        """
+        assert (cls(1) == cls(1.0)) == (not requires_same_type)
+        assert not (cls(1) != cls(1.0)) == (not requires_same_type)
+
+    #########
+    # lt
+    #########
+    @pytest.mark.skipif(PY2, reason="PY2 does not raise TypeError")
+    @pytest.mark.parametrize("cls, requires_same_type", eq_data, ids=eq_ids)
+    def test_lt_unorderable(self, cls, requires_same_type):
+        """
+        TypeError is raised if class does not implement __lt__.
+        """
+        with pytest.raises(TypeError):
+            cls(1) < cls(2)
+
+    @pytest.mark.parametrize(
+        "cls, requires_same_type", order_data, ids=order_ids
+    )
+    def test_lt_same_type(self, cls, requires_same_type):
+        """
+        Less-than objects are detected appropriately.
+        """
+        assert cls(1) < cls(2)
+        assert not (cls(2) < cls(1))
+
+    @pytest.mark.parametrize(
+        "cls, requires_same_type", order_data, ids=order_ids
+    )
+    def test_not_lt_same_type(self, cls, requires_same_type):
+        """
+        Not less-than objects are detected appropriately.
+        """
+        assert cls(2) >= cls(1)
+        assert not (cls(1) >= cls(2))
+
+    @pytest.mark.parametrize(
+        "cls, requires_same_type", order_data, ids=order_ids
+    )
+    def test_lt_different_type(self, cls, requires_same_type):
+        """
+        Less-than values of different types are detected appropriately.
+        """
+        if requires_same_type:
+            # Unlike __eq__, NotImplemented will cause an exception to be
+            # raised from __lt__.
+            if not PY2:
+                with pytest.raises(TypeError):
+                    cls(1) < cls(2.0)
+        else:
+            assert cls(1) < cls(2.0)
+            assert not (cls(2) < cls(1.0))
+
+    #########
+    # le
+    #########
+    @pytest.mark.skipif(PY2, reason="PY2 does not raise TypeError")
+    @pytest.mark.parametrize("cls, requires_same_type", eq_data, ids=eq_ids)
+    def test_le_unorderable(self, cls, requires_same_type):
+        """
+        TypeError is raised if class does not implement __le__.
+        """
+        with pytest.raises(TypeError):
+            cls(1) <= cls(2)
+
+    @pytest.mark.parametrize(
+        "cls, requires_same_type", order_data, ids=order_ids
+    )
+    def test_le_same_type(self, cls, requires_same_type):
+        """
+        Less-than-or-equal objects are detected appropriately.
+        """
+        assert cls(1) <= cls(1)
+        assert cls(1) <= cls(2)
+        assert not (cls(2) <= cls(1))
+
+    @pytest.mark.parametrize(
+        "cls, requires_same_type", order_data, ids=order_ids
+    )
+    def test_not_le_same_type(self, cls, requires_same_type):
+        """
+        Not less-than-or-equal objects are detected appropriately.
+        """
+        assert cls(2) > cls(1)
+        assert not (cls(1) > cls(1))
+        assert not (cls(1) > cls(2))
+
+    @pytest.mark.parametrize(
+        "cls, requires_same_type", order_data, ids=order_ids
+    )
+    def test_le_different_type(self, cls, requires_same_type):
+        """
+        Less-than-or-equal values of diff. types are detected appropriately.
+        """
+        if requires_same_type:
+            # Unlike __eq__, NotImplemented will cause an exception to be
+            # raised from __le__.
+            if not PY2:
+                with pytest.raises(TypeError):
+                    cls(1) <= cls(2.0)
+        else:
+            assert cls(1) <= cls(2.0)
+            assert cls(1) <= cls(1.0)
+            assert not (cls(2) <= cls(1.0))
+
+    #########
+    # gt
+    #########
+    @pytest.mark.skipif(PY2, reason="PY2 does not raise TypeError")
+    @pytest.mark.parametrize("cls, requires_same_type", eq_data, ids=eq_ids)
+    def test_gt_unorderable(self, cls, requires_same_type):
+        """
+        TypeError is raised if class does not implement __gt__.
+        """
+        with pytest.raises(TypeError):
+            cls(2) > cls(1)
+
+    @pytest.mark.parametrize(
+        "cls, requires_same_type", order_data, ids=order_ids
+    )
+    def test_gt_same_type(self, cls, requires_same_type):
+        """
+        Greater-than objects are detected appropriately.
+        """
+        assert cls(2) > cls(1)
+        assert not (cls(1) > cls(2))
+
+    @pytest.mark.parametrize(
+        "cls, requires_same_type", order_data, ids=order_ids
+    )
+    def test_not_gt_same_type(self, cls, requires_same_type):
+        """
+        Not greater-than objects are detected appropriately.
+        """
+        assert cls(1) <= cls(2)
+        assert not (cls(2) <= cls(1))
+
+    @pytest.mark.parametrize(
+        "cls, requires_same_type", order_data, ids=order_ids
+    )
+    def test_gt_different_type(self, cls, requires_same_type):
+        """
+        Greater-than values of different types are detected appropriately.
+        """
+        if requires_same_type:
+            # Unlike __eq__, NotImplemented will cause an exception to be
+            # raised from __gt__.
+            if not PY2:
+                with pytest.raises(TypeError):
+                    cls(2) > cls(1.0)
+        else:
+            assert cls(2) > cls(1.0)
+            assert not (cls(1) > cls(2.0))
+
+    #########
+    # ge
+    #########
+    @pytest.mark.skipif(PY2, reason="PY2 does not raise TypeError")
+    @pytest.mark.parametrize("cls, requires_same_type", eq_data, ids=eq_ids)
+    def test_ge_unorderable(self, cls, requires_same_type):
+        """
+        TypeError is raised if class does not implement __ge__.
+        """
+        with pytest.raises(TypeError):
+            cls(2) >= cls(1)
+
+    @pytest.mark.parametrize(
+        "cls, requires_same_type", order_data, ids=order_ids
+    )
+    def test_ge_same_type(self, cls, requires_same_type):
+        """
+        Greater-than-or-equal objects are detected appropriately.
+        """
+        assert cls(1) >= cls(1)
+        assert cls(2) >= cls(1)
+        assert not (cls(1) >= cls(2))
+
+    @pytest.mark.parametrize(
+        "cls, requires_same_type", order_data, ids=order_ids
+    )
+    def test_not_ge_same_type(self, cls, requires_same_type):
+        """
+        Not greater-than-or-equal objects are detected appropriately.
+        """
+        assert cls(1) < cls(2)
+        assert not (cls(1) < cls(1))
+        assert not (cls(2) < cls(1))
+
+    @pytest.mark.parametrize(
+        "cls, requires_same_type", order_data, ids=order_ids
+    )
+    def test_ge_different_type(self, cls, requires_same_type):
+        """
+        Greater-than-or-equal values of diff. types are detected appropriately.
+        """
+        if requires_same_type:
+            # Unlike __eq__, NotImplemented will cause an exception to be
+            # raised from __ge__.
+            if not PY2:
+                with pytest.raises(TypeError):
+                    cls(2) >= cls(1.0)
+        else:
+            assert cls(2) >= cls(2.0)
+            assert cls(2) >= cls(1.0)
+            assert not (cls(1) >= cls(2.0))
+
+
+class TestDundersUnnamedClass(object):
+    """
+    Tests for dunder attributes of unnamed classes.
+    """
+
+    cls = cmp_using(eq=lambda a, b: a == b)
+
+    def test_class(self):
+        """
+        Class name and qualified name should be well behaved.
+        """
+        assert self.cls.__name__ == "Comparable"
+        if not PY2:
+            assert self.cls.__qualname__ == "Comparable"
+
+    def test_eq(self):
+        """
+        __eq__ docstring and qualified name should be well behaved.
+        """
+        method = self.cls.__eq__
+        assert method.__doc__.strip() == "Return a == b.  Computed by attrs."
+        assert method.__name__ == "__eq__"
+
+    def test_ne(self):
+        """
+        __ne__ docstring and qualified name should be well behaved.
+        """
+        method = self.cls.__ne__
+        assert method.__doc__.strip() == (
+            "Check equality and either forward a NotImplemented or\n"
+            "        return the result negated."
+        )
+        assert method.__name__ == "__ne__"
+
+
+class TestTotalOrderingException(object):
+    """
+    Test for exceptions related to total ordering.
+    """
+
+    def test_eq_must_specified(self):
+        """
+        `total_ordering` requires `__eq__` to be specified.
+        """
+        with pytest.raises(ValueError) as ei:
+            cmp_using(lt=lambda a, b: a < b)
+
+        assert ei.value.args[0] == (
+            "eq must be define is order to complete ordering from "
+            "lt, le, gt, ge."
+        )
+
+
+class TestNotImplementedIsPropagated(object):
+    """
+    Test related to functions that return NotImplemented.
+    """
+
+    def test_not_implemented_is_propagated(self):
+        """
+        If the comparison function returns NotImplemented,
+        the dunder method should too.
+        """
+        C = cmp_using(eq=lambda a, b: NotImplemented if a == 1 else a == b)
+
+        assert C(2) == C(2)
+        assert C(1) != C(1)
+
+
+class TestDundersPartialOrdering(object):
+    """
+    Tests for dunder attributes of classes with partial ordering.
+    """
+
+    cls = PartialOrderCSameType
+
+    def test_class(self):
+        """
+        Class name and qualified name should be well behaved.
+        """
+        assert self.cls.__name__ == "PartialOrderCSameType"
+        if not PY2:
+            assert self.cls.__qualname__ == "PartialOrderCSameType"
+
+    def test_eq(self):
+        """
+        __eq__ docstring and qualified name should be well behaved.
+        """
+        method = self.cls.__eq__
+        assert method.__doc__.strip() == "Return a == b.  Computed by attrs."
+        assert method.__name__ == "__eq__"
+
+    def test_ne(self):
+        """
+        __ne__ docstring and qualified name should be well behaved.
+        """
+        method = self.cls.__ne__
+        assert method.__doc__.strip() == (
+            "Check equality and either forward a NotImplemented or\n"
+            "        return the result negated."
+        )
+        assert method.__name__ == "__ne__"
+
+    def test_lt(self):
+        """
+        __lt__ docstring and qualified name should be well behaved.
+        """
+        method = self.cls.__lt__
+        assert method.__doc__.strip() == "Return a < b.  Computed by attrs."
+        assert method.__name__ == "__lt__"
+
+    def test_le(self):
+        """
+        __le__ docstring and qualified name should be well behaved.
+        """
+        method = self.cls.__le__
+        if PY2:
+            assert method.__doc__ == "x.__le__(y) <==> x<=y"
+        else:
+            assert method.__doc__.strip().startswith(
+                "Return a <= b.  Computed by @total_ordering from"
+            )
+        assert method.__name__ == "__le__"
+
+    def test_gt(self):
+        """
+        __gt__ docstring and qualified name should be well behaved.
+        """
+        method = self.cls.__gt__
+        if PY2:
+            assert method.__doc__ == "x.__gt__(y) <==> x>y"
+        else:
+            assert method.__doc__.strip().startswith(
+                "Return a > b.  Computed by @total_ordering from"
+            )
+        assert method.__name__ == "__gt__"
+
+    def test_ge(self):
+        """
+        __ge__ docstring and qualified name should be well behaved.
+        """
+        method = self.cls.__ge__
+        if PY2:
+            assert method.__doc__ == "x.__ge__(y) <==> x>=y"
+        else:
+            assert method.__doc__.strip().startswith(
+                "Return a >= b.  Computed by @total_ordering from"
+            )
+        assert method.__name__ == "__ge__"
+
+
+class TestDundersFullOrdering(object):
+    """
+    Tests for dunder attributes of classes with full ordering.
+    """
+
+    cls = FullOrderCSameType
+
+    def test_class(self):
+        """
+        Class name and qualified name should be well behaved.
+        """
+        assert self.cls.__name__ == "FullOrderCSameType"
+        if not PY2:
+            assert self.cls.__qualname__ == "FullOrderCSameType"
+
+    def test_eq(self):
+        """
+        __eq__ docstring and qualified name should be well behaved.
+        """
+        method = self.cls.__eq__
+        assert method.__doc__.strip() == "Return a == b.  Computed by attrs."
+        assert method.__name__ == "__eq__"
+
+    def test_ne(self):
+        """
+        __ne__ docstring and qualified name should be well behaved.
+        """
+        method = self.cls.__ne__
+        assert method.__doc__.strip() == (
+            "Check equality and either forward a NotImplemented or\n"
+            "        return the result negated."
+        )
+        assert method.__name__ == "__ne__"
+
+    def test_lt(self):
+        """
+        __lt__ docstring and qualified name should be well behaved.
+        """
+        method = self.cls.__lt__
+        assert method.__doc__.strip() == "Return a < b.  Computed by attrs."
+        assert method.__name__ == "__lt__"
+
+    def test_le(self):
+        """
+        __le__ docstring and qualified name should be well behaved.
+        """
+        method = self.cls.__le__
+        assert method.__doc__.strip() == "Return a <= b.  Computed by attrs."
+        assert method.__name__ == "__le__"
+
+    def test_gt(self):
+        """
+        __gt__ docstring and qualified name should be well behaved.
+        """
+        method = self.cls.__gt__
+        assert method.__doc__.strip() == "Return a > b.  Computed by attrs."
+        assert method.__name__ == "__gt__"
+
+    def test_ge(self):
+        """
+        __ge__ docstring and qualified name should be well behaved.
+        """
+        method = self.cls.__ge__
+        assert method.__doc__.strip() == "Return a >= b.  Computed by attrs."
+        assert method.__name__ == "__ge__"