Skip to content

Commit 86589a1

Browse files
committed
Implement auto_detect
Fixes #324
1 parent 44289f2 commit 86589a1

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
@@ -192,7 +192,7 @@ def attrib(
192192
.. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01.
193193
.. versionadded:: 19.2.0 *eq* and *order*
194194
"""
195-
eq, order = _determine_eq_order(cmp, eq, order)
195+
eq, order = _determine_eq_order(cmp, eq, order, True)
196196

197197
if hash is not None and hash is not True and hash is not False:
198198
raise TypeError(
@@ -282,20 +282,32 @@ def _is_class_var(annot):
282282
return str(annot).startswith(_classvar_prefixes)
283283

284284

285-
def _get_annotations(cls):
285+
def _has_own_attribute(cls, attrib_name):
286286
"""
287-
Get annotations for *cls*.
287+
Check whether *cls* defines *attrib_name* (and doesn't just inherit it).
288+
289+
Requires Python 3.
288290
"""
289-
anns = getattr(cls, "__annotations__", None)
290-
if anns is None:
291-
return {}
291+
attr = getattr(cls, attrib_name, _sentinel)
292+
if attr is _sentinel:
293+
return False
292294

293-
# Verify that the annotations aren't merely inherited.
294295
for base_cls in cls.__mro__[1:]:
295-
if anns is getattr(base_cls, "__annotations__", None):
296-
return {}
296+
a = getattr(base_cls, attrib_name, None)
297+
if attr is a:
298+
return False
299+
300+
return True
301+
302+
303+
def _get_annotations(cls):
304+
"""
305+
Get annotations for *cls*.
306+
"""
307+
if _has_own_attribute(cls, "__annotations__"):
308+
return cls.__annotations__
297309

298-
return anns
310+
return {}
299311

300312

301313
def _counter_getter(e):
@@ -737,10 +749,10 @@ def _add_method_dunders(self, method):
737749
)
738750

739751

740-
def _determine_eq_order(cmp, eq, order):
752+
def _determine_eq_order(cmp, eq, order, default_eq):
741753
"""
742754
Validate the combination of *cmp*, *eq*, and *order*. Derive the effective
743-
values of eq and order.
755+
values of eq and order. If *eq* is None, set it to *default_eq*.
744756
"""
745757
if cmp is not None and any((eq is not None, order is not None)):
746758
raise ValueError("Don't mix `cmp` with `eq' and `order`.")
@@ -751,9 +763,10 @@ def _determine_eq_order(cmp, eq, order):
751763

752764
return cmp, cmp
753765

754-
# If left None, equality is on and ordering mirrors equality.
766+
# If left None, equality is set to the specified default and ordering
767+
# mirrors equality.
755768
if eq is None:
756-
eq = True
769+
eq = default_eq
757770

758771
if order is None:
759772
order = eq
@@ -764,14 +777,38 @@ def _determine_eq_order(cmp, eq, order):
764777
return eq, order
765778

766779

780+
def _determine_whether_to_implement(cls, flag, auto_detect, dunders):
781+
"""
782+
Check whether we should implement a set of methods for *cls*.
783+
784+
*flag* is the argument passed into @attr.s like 'init', *auto_detect* the
785+
same as passed into @attr.s and *dunders* is a tuple of attribute names
786+
whose presence signal that the user has implemented it themselves.
787+
788+
auto_detect must be False on Python 2.
789+
"""
790+
if flag is True or flag is None and auto_detect is False:
791+
return True
792+
793+
if flag is False:
794+
return False
795+
796+
# Logically, flag is None and auto_detect is True here.
797+
for dunder in dunders:
798+
if _has_own_attribute(cls, dunder):
799+
return False
800+
801+
return True
802+
803+
767804
def attrs(
768805
maybe_cls=None,
769806
these=None,
770807
repr_ns=None,
771-
repr=True,
808+
repr=None,
772809
cmp=None,
773810
hash=None,
774-
init=True,
811+
init=None,
775812
slots=False,
776813
frozen=False,
777814
weakref_slot=True,
@@ -782,6 +819,7 @@ def attrs(
782819
auto_exc=False,
783820
eq=None,
784821
order=None,
822+
auto_detect=False,
785823
):
786824
r"""
787825
A class decorator that adds `dunder
@@ -806,6 +844,22 @@ def attrs(
806844
:param str repr_ns: When using nested classes, there's no way in Python 2
807845
to automatically detect that. Therefore it's possible to set the
808846
namespace explicitly for a more meaningful ``repr`` output.
847+
:param bool auto_detect: Instead of setting the *init*, *repr*, *eq*,
848+
*order*, and *hash* arguments explicitly, assume they are set to
849+
``True`` **unless any** of the involved methods for one of the
850+
arguments is implemented in the *current* class (i.e. it is *not*
851+
inherited from some base class).
852+
853+
So for example by implementing ``__eq__`` on a class yourself,
854+
``attrs`` will deduce ``eq=False`` and won't create *neither*
855+
``__eq__`` *nor* ``__ne__``.
856+
857+
Passing ``True`` or ``False`` to *init*, *repr*, *eq*, *order*,
858+
*cmp*, or *hash* overrides whatever *auto_detect* would determine.
859+
860+
*auto_detect* requires Python 3. Setting it ``True`` on Python 2 raises
861+
a `PythonTooOldError`.
862+
809863
:param bool repr: Create a ``__repr__`` method with a human readable
810864
representation of ``attrs`` attributes..
811865
:param bool str: Create a ``__str__`` method that is identical to
@@ -874,8 +928,8 @@ def attrs(
874928
875929
:param bool weakref_slot: Make instances weak-referenceable. This has no
876930
effect unless ``slots`` is also enabled.
877-
:param bool auto_attribs: If True, collect `PEP 526`_-annotated attributes
878-
(Python 3.6 and later only) from the class body.
931+
:param bool auto_attribs: If ``True``, collect `PEP 526`_-annotated
932+
attributes (Python 3.6 and later only) from the class body.
879933
880934
In this case, you **must** annotate every field. If ``attrs``
881935
encounters a field that is set to an `attr.ib` but lacks a type
@@ -940,8 +994,15 @@ def attrs(
940994
.. versionadded:: 19.1.0 *auto_exc*
941995
.. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01.
942996
.. versionadded:: 19.2.0 *eq* and *order*
997+
.. versionadded:: 20.1.0 *auto_detect*
943998
"""
944-
eq, order = _determine_eq_order(cmp, eq, order)
999+
if auto_detect and PY2:
1000+
raise PythonTooOldError(
1001+
"auto_detect only works on Python 3 and later."
1002+
)
1003+
1004+
eq_, order_ = _determine_eq_order(cmp, eq, order, None)
1005+
hash_ = hash # workaround the lack of nonlocal
9451006

9461007
def wrap(cls):
9471008

@@ -961,16 +1022,31 @@ def wrap(cls):
9611022
cache_hash,
9621023
is_exc,
9631024
)
964-
965-
if repr is True:
1025+
if _determine_whether_to_implement(
1026+
cls, repr, auto_detect, ("__repr__",)
1027+
):
9661028
builder.add_repr(repr_ns)
9671029
if str is True:
9681030
builder.add_str()
969-
if eq is True and not is_exc:
1031+
1032+
eq = _determine_whether_to_implement(
1033+
cls, eq_, auto_detect, ("__eq__", "__ne__")
1034+
)
1035+
if not is_exc and eq is True:
9701036
builder.add_eq()
971-
if order is True and not is_exc:
1037+
if not is_exc and _determine_whether_to_implement(
1038+
cls, order_, auto_detect, ("__lt__", "__le__", "__gt__", "__ge__")
1039+
):
9721040
builder.add_order()
9731041

1042+
if (
1043+
hash_ is None
1044+
and auto_detect is True
1045+
and _has_own_attribute(cls, "__hash__")
1046+
):
1047+
hash = False
1048+
else:
1049+
hash = hash_
9741050
if hash is not True and hash is not False and hash is not None:
9751051
# Can't use `hash in` because 1 == True for example.
9761052
raise TypeError(
@@ -998,7 +1074,9 @@ def wrap(cls):
9981074
)
9991075
builder.make_unhashable()
10001076

1001-
if init is True:
1077+
if _determine_whether_to_implement(
1078+
cls, init, auto_detect, ("__init__",)
1079+
):
10021080
builder.add_init()
10031081
else:
10041082
if cache_hash:
@@ -1781,7 +1859,7 @@ def __init__(
17811859
eq=None,
17821860
order=None,
17831861
):
1784-
eq, order = _determine_eq_order(cmp, eq, order)
1862+
eq, order = _determine_eq_order(cmp, eq, order, True)
17851863

17861864
# Cache this descriptor here to speed things up later.
17871865
bound_setattr = _obj_setattr.__get__(self, Attribute)
@@ -2124,7 +2202,10 @@ def make_class(name, attrs, bases=(object,), **attributes_arguments):
21242202
attributes_arguments["eq"],
21252203
attributes_arguments["order"],
21262204
) = _determine_eq_order(
2127-
cmp, attributes_arguments.get("eq"), attributes_arguments.get("order")
2205+
cmp,
2206+
attributes_arguments.get("eq"),
2207+
attributes_arguments.get("order"),
2208+
True,
21282209
)
21292210

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