| # 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"{'' if PY_3_13_PLUS else ' ' * 4}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"{'' if PY_3_13_PLUS else ' ' * 4}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"{'' if PY_3_13_PLUS else ' ' * 4}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__" |