@@ -897,6 +897,26 @@ def add_init(self):
897
897
self ._is_exc ,
898
898
self ._on_setattr is not None
899
899
and self ._on_setattr is not setters .NO_OP ,
900
+ attrs_init = False ,
901
+ )
902
+ )
903
+
904
+ return self
905
+
906
+ def add_attrs_init (self ):
907
+ self ._cls_dict ["__attrs_init__" ] = self ._add_method_dunders (
908
+ _make_init (
909
+ self ._cls ,
910
+ self ._attrs ,
911
+ self ._has_post_init ,
912
+ self ._frozen ,
913
+ self ._slots ,
914
+ self ._cache_hash ,
915
+ self ._base_attr_map ,
916
+ self ._is_exc ,
917
+ self ._on_setattr is not None
918
+ and self ._on_setattr is not setters .NO_OP ,
919
+ attrs_init = True ,
900
920
)
901
921
)
902
922
@@ -1160,6 +1180,11 @@ def attrs(
1160
1180
``attrs`` attributes. Leading underscores are stripped for the
1161
1181
argument name. If a ``__attrs_post_init__`` method exists on the
1162
1182
class, it will be called after the class is fully initialized.
1183
+
1184
+ If ``init`` is ``False``, an ``__attrs_init__`` method will be
1185
+ injected instead. This allows you to define a custom ``__init__``
1186
+ method that can do pre-init work such as ``super().__init__()``,
1187
+ and then call ``__attrs_init__()`` and ``__attrs_post_init__()``.
1163
1188
:param bool slots: Create a `slotted class <slotted classes>` that's more
1164
1189
memory-efficient. Slotted classes are generally superior to the default
1165
1190
dict classes, but have some gotchas you should know about, so we
@@ -1299,6 +1324,8 @@ def attrs(
1299
1324
.. versionadded:: 20.1.0 *getstate_setstate*
1300
1325
.. versionadded:: 20.1.0 *on_setattr*
1301
1326
.. versionadded:: 20.3.0 *field_transformer*
1327
+ .. versionchanged:: 21.1.0
1328
+ ``init=False`` injects ``__attrs_init__``
1302
1329
"""
1303
1330
if auto_detect and PY2 :
1304
1331
raise PythonTooOldError (
@@ -1408,6 +1435,7 @@ def wrap(cls):
1408
1435
):
1409
1436
builder .add_init ()
1410
1437
else :
1438
+ builder .add_attrs_init ()
1411
1439
if cache_hash :
1412
1440
raise TypeError (
1413
1441
"Invalid value for cache_hash. To use hash caching,"
@@ -1872,6 +1900,7 @@ def _make_init(
1872
1900
base_attr_map ,
1873
1901
is_exc ,
1874
1902
has_global_on_setattr ,
1903
+ attrs_init ,
1875
1904
):
1876
1905
if frozen and has_global_on_setattr :
1877
1906
raise ValueError ("Frozen classes can't use on_setattr." )
@@ -1908,6 +1937,7 @@ def _make_init(
1908
1937
is_exc ,
1909
1938
needs_cached_setattr ,
1910
1939
has_global_on_setattr ,
1940
+ attrs_init ,
1911
1941
)
1912
1942
locs = {}
1913
1943
bytecode = compile (script , unique_filename , "exec" )
@@ -1929,10 +1959,10 @@ def _make_init(
1929
1959
unique_filename ,
1930
1960
)
1931
1961
1932
- __init__ = locs ["__init__" ]
1933
- __init__ .__annotations__ = annotations
1962
+ init = locs [ "__attrs_init__" ] if attrs_init else locs ["__init__" ]
1963
+ init .__annotations__ = annotations
1934
1964
1935
- return __init__
1965
+ return init
1936
1966
1937
1967
1938
1968
def _setattr (attr_name , value_var , has_on_setattr ):
@@ -2047,6 +2077,7 @@ def _attrs_to_init_script(
2047
2077
is_exc ,
2048
2078
needs_cached_setattr ,
2049
2079
has_global_on_setattr ,
2080
+ attrs_init ,
2050
2081
):
2051
2082
"""
2052
2083
Return a script of an initializer for *attrs* and a dict of globals.
@@ -2317,10 +2348,12 @@ def fmt_setter_with_converter(
2317
2348
)
2318
2349
return (
2319
2350
"""\
2320
- def __init__ (self, {args}):
2351
+ def {init_name} (self, {args}):
2321
2352
{lines}
2322
2353
""" .format (
2323
- args = args , lines = "\n " .join (lines ) if lines else "pass"
2354
+ init_name = ("__attrs_init__" if attrs_init else "__init__" ),
2355
+ args = args ,
2356
+ lines = "\n " .join (lines ) if lines else "pass" ,
2324
2357
),
2325
2358
names_for_globals ,
2326
2359
annotations ,
@@ -2666,7 +2699,6 @@ def default(self, meth):
2666
2699
_CountingAttr = _add_eq (_add_repr (_CountingAttr ))
2667
2700
2668
2701
2669
- @attrs (slots = True , init = False , hash = True )
2670
2702
class Factory (object ):
2671
2703
"""
2672
2704
Stores a factory callable.
@@ -2682,8 +2714,7 @@ class Factory(object):
2682
2714
.. versionadded:: 17.1.0 *takes_self*
2683
2715
"""
2684
2716
2685
- factory = attrib ()
2686
- takes_self = attrib ()
2717
+ __slots__ = ("factory" , "takes_self" )
2687
2718
2688
2719
def __init__ (self , factory , takes_self = False ):
2689
2720
"""
@@ -2693,6 +2724,38 @@ def __init__(self, factory, takes_self=False):
2693
2724
self .factory = factory
2694
2725
self .takes_self = takes_self
2695
2726
2727
+ def __getstate__ (self ):
2728
+ """
2729
+ Play nice with pickle.
2730
+ """
2731
+ return tuple (getattr (self , name ) for name in self .__slots__ )
2732
+
2733
+ def __setstate__ (self , state ):
2734
+ """
2735
+ Play nice with pickle.
2736
+ """
2737
+ for name , value in zip (self .__slots__ , state ):
2738
+ setattr (self , name , value )
2739
+
2740
+
2741
+ _f = [
2742
+ Attribute (
2743
+ name = name ,
2744
+ default = NOTHING ,
2745
+ validator = None ,
2746
+ repr = True ,
2747
+ cmp = None ,
2748
+ eq = True ,
2749
+ order = False ,
2750
+ hash = True ,
2751
+ init = True ,
2752
+ inherited = False ,
2753
+ )
2754
+ for name in Factory .__slots__
2755
+ ]
2756
+
2757
+ Factory = _add_hash (_add_eq (_add_repr (Factory , attrs = _f ), attrs = _f ), attrs = _f )
2758
+
2696
2759
2697
2760
def make_class (name , attrs , bases = (object ,), ** attributes_arguments ):
2698
2761
"""
@@ -2727,11 +2790,15 @@ def make_class(name, attrs, bases=(object,), **attributes_arguments):
2727
2790
raise TypeError ("attrs argument must be a dict or a list." )
2728
2791
2729
2792
post_init = cls_dict .pop ("__attrs_post_init__" , None )
2730
- type_ = type (
2731
- name ,
2732
- bases ,
2733
- {} if post_init is None else {"__attrs_post_init__" : post_init },
2734
- )
2793
+ user_init = cls_dict .pop ("__init__" , None )
2794
+
2795
+ body = {}
2796
+ if post_init is not None :
2797
+ body ["__attrs_post_init__" ] = post_init
2798
+ if user_init is not None :
2799
+ body ["__init__" ] = user_init
2800
+
2801
+ type_ = type (name , bases , body )
2735
2802
# For pickling to work, the __module__ variable needs to be set to the
2736
2803
# frame where the class is created. Bypass this step in environments where
2737
2804
# sys._getframe is not defined (Jython for example) or sys._getframe is not
0 commit comments