@@ -218,7 +218,7 @@ def attrib(
218
218
.. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01.
219
219
.. versionadded:: 19.2.0 *eq* and *order*
220
220
"""
221
- eq , order = _determine_eq_order (cmp , eq , order )
221
+ eq , order = _determine_eq_order (cmp , eq , order , True )
222
222
223
223
if hash is not None and hash is not True and hash is not False :
224
224
raise TypeError (
@@ -308,20 +308,32 @@ def _is_class_var(annot):
308
308
return str (annot ).startswith (_classvar_prefixes )
309
309
310
310
311
- def _get_annotations (cls ):
311
+ def _has_own_attribute (cls , attrib_name ):
312
312
"""
313
- Get annotations for *cls*.
313
+ Check whether *cls* defines *attrib_name* (and doesn't just inherit it).
314
+
315
+ Requires Python 3.
314
316
"""
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
318
320
319
- # Verify that the annotations aren't merely inherited.
320
321
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__
323
335
324
- return anns
336
+ return {}
325
337
326
338
327
339
def _counter_getter (e ):
@@ -749,10 +761,10 @@ def _add_method_dunders(self, method):
749
761
)
750
762
751
763
752
- def _determine_eq_order (cmp , eq , order ):
764
+ def _determine_eq_order (cmp , eq , order , default_eq ):
753
765
"""
754
766
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*.
756
768
"""
757
769
if cmp is not None and any ((eq is not None , order is not None )):
758
770
raise ValueError ("Don't mix `cmp` with `eq' and `order`." )
@@ -763,9 +775,10 @@ def _determine_eq_order(cmp, eq, order):
763
775
764
776
return cmp , cmp
765
777
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.
767
780
if eq is None :
768
- eq = True
781
+ eq = default_eq
769
782
770
783
if order is None :
771
784
order = eq
@@ -776,14 +789,38 @@ def _determine_eq_order(cmp, eq, order):
776
789
return eq , order
777
790
778
791
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
+
779
816
def attrs (
780
817
maybe_cls = None ,
781
818
these = None ,
782
819
repr_ns = None ,
783
- repr = True ,
820
+ repr = None ,
784
821
cmp = None ,
785
822
hash = None ,
786
- init = True ,
823
+ init = None ,
787
824
slots = False ,
788
825
frozen = False ,
789
826
weakref_slot = True ,
@@ -794,6 +831,7 @@ def attrs(
794
831
auto_exc = False ,
795
832
eq = None ,
796
833
order = None ,
834
+ auto_detect = False ,
797
835
):
798
836
r"""
799
837
A class decorator that adds `dunder
@@ -818,6 +856,22 @@ def attrs(
818
856
:param str repr_ns: When using nested classes, there's no way in Python 2
819
857
to automatically detect that. Therefore it's possible to set the
820
858
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
+
821
875
:param bool repr: Create a ``__repr__`` method with a human readable
822
876
representation of ``attrs`` attributes..
823
877
:param bool str: Create a ``__str__`` method that is identical to
@@ -886,8 +940,8 @@ def attrs(
886
940
887
941
:param bool weakref_slot: Make instances weak-referenceable. This has no
888
942
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.
891
945
892
946
In this case, you **must** annotate every field. If ``attrs``
893
947
encounters a field that is set to an `attr.ib` but lacks a type
@@ -952,8 +1006,15 @@ def attrs(
952
1006
.. versionadded:: 19.1.0 *auto_exc*
953
1007
.. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01.
954
1008
.. versionadded:: 19.2.0 *eq* and *order*
1009
+ .. versionadded:: 20.1.0 *auto_detect*
955
1010
"""
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
957
1018
958
1019
def wrap (cls ):
959
1020
@@ -973,16 +1034,31 @@ def wrap(cls):
973
1034
cache_hash ,
974
1035
is_exc ,
975
1036
)
976
-
977
- if repr is True :
1037
+ if _determine_whether_to_implement (
1038
+ cls , repr , auto_detect , ("__repr__" ,)
1039
+ ):
978
1040
builder .add_repr (repr_ns )
979
1041
if str is True :
980
1042
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 :
982
1048
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
+ ):
984
1052
builder .add_order ()
985
1053
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_
986
1062
if hash is not True and hash is not False and hash is not None :
987
1063
# Can't use `hash in` because 1 == True for example.
988
1064
raise TypeError (
@@ -1010,7 +1086,9 @@ def wrap(cls):
1010
1086
)
1011
1087
builder .make_unhashable ()
1012
1088
1013
- if init is True :
1089
+ if _determine_whether_to_implement (
1090
+ cls , init , auto_detect , ("__init__" ,)
1091
+ ):
1014
1092
builder .add_init ()
1015
1093
else :
1016
1094
if cache_hash :
@@ -1813,7 +1891,7 @@ def __init__(
1813
1891
eq = None ,
1814
1892
order = None ,
1815
1893
):
1816
- eq , order = _determine_eq_order (cmp , eq , order )
1894
+ eq , order = _determine_eq_order (cmp , eq , order , True )
1817
1895
1818
1896
# Cache this descriptor here to speed things up later.
1819
1897
bound_setattr = _obj_setattr .__get__ (self , Attribute )
@@ -2159,7 +2237,10 @@ def make_class(name, attrs, bases=(object,), **attributes_arguments):
2159
2237
attributes_arguments ["eq" ],
2160
2238
attributes_arguments ["order" ],
2161
2239
) = _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 ,
2163
2244
)
2164
2245
2165
2246
return _attrs (these = cls_dict , ** attributes_arguments )(type_ )
0 commit comments