Skip to content

Commit e677845

Browse files
committed
Implement auto_detect
Fixes #324
1 parent 609b10e commit e677845

File tree

7 files changed

+427
-33
lines changed

7 files changed

+427
-33
lines changed

changelog.d/607.change.rst

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
``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)``.
2+
``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*.
4+
5+
This feature requires Python 3.

docs/api.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ What follows is the API explanation, if you'd like a more hands-on introduction,
1616
Core
1717
----
1818

19-
.. autofunction:: attr.s(these=None, repr_ns=None, repr=True, cmp=None, hash=None, init=True, slots=False, frozen=False, weakref_slot=True, str=False, auto_attribs=False, kw_only=False, cache_hash=False, auto_exc=False, eq=None, order=None)
19+
.. autofunction:: attr.s(these=None, repr_ns=None, repr=None, cmp=None, hash=None, init=None, slots=False, frozen=False, weakref_slot=True, str=False, auto_attribs=False, kw_only=False, cache_hash=False, auto_exc=False, eq=None, order=None, auto_detect=False)
2020

2121
.. note::
2222

src/attr/__init__.pyi

+2
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ def attrs(
187187
auto_exc: bool = ...,
188188
eq: Optional[bool] = ...,
189189
order: Optional[bool] = ...,
190+
auto_detect: bool = ...,
190191
) -> _C: ...
191192
@overload
192193
def attrs(
@@ -207,6 +208,7 @@ def attrs(
207208
auto_exc: bool = ...,
208209
eq: Optional[bool] = ...,
209210
order: Optional[bool] = ...,
211+
auto_detect: bool = ...,
210212
) -> Callable[[_C], _C]: ...
211213

212214
# TODO: add support for returning NamedTuple from the mypy plugin

src/attr/_make.py

+107-26
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ def attrib(
218218
.. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01.
219219
.. versionadded:: 19.2.0 *eq* and *order*
220220
"""
221-
eq, order = _determine_eq_order(cmp, eq, order)
221+
eq, order = _determine_eq_order(cmp, eq, order, True)
222222

223223
if hash is not None and hash is not True and hash is not False:
224224
raise TypeError(
@@ -308,20 +308,32 @@ def _is_class_var(annot):
308308
return str(annot).startswith(_classvar_prefixes)
309309

310310

311-
def _get_annotations(cls):
311+
def _has_own_attribute(cls, attrib_name):
312312
"""
313-
Get annotations for *cls*.
313+
Check whether *cls* defines *attrib_name* (and doesn't just inherit it).
314+
315+
Requires Python 3.
314316
"""
315-
anns = getattr(cls, "__annotations__", None)
316-
if anns is None:
317-
return {}
317+
attr = getattr(cls, attrib_name, _sentinel)
318+
if attr is _sentinel:
319+
return False
318320

319-
# Verify that the annotations aren't merely inherited.
320321
for base_cls in cls.__mro__[1:]:
321-
if anns is getattr(base_cls, "__annotations__", None):
322-
return {}
322+
a = getattr(base_cls, attrib_name, None)
323+
if attr is a:
324+
return False
325+
326+
return True
327+
328+
329+
def _get_annotations(cls):
330+
"""
331+
Get annotations for *cls*.
332+
"""
333+
if _has_own_attribute(cls, "__annotations__"):
334+
return cls.__annotations__
323335

324-
return anns
336+
return {}
325337

326338

327339
def _counter_getter(e):
@@ -749,10 +761,10 @@ def _add_method_dunders(self, method):
749761
)
750762

751763

752-
def _determine_eq_order(cmp, eq, order):
764+
def _determine_eq_order(cmp, eq, order, default_eq):
753765
"""
754766
Validate the combination of *cmp*, *eq*, and *order*. Derive the effective
755-
values of eq and order.
767+
values of eq and order. If *eq* is None, set it to *default_eq*.
756768
"""
757769
if cmp is not None and any((eq is not None, order is not None)):
758770
raise ValueError("Don't mix `cmp` with `eq' and `order`.")
@@ -763,9 +775,10 @@ def _determine_eq_order(cmp, eq, order):
763775

764776
return cmp, cmp
765777

766-
# If left None, equality is on and ordering mirrors equality.
778+
# If left None, equality is set to the specified default and ordering
779+
# mirrors equality.
767780
if eq is None:
768-
eq = True
781+
eq = default_eq
769782

770783
if order is None:
771784
order = eq
@@ -776,14 +789,38 @@ def _determine_eq_order(cmp, eq, order):
776789
return eq, order
777790

778791

792+
def _determine_whether_to_implement(cls, flag, auto_detect, dunders):
793+
"""
794+
Check whether we should implement a set of methods for *cls*.
795+
796+
*flag* is the argument passed into @attr.s like 'init', *auto_detect* the
797+
same as passed into @attr.s and *dunders* is a tuple of attribute names
798+
whose presence signal that the user has implemented it themselves.
799+
800+
auto_detect must be False on Python 2.
801+
"""
802+
if flag is True or flag is None and auto_detect is False:
803+
return True
804+
805+
if flag is False:
806+
return False
807+
808+
# Logically, flag is None and auto_detect is True here.
809+
for dunder in dunders:
810+
if _has_own_attribute(cls, dunder):
811+
return False
812+
813+
return True
814+
815+
779816
def attrs(
780817
maybe_cls=None,
781818
these=None,
782819
repr_ns=None,
783-
repr=True,
820+
repr=None,
784821
cmp=None,
785822
hash=None,
786-
init=True,
823+
init=None,
787824
slots=False,
788825
frozen=False,
789826
weakref_slot=True,
@@ -794,6 +831,7 @@ def attrs(
794831
auto_exc=False,
795832
eq=None,
796833
order=None,
834+
auto_detect=False,
797835
):
798836
r"""
799837
A class decorator that adds `dunder
@@ -818,6 +856,22 @@ def attrs(
818856
:param str repr_ns: When using nested classes, there's no way in Python 2
819857
to automatically detect that. Therefore it's possible to set the
820858
namespace explicitly for a more meaningful ``repr`` output.
859+
:param bool auto_detect: Instead of setting the *init*, *repr*, *eq*,
860+
*order*, and *hash* arguments explicitly, assume they are set to
861+
``True`` **unless any** of the involved methods for one of the
862+
arguments is implemented in the *current* class (i.e. it is *not*
863+
inherited from some base class).
864+
865+
So for example by implementing ``__eq__`` on a class yourself,
866+
``attrs`` will deduce ``eq=False`` and won't create *neither*
867+
``__eq__`` *nor* ``__ne__``.
868+
869+
Passing ``True`` or ``False`` to *init*, *repr*, *eq*, *order*,
870+
*cmp*, or *hash* overrides whatever *auto_detect* would determine.
871+
872+
*auto_detect* requires Python 3. Setting it ``True`` on Python 2 raises
873+
a `PythonTooOldError`.
874+
821875
:param bool repr: Create a ``__repr__`` method with a human readable
822876
representation of ``attrs`` attributes..
823877
:param bool str: Create a ``__str__`` method that is identical to
@@ -886,8 +940,8 @@ def attrs(
886940
887941
:param bool weakref_slot: Make instances weak-referenceable. This has no
888942
effect unless ``slots`` is also enabled.
889-
:param bool auto_attribs: If True, collect `PEP 526`_-annotated attributes
890-
(Python 3.6 and later only) from the class body.
943+
:param bool auto_attribs: If ``True``, collect `PEP 526`_-annotated
944+
attributes (Python 3.6 and later only) from the class body.
891945
892946
In this case, you **must** annotate every field. If ``attrs``
893947
encounters a field that is set to an `attr.ib` but lacks a type
@@ -952,8 +1006,15 @@ def attrs(
9521006
.. versionadded:: 19.1.0 *auto_exc*
9531007
.. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01.
9541008
.. versionadded:: 19.2.0 *eq* and *order*
1009+
.. versionadded:: 20.1.0 *auto_detect*
9551010
"""
956-
eq, order = _determine_eq_order(cmp, eq, order)
1011+
if auto_detect and PY2:
1012+
raise PythonTooOldError(
1013+
"auto_detect only works on Python 3 and later."
1014+
)
1015+
1016+
eq_, order_ = _determine_eq_order(cmp, eq, order, None)
1017+
hash_ = hash # workaround the lack of nonlocal
9571018

9581019
def wrap(cls):
9591020

@@ -973,16 +1034,31 @@ def wrap(cls):
9731034
cache_hash,
9741035
is_exc,
9751036
)
976-
977-
if repr is True:
1037+
if _determine_whether_to_implement(
1038+
cls, repr, auto_detect, ("__repr__",)
1039+
):
9781040
builder.add_repr(repr_ns)
9791041
if str is True:
9801042
builder.add_str()
981-
if eq is True and not is_exc:
1043+
1044+
eq = _determine_whether_to_implement(
1045+
cls, eq_, auto_detect, ("__eq__", "__ne__")
1046+
)
1047+
if not is_exc and eq is True:
9821048
builder.add_eq()
983-
if order is True and not is_exc:
1049+
if not is_exc and _determine_whether_to_implement(
1050+
cls, order_, auto_detect, ("__lt__", "__le__", "__gt__", "__ge__")
1051+
):
9841052
builder.add_order()
9851053

1054+
if (
1055+
hash_ is None
1056+
and auto_detect is True
1057+
and _has_own_attribute(cls, "__hash__")
1058+
):
1059+
hash = False
1060+
else:
1061+
hash = hash_
9861062
if hash is not True and hash is not False and hash is not None:
9871063
# Can't use `hash in` because 1 == True for example.
9881064
raise TypeError(
@@ -1010,7 +1086,9 @@ def wrap(cls):
10101086
)
10111087
builder.make_unhashable()
10121088

1013-
if init is True:
1089+
if _determine_whether_to_implement(
1090+
cls, init, auto_detect, ("__init__",)
1091+
):
10141092
builder.add_init()
10151093
else:
10161094
if cache_hash:
@@ -1813,7 +1891,7 @@ def __init__(
18131891
eq=None,
18141892
order=None,
18151893
):
1816-
eq, order = _determine_eq_order(cmp, eq, order)
1894+
eq, order = _determine_eq_order(cmp, eq, order, True)
18171895

18181896
# Cache this descriptor here to speed things up later.
18191897
bound_setattr = _obj_setattr.__get__(self, Attribute)
@@ -2159,7 +2237,10 @@ def make_class(name, attrs, bases=(object,), **attributes_arguments):
21592237
attributes_arguments["eq"],
21602238
attributes_arguments["order"],
21612239
) = _determine_eq_order(
2162-
cmp, attributes_arguments.get("eq"), attributes_arguments.get("order")
2240+
cmp,
2241+
attributes_arguments.get("eq"),
2242+
attributes_arguments.get("order"),
2243+
True,
21632244
)
21642245

21652246
return _attrs(these=cls_dict, **attributes_arguments)(type_)

src/attr/exceptions.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ class UnannotatedAttributeError(RuntimeError):
5151

5252
class PythonTooOldError(RuntimeError):
5353
"""
54-
An ``attrs`` feature requiring a more recent python version has been used.
54+
It was attempted to use an ``attrs`` feature that requires a newer Python
55+
version.
5556
5657
.. versionadded:: 18.2.0
5758
"""

0 commit comments

Comments
 (0)