Skip to content

Commit 6a01a4b

Browse files
committed
Implement auto_detect
Fixes #324
1 parent d6a65fb commit 6a01a4b

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
@@ -217,7 +217,7 @@ def attrib(
217217
.. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01.
218218
.. versionadded:: 19.2.0 *eq* and *order*
219219
"""
220-
eq, order = _determine_eq_order(cmp, eq, order)
220+
eq, order = _determine_eq_order(cmp, eq, order, True)
221221

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

309309

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

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

323-
return anns
335+
return {}
324336

325337

326338
def _counter_getter(e):
@@ -741,10 +753,10 @@ def _add_method_dunders(self, method):
741753
)
742754

743755

744-
def _determine_eq_order(cmp, eq, order):
756+
def _determine_eq_order(cmp, eq, order, default_eq):
745757
"""
746758
Validate the combination of *cmp*, *eq*, and *order*. Derive the effective
747-
values of eq and order.
759+
values of eq and order. If *eq* is None, set it to *default_eq*.
748760
"""
749761
if cmp is not None and any((eq is not None, order is not None)):
750762
raise ValueError("Don't mix `cmp` with `eq' and `order`.")
@@ -755,9 +767,10 @@ def _determine_eq_order(cmp, eq, order):
755767

756768
return cmp, cmp
757769

758-
# If left None, equality is on and ordering mirrors equality.
770+
# If left None, equality is set to the specified default and ordering
771+
# mirrors equality.
759772
if eq is None:
760-
eq = True
773+
eq = default_eq
761774

762775
if order is None:
763776
order = eq
@@ -768,14 +781,38 @@ def _determine_eq_order(cmp, eq, order):
768781
return eq, order
769782

770783

784+
def _determine_whether_to_implement(cls, flag, auto_detect, dunders):
785+
"""
786+
Check whether we should implement a set of methods for *cls*.
787+
788+
*flag* is the argument passed into @attr.s like 'init', *auto_detect* the
789+
same as passed into @attr.s and *dunders* is a tuple of attribute names
790+
whose presence signal that the user has implemented it themselves.
791+
792+
auto_detect must be False on Python 2.
793+
"""
794+
if flag is True or flag is None and auto_detect is False:
795+
return True
796+
797+
if flag is False:
798+
return False
799+
800+
# Logically, flag is None and auto_detect is True here.
801+
for dunder in dunders:
802+
if _has_own_attribute(cls, dunder):
803+
return False
804+
805+
return True
806+
807+
771808
def attrs(
772809
maybe_cls=None,
773810
these=None,
774811
repr_ns=None,
775-
repr=True,
812+
repr=None,
776813
cmp=None,
777814
hash=None,
778-
init=True,
815+
init=None,
779816
slots=False,
780817
frozen=False,
781818
weakref_slot=True,
@@ -786,6 +823,7 @@ def attrs(
786823
auto_exc=False,
787824
eq=None,
788825
order=None,
826+
auto_detect=False,
789827
):
790828
r"""
791829
A class decorator that adds `dunder
@@ -810,6 +848,22 @@ def attrs(
810848
:param str repr_ns: When using nested classes, there's no way in Python 2
811849
to automatically detect that. Therefore it's possible to set the
812850
namespace explicitly for a more meaningful ``repr`` output.
851+
:param bool auto_detect: Instead of setting the *init*, *repr*, *eq*,
852+
*order*, and *hash* arguments explicitly, assume they are set to
853+
``True`` **unless any** of the involved methods for one of the
854+
arguments is implemented in the *current* class (i.e. it is *not*
855+
inherited from some base class).
856+
857+
So for example by implementing ``__eq__`` on a class yourself,
858+
``attrs`` will deduce ``eq=False`` and won't create *neither*
859+
``__eq__`` *nor* ``__ne__``.
860+
861+
Passing ``True`` or ``False`` to *init*, *repr*, *eq*, *order*,
862+
*cmp*, or *hash* overrides whatever *auto_detect* would determine.
863+
864+
*auto_detect* requires Python 3. Setting it ``True`` on Python 2 raises
865+
a `PythonTooOldError`.
866+
813867
:param bool repr: Create a ``__repr__`` method with a human readable
814868
representation of ``attrs`` attributes..
815869
:param bool str: Create a ``__str__`` method that is identical to
@@ -878,8 +932,8 @@ def attrs(
878932
879933
:param bool weakref_slot: Make instances weak-referenceable. This has no
880934
effect unless ``slots`` is also enabled.
881-
:param bool auto_attribs: If True, collect `PEP 526`_-annotated attributes
882-
(Python 3.6 and later only) from the class body.
935+
:param bool auto_attribs: If ``True``, collect `PEP 526`_-annotated
936+
attributes (Python 3.6 and later only) from the class body.
883937
884938
In this case, you **must** annotate every field. If ``attrs``
885939
encounters a field that is set to an `attr.ib` but lacks a type
@@ -944,8 +998,15 @@ def attrs(
944998
.. versionadded:: 19.1.0 *auto_exc*
945999
.. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01.
9461000
.. versionadded:: 19.2.0 *eq* and *order*
1001+
.. versionadded:: 20.1.0 *auto_detect*
9471002
"""
948-
eq, order = _determine_eq_order(cmp, eq, order)
1003+
if auto_detect and PY2:
1004+
raise PythonTooOldError(
1005+
"auto_detect only works on Python 3 and later."
1006+
)
1007+
1008+
eq_, order_ = _determine_eq_order(cmp, eq, order, None)
1009+
hash_ = hash # workaround the lack of nonlocal
9491010

9501011
def wrap(cls):
9511012

@@ -965,16 +1026,31 @@ def wrap(cls):
9651026
cache_hash,
9661027
is_exc,
9671028
)
968-
969-
if repr is True:
1029+
if _determine_whether_to_implement(
1030+
cls, repr, auto_detect, ("__repr__",)
1031+
):
9701032
builder.add_repr(repr_ns)
9711033
if str is True:
9721034
builder.add_str()
973-
if eq is True and not is_exc:
1035+
1036+
eq = _determine_whether_to_implement(
1037+
cls, eq_, auto_detect, ("__eq__", "__ne__")
1038+
)
1039+
if not is_exc and eq is True:
9741040
builder.add_eq()
975-
if order is True and not is_exc:
1041+
if not is_exc and _determine_whether_to_implement(
1042+
cls, order_, auto_detect, ("__lt__", "__le__", "__gt__", "__ge__")
1043+
):
9761044
builder.add_order()
9771045

1046+
if (
1047+
hash_ is None
1048+
and auto_detect is True
1049+
and _has_own_attribute(cls, "__hash__")
1050+
):
1051+
hash = False
1052+
else:
1053+
hash = hash_
9781054
if hash is not True and hash is not False and hash is not None:
9791055
# Can't use `hash in` because 1 == True for example.
9801056
raise TypeError(
@@ -1002,7 +1078,9 @@ def wrap(cls):
10021078
)
10031079
builder.make_unhashable()
10041080

1005-
if init is True:
1081+
if _determine_whether_to_implement(
1082+
cls, init, auto_detect, ("__init__",)
1083+
):
10061084
builder.add_init()
10071085
else:
10081086
if cache_hash:
@@ -1805,7 +1883,7 @@ def __init__(
18051883
eq=None,
18061884
order=None,
18071885
):
1808-
eq, order = _determine_eq_order(cmp, eq, order)
1886+
eq, order = _determine_eq_order(cmp, eq, order, True)
18091887

18101888
# Cache this descriptor here to speed things up later.
18111889
bound_setattr = _obj_setattr.__get__(self, Attribute)
@@ -2148,7 +2226,10 @@ def make_class(name, attrs, bases=(object,), **attributes_arguments):
21482226
attributes_arguments["eq"],
21492227
attributes_arguments["order"],
21502228
) = _determine_eq_order(
2151-
cmp, attributes_arguments.get("eq"), attributes_arguments.get("order")
2229+
cmp,
2230+
attributes_arguments.get("eq"),
2231+
attributes_arguments.get("order"),
2232+
True,
21522233
)
21532234

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