@@ -192,7 +192,7 @@ def attrib(
192
192
.. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01.
193
193
.. versionadded:: 19.2.0 *eq* and *order*
194
194
"""
195
- eq , order = _determine_eq_order (cmp , eq , order )
195
+ eq , order = _determine_eq_order (cmp , eq , order , True )
196
196
197
197
if hash is not None and hash is not True and hash is not False :
198
198
raise TypeError (
@@ -282,20 +282,32 @@ def _is_class_var(annot):
282
282
return str (annot ).startswith (_classvar_prefixes )
283
283
284
284
285
- def _get_annotations (cls ):
285
+ def _has_own_attribute (cls , attrib_name ):
286
286
"""
287
- Get annotations for *cls*.
287
+ Check whether *cls* defines *attrib_name* (and doesn't just inherit it).
288
+
289
+ Requires Python 3.
288
290
"""
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
292
294
293
- # Verify that the annotations aren't merely inherited.
294
295
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__
297
309
298
- return anns
310
+ return {}
299
311
300
312
301
313
def _counter_getter (e ):
@@ -737,10 +749,10 @@ def _add_method_dunders(self, method):
737
749
)
738
750
739
751
740
- def _determine_eq_order (cmp , eq , order ):
752
+ def _determine_eq_order (cmp , eq , order , default_eq ):
741
753
"""
742
754
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*.
744
756
"""
745
757
if cmp is not None and any ((eq is not None , order is not None )):
746
758
raise ValueError ("Don't mix `cmp` with `eq' and `order`." )
@@ -751,9 +763,10 @@ def _determine_eq_order(cmp, eq, order):
751
763
752
764
return cmp , cmp
753
765
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.
755
768
if eq is None :
756
- eq = True
769
+ eq = default_eq
757
770
758
771
if order is None :
759
772
order = eq
@@ -764,14 +777,38 @@ def _determine_eq_order(cmp, eq, order):
764
777
return eq , order
765
778
766
779
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
+
767
804
def attrs (
768
805
maybe_cls = None ,
769
806
these = None ,
770
807
repr_ns = None ,
771
- repr = True ,
808
+ repr = None ,
772
809
cmp = None ,
773
810
hash = None ,
774
- init = True ,
811
+ init = None ,
775
812
slots = False ,
776
813
frozen = False ,
777
814
weakref_slot = True ,
@@ -782,6 +819,7 @@ def attrs(
782
819
auto_exc = False ,
783
820
eq = None ,
784
821
order = None ,
822
+ auto_detect = False ,
785
823
):
786
824
r"""
787
825
A class decorator that adds `dunder
@@ -806,6 +844,22 @@ def attrs(
806
844
:param str repr_ns: When using nested classes, there's no way in Python 2
807
845
to automatically detect that. Therefore it's possible to set the
808
846
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
+
809
863
:param bool repr: Create a ``__repr__`` method with a human readable
810
864
representation of ``attrs`` attributes..
811
865
:param bool str: Create a ``__str__`` method that is identical to
@@ -874,8 +928,8 @@ def attrs(
874
928
875
929
:param bool weakref_slot: Make instances weak-referenceable. This has no
876
930
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.
879
933
880
934
In this case, you **must** annotate every field. If ``attrs``
881
935
encounters a field that is set to an `attr.ib` but lacks a type
@@ -940,8 +994,15 @@ def attrs(
940
994
.. versionadded:: 19.1.0 *auto_exc*
941
995
.. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01.
942
996
.. versionadded:: 19.2.0 *eq* and *order*
997
+ .. versionadded:: 20.1.0 *auto_detect*
943
998
"""
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
945
1006
946
1007
def wrap (cls ):
947
1008
@@ -961,16 +1022,31 @@ def wrap(cls):
961
1022
cache_hash ,
962
1023
is_exc ,
963
1024
)
964
-
965
- if repr is True :
1025
+ if _determine_whether_to_implement (
1026
+ cls , repr , auto_detect , ("__repr__" ,)
1027
+ ):
966
1028
builder .add_repr (repr_ns )
967
1029
if str is True :
968
1030
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 :
970
1036
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
+ ):
972
1040
builder .add_order ()
973
1041
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_
974
1050
if hash is not True and hash is not False and hash is not None :
975
1051
# Can't use `hash in` because 1 == True for example.
976
1052
raise TypeError (
@@ -998,7 +1074,9 @@ def wrap(cls):
998
1074
)
999
1075
builder .make_unhashable ()
1000
1076
1001
- if init is True :
1077
+ if _determine_whether_to_implement (
1078
+ cls , init , auto_detect , ("__init__" ,)
1079
+ ):
1002
1080
builder .add_init ()
1003
1081
else :
1004
1082
if cache_hash :
@@ -1781,7 +1859,7 @@ def __init__(
1781
1859
eq = None ,
1782
1860
order = None ,
1783
1861
):
1784
- eq , order = _determine_eq_order (cmp , eq , order )
1862
+ eq , order = _determine_eq_order (cmp , eq , order , True )
1785
1863
1786
1864
# Cache this descriptor here to speed things up later.
1787
1865
bound_setattr = _obj_setattr .__get__ (self , Attribute )
@@ -2124,7 +2202,10 @@ def make_class(name, attrs, bases=(object,), **attributes_arguments):
2124
2202
attributes_arguments ["eq" ],
2125
2203
attributes_arguments ["order" ],
2126
2204
) = _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 ,
2128
2209
)
2129
2210
2130
2211
return _attrs (these = cls_dict , ** attributes_arguments )(type_ )
0 commit comments