Skip to content

Commit 1246c43

Browse files
committed
Implement auto_detect
Fixes #324
1 parent 618373e commit 1246c43

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):
@@ -754,10 +766,10 @@ def _add_method_dunders(self, method):
754766
)
755767

756768

757-
def _determine_eq_order(cmp, eq, order):
769+
def _determine_eq_order(cmp, eq, order, default_eq):
758770
"""
759771
Validate the combination of *cmp*, *eq*, and *order*. Derive the effective
760-
values of eq and order.
772+
values of eq and order. If *eq* is None, set it to *default_eq*.
761773
"""
762774
if cmp is not None and any((eq is not None, order is not None)):
763775
raise ValueError("Don't mix `cmp` with `eq' and `order`.")
@@ -768,9 +780,10 @@ def _determine_eq_order(cmp, eq, order):
768780

769781
return cmp, cmp
770782

771-
# If left None, equality is on and ordering mirrors equality.
783+
# If left None, equality is set to the specified default and ordering
784+
# mirrors equality.
772785
if eq is None:
773-
eq = True
786+
eq = default_eq
774787

775788
if order is None:
776789
order = eq
@@ -781,14 +794,38 @@ def _determine_eq_order(cmp, eq, order):
781794
return eq, order
782795

783796

797+
def _determine_whether_to_implement(cls, flag, auto_detect, dunders):
798+
"""
799+
Check whether we should implement a set of methods for *cls*.
800+
801+
*flag* is the argument passed into @attr.s like 'init', *auto_detect* the
802+
same as passed into @attr.s and *dunders* is a tuple of attribute names
803+
whose presence signal that the user has implemented it themselves.
804+
805+
auto_detect must be False on Python 2.
806+
"""
807+
if flag is True or flag is None and auto_detect is False:
808+
return True
809+
810+
if flag is False:
811+
return False
812+
813+
# Logically, flag is None and auto_detect is True here.
814+
for dunder in dunders:
815+
if _has_own_attribute(cls, dunder):
816+
return False
817+
818+
return True
819+
820+
784821
def attrs(
785822
maybe_cls=None,
786823
these=None,
787824
repr_ns=None,
788-
repr=True,
825+
repr=None,
789826
cmp=None,
790827
hash=None,
791-
init=True,
828+
init=None,
792829
slots=False,
793830
frozen=False,
794831
weakref_slot=True,
@@ -799,6 +836,7 @@ def attrs(
799836
auto_exc=False,
800837
eq=None,
801838
order=None,
839+
auto_detect=False,
802840
):
803841
r"""
804842
A class decorator that adds `dunder
@@ -823,6 +861,22 @@ def attrs(
823861
:param str repr_ns: When using nested classes, there's no way in Python 2
824862
to automatically detect that. Therefore it's possible to set the
825863
namespace explicitly for a more meaningful ``repr`` output.
864+
:param bool auto_detect: Instead of setting the *init*, *repr*, *eq*,
865+
*order*, and *hash* arguments explicitly, assume they are set to
866+
``True`` **unless any** of the involved methods for one of the
867+
arguments is implemented in the *current* class (i.e. it is *not*
868+
inherited from some base class).
869+
870+
So for example by implementing ``__eq__`` on a class yourself,
871+
``attrs`` will deduce ``eq=False`` and won't create *neither*
872+
``__eq__`` *nor* ``__ne__``.
873+
874+
Passing ``True`` or ``False`` to *init*, *repr*, *eq*, *order*,
875+
*cmp*, or *hash* overrides whatever *auto_detect* would determine.
876+
877+
*auto_detect* requires Python 3. Setting it ``True`` on Python 2 raises
878+
a `PythonTooOldError`.
879+
826880
:param bool repr: Create a ``__repr__`` method with a human readable
827881
representation of ``attrs`` attributes..
828882
:param bool str: Create a ``__str__`` method that is identical to
@@ -891,8 +945,8 @@ def attrs(
891945
892946
:param bool weakref_slot: Make instances weak-referenceable. This has no
893947
effect unless ``slots`` is also enabled.
894-
:param bool auto_attribs: If True, collect `PEP 526`_-annotated attributes
895-
(Python 3.6 and later only) from the class body.
948+
:param bool auto_attribs: If ``True``, collect `PEP 526`_-annotated
949+
attributes (Python 3.6 and later only) from the class body.
896950
897951
In this case, you **must** annotate every field. If ``attrs``
898952
encounters a field that is set to an `attr.ib` but lacks a type
@@ -957,8 +1011,15 @@ def attrs(
9571011
.. versionadded:: 19.1.0 *auto_exc*
9581012
.. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01.
9591013
.. versionadded:: 19.2.0 *eq* and *order*
1014+
.. versionadded:: 20.1.0 *auto_detect*
9601015
"""
961-
eq, order = _determine_eq_order(cmp, eq, order)
1016+
if auto_detect and PY2:
1017+
raise PythonTooOldError(
1018+
"auto_detect only works on Python 3 and later."
1019+
)
1020+
1021+
eq_, order_ = _determine_eq_order(cmp, eq, order, None)
1022+
hash_ = hash # workaround the lack of nonlocal
9621023

9631024
def wrap(cls):
9641025

@@ -978,16 +1039,31 @@ def wrap(cls):
9781039
cache_hash,
9791040
is_exc,
9801041
)
981-
982-
if repr is True:
1042+
if _determine_whether_to_implement(
1043+
cls, repr, auto_detect, ("__repr__",)
1044+
):
9831045
builder.add_repr(repr_ns)
9841046
if str is True:
9851047
builder.add_str()
986-
if eq is True and not is_exc:
1048+
1049+
eq = _determine_whether_to_implement(
1050+
cls, eq_, auto_detect, ("__eq__", "__ne__")
1051+
)
1052+
if not is_exc and eq is True:
9871053
builder.add_eq()
988-
if order is True and not is_exc:
1054+
if not is_exc and _determine_whether_to_implement(
1055+
cls, order_, auto_detect, ("__lt__", "__le__", "__gt__", "__ge__")
1056+
):
9891057
builder.add_order()
9901058

1059+
if (
1060+
hash_ is None
1061+
and auto_detect is True
1062+
and _has_own_attribute(cls, "__hash__")
1063+
):
1064+
hash = False
1065+
else:
1066+
hash = hash_
9911067
if hash is not True and hash is not False and hash is not None:
9921068
# Can't use `hash in` because 1 == True for example.
9931069
raise TypeError(
@@ -1015,7 +1091,9 @@ def wrap(cls):
10151091
)
10161092
builder.make_unhashable()
10171093

1018-
if init is True:
1094+
if _determine_whether_to_implement(
1095+
cls, init, auto_detect, ("__init__",)
1096+
):
10191097
builder.add_init()
10201098
else:
10211099
if cache_hash:
@@ -1832,7 +1910,7 @@ def __init__(
18321910
eq=None,
18331911
order=None,
18341912
):
1835-
eq, order = _determine_eq_order(cmp, eq, order)
1913+
eq, order = _determine_eq_order(cmp, eq, order, True)
18361914

18371915
# Cache this descriptor here to speed things up later.
18381916
bound_setattr = _obj_setattr.__get__(self, Attribute)
@@ -2178,7 +2256,10 @@ def make_class(name, attrs, bases=(object,), **attributes_arguments):
21782256
attributes_arguments["eq"],
21792257
attributes_arguments["order"],
21802258
) = _determine_eq_order(
2181-
cmp, attributes_arguments.get("eq"), attributes_arguments.get("order")
2259+
cmp,
2260+
attributes_arguments.get("eq"),
2261+
attributes_arguments.get("order"),
2262+
True,
21822263
)
21832264

21842265
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)