attrs/tests/test_cmp.py

506 lines
15 KiB
Python

# SPDX-License-Identifier: MIT
"""
Tests for methods from `attrib._cmp`.
"""
import pytest
from attr._cmp import cmp_using
from attr._compat import PY_3_13_PLUS
# 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
# Compiler strips indents from docstrings in Python 3.13+
indent = "" if PY_3_13_PLUS else " " * 8
class TestEqOrder:
"""
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.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__.
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.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__.
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.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__.
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.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__.
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:
"""
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"
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"
f"{indent}return the result negated."
)
assert method.__name__ == "__ne__"
class TestTotalOrderingException:
"""
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:
"""
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:
"""
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"
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"
f"{indent}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().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__
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__
assert method.__doc__.strip().startswith(
"Return a >= b. Computed by @total_ordering from"
)
assert method.__name__ == "__ge__"
class TestDundersFullOrdering:
"""
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"
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"
f"{indent}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__"