Skip to content

Commit 590ef43

Browse files
committed
Warn if a method is missing
1 parent a4756b0 commit 590ef43

File tree

3 files changed

+47
-14
lines changed

3 files changed

+47
-14
lines changed

changelog.d/607.change.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
``attrs`` can now automatically detect your own implementations and infer ``init=False``, ``repr=False``, ``eq=False``, ``order=False``, and ``hash=False`` if you set ``@attr.s(auto_detect=True)``.
22
``attrs`` will ignore inherited methods.
3-
If the argument implies more than one methods (e.g. ``eq=True`` creates both ``__eq__`` and ``__ne__``), it's enough for *one* of them to exist and ``attrs`` will create *neither*.
3+
If the argument implies more than one method (e.g. ``eq=True`` creates both ``__eq__`` and ``__ne__``), it's enough for *one* of them to exist and ``attrs`` will create *neither*.
44

55
This feature requires Python 3.

src/attr/_make.py

+21-1
Original file line numberDiff line numberDiff line change
@@ -864,7 +864,11 @@ def attrs(
864864
865865
So for example by implementing ``__eq__`` on a class yourself,
866866
``attrs`` will deduce ``eq=False`` and won't create *neither*
867-
``__eq__`` *nor* ``__ne__``.
867+
``__eq__`` *nor* ``__ne__`` (but Python classes come with a sensible
868+
``__ne__`` by default, so it *should* be enough to only implement
869+
``__eq__`` in most cases). If you implement only a subset of the
870+
ordering methods but miss some, ``attrs`` will raise a `UserWarning`.
871+
Consider using `functools.total_ordering`.
868872
869873
Passing ``True`` or ``False`` to *init*, *repr*, *eq*, *order*,
870874
*cmp*, or *hash* overrides whatever *auto_detect* would determine.
@@ -1050,6 +1054,22 @@ def wrap(cls):
10501054
cls, order_, auto_detect, ("__lt__", "__le__", "__gt__", "__ge__")
10511055
):
10521056
builder.add_order()
1057+
elif not is_exc and order_ is None and auto_detect:
1058+
# This means it was auto-detected to not implement, warn the user
1059+
# if their class is not sound.
1060+
if not all(
1061+
[
1062+
_has_own_attribute(cls, meth_name)
1063+
for meth_name in ("__lt__", "__le__", "__gt__", "__ge__",)
1064+
]
1065+
):
1066+
warnings.warn(
1067+
"Your class implements only a subset of "
1068+
"__lt_, __le__, __gt__, and __ge__. "
1069+
"Its orderring behavior is not sound; consider "
1070+
"implementing them or using functools.total_ordering().",
1071+
stacklevel=2,
1072+
)
10531073

10541074
if (
10551075
hash_ is None

tests/test_make.py

+25-12
Original file line numberDiff line numberDiff line change
@@ -1811,6 +1811,9 @@ def test_detect_auto_order(self, slots, frozen):
18111811
18121812
It's surprisingly difficult to test this programmatically, so we do it
18131813
by hand.
1814+
1815+
Also verify that we warn the user if they don't implement all of the
1816+
ordering methods.
18141817
"""
18151818

18161819
def assert_not_set(cls, ex, meth_name):
@@ -1828,21 +1831,31 @@ def assert_none_set(cls, ex):
18281831
for m in ("le", "lt", "ge", "gt"):
18291832
assert_not_set(cls, ex, "__" + m + "__")
18301833

1831-
@attr.s(auto_detect=True, slots=slots, frozen=frozen)
1832-
class LE(object):
1833-
__le__ = 42
1834+
msg = "Your class implements only a subset of __lt_, __le__, __gt__,"
18341835

1835-
@attr.s(auto_detect=True, slots=slots, frozen=frozen)
1836-
class LT(object):
1837-
__lt__ = 42
1836+
with pytest.warns(UserWarning, match=msg):
18381837

1839-
@attr.s(auto_detect=True, slots=slots, frozen=frozen)
1840-
class GE(object):
1841-
__ge__ = 42
1838+
@attr.s(auto_detect=True, slots=slots, frozen=frozen)
1839+
class LE(object):
1840+
__le__ = 42
18421841

1843-
@attr.s(auto_detect=True, slots=slots, frozen=frozen)
1844-
class GT(object):
1845-
__gt__ = 42
1842+
with pytest.warns(UserWarning, match=msg):
1843+
1844+
@attr.s(auto_detect=True, slots=slots, frozen=frozen)
1845+
class LT(object):
1846+
__lt__ = 42
1847+
1848+
with pytest.warns(UserWarning, match=msg):
1849+
1850+
@attr.s(auto_detect=True, slots=slots, frozen=frozen)
1851+
class GE(object):
1852+
__ge__ = 42
1853+
1854+
with pytest.warns(UserWarning, match=msg):
1855+
1856+
@attr.s(auto_detect=True, slots=slots, frozen=frozen)
1857+
class GT(object):
1858+
__gt__ = 42
18461859

18471860
assert_none_set(LE, "__le__")
18481861
assert_none_set(LT, "__lt__")

0 commit comments

Comments
 (0)