From 9030d1ee5506eb3cc7ee23ce7d0029b1ebdf3810 Mon Sep 17 00:00:00 2001 From: Venky Iyer Date: Sat, 12 Dec 2020 16:18:18 -0800 Subject: [PATCH 01/11] wip version of __attrs_init_(). tests pass for py38 --- src/attr/_make.py | 100 ++++++++++++++++++++++++++++-------------- tests/test_dunders.py | 3 +- tests/test_make.py | 2 +- 3 files changed, 70 insertions(+), 35 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 49484f935..e427fb7b1 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -46,6 +46,10 @@ # Unique object for unequivocal getattr() defaults. _sentinel = object() +# Need a forward definition of Factory because _make_init() refers to +# it, but since Factory is an attrs class, it calls _make_init() +Factory = None + class _Nothing(object): """ @@ -848,22 +852,38 @@ def add_hash(self): return self - def add_init(self): - self._cls_dict["__init__"] = self._add_method_dunders( - _make_init( - self._cls, - self._attrs, - self._has_post_init, - self._frozen, - self._slots, - self._cache_hash, - self._base_attr_map, - self._is_exc, + def add_init(self, init, auto_detect): + should_implement_init = _determine_whether_to_implement( + self._cls, init, auto_detect, ("__init__",) + ) + attrs_init_method, init_method = _make_init( + self._cls, + self._attrs, + self._has_post_init, + self._frozen, + self._slots, + self._cache_hash, + self._base_attr_map, + self._is_exc, + ( self._on_setattr is not None - and self._on_setattr is not setters.NO_OP, - ) + and self._on_setattr is not setters.NO_OP + ), + should_implement_init, ) + self._cls_dict["__attrs_init__"] = self._add_method_dunders( + attrs_init_method + ) + if init_method is not None: + self._cls_dict["__init__"] = self._add_method_dunders(init_method) + else: + if self._cache_hash: + raise TypeError( + "Invalid value for cache_hash. To use hash caching," + " init must be True." + ) + return self def add_eq(self): @@ -1367,17 +1387,7 @@ def wrap(cls): ) builder.make_unhashable() - if _determine_whether_to_implement( - cls, init, auto_detect, ("__init__",) - ): - builder.add_init() - else: - if cache_hash: - raise TypeError( - "Invalid value for cache_hash. To use hash caching," - " init must be True." - ) - + builder.add_init(init, auto_detect) return builder.build_class() # maybe_cls's type depends on the usage of the decorator. It's a class @@ -1836,6 +1846,7 @@ def _make_init( base_attr_map, is_exc, has_global_on_setattr, + should_implement_init, ): if frozen and has_global_on_setattr: raise ValueError("Frozen classes can't use on_setattr.") @@ -1872,6 +1883,7 @@ def _make_init( is_exc, needs_cached_setattr, has_global_on_setattr, + should_implement_init, ) locs = {} bytecode = compile(script, unique_filename, "exec") @@ -1893,10 +1905,16 @@ def _make_init( unique_filename, ) - __init__ = locs["__init__"] - __init__.__annotations__ = annotations + __attrs_init__ = locs["__attrs_init__"] + __attrs_init__.__annotations__ = annotations + + if "__init__" in locs: + __init__ = locs["__init__"] + __init__.__annotations__ = annotations + else: + __init__ = None - return __init__ + return __attrs_init__, __init__ def _setattr(attr_name, value_var, has_on_setattr): @@ -2011,6 +2029,7 @@ def _attrs_to_init_script( is_exc, needs_cached_setattr, has_global_on_setattr, + should_implement_init, ): """ Return a script of an initializer for *attrs* and a dict of globals. @@ -2084,7 +2103,7 @@ def fmt_setter_with_converter( ) arg_name = a.name.lstrip("_") - has_factory = isinstance(a.default, Factory) + has_factory = Factory is not None and isinstance(a.default, Factory) if has_factory and a.default.takes_self: maybe_self = "self" else: @@ -2225,9 +2244,6 @@ def fmt_setter_with_converter( names_for_globals[val_name] = a.validator names_for_globals[attr_name] = a - if post_init: - lines.append("self.__attrs_post_init__()") - # because this is set only after __attrs_post_init is called, a crash # will result if post-init tries to access the hash code. This seemed # preferable to setting this beforehand, in which case alteration to @@ -2263,12 +2279,30 @@ def fmt_setter_with_converter( ", " if args else "", # leading comma ", ".join(kw_only_args), # kw_only args ) + + init_lines = "" + if should_implement_init: + init_lines = """\ + +def __init__(self, {args}): + args = locals() + del args['self'] + self.__attrs_init__(**args) + {post_init_call} +""".format( + args=args, + post_init_call="self.__attrs_post_init__()" if post_init else "", + ) + return ( """\ -def __init__(self, {args}): +def __attrs_init__(self, {args}): {lines} +{init_lines} """.format( - args=args, lines="\n ".join(lines) if lines else "pass" + args=args, + lines="\n ".join(lines) if lines else "pass", + init_lines=init_lines, ), names_for_globals, annotations, diff --git a/tests/test_dunders.py b/tests/test_dunders.py index 2f1ebabdd..64b5e154f 100644 --- a/tests/test_dunders.py +++ b/tests/test_dunders.py @@ -59,7 +59,7 @@ def _add_init(cls, frozen): This function used to be part of _make. It wasn't used anymore however the tests for it are still useful to test the behavior of _make_init. """ - cls.__init__ = _make_init( + cls.__attrs_init__, cls.__init__ = _make_init( cls, cls.__attrs_attrs__, getattr(cls, "__attrs_post_init__", False), @@ -69,6 +69,7 @@ def _add_init(cls, frozen): base_attr_map={}, is_exc=False, has_global_on_setattr=False, + should_implement_init=True, ) return cls diff --git a/tests/test_make.py b/tests/test_make.py index fad4ec7e2..97d4be201 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -1526,7 +1526,7 @@ class C(object): b.add_eq() .add_order() .add_hash() - .add_init() + .add_init(True, True) .add_repr("ns") .add_str() .build_class() From 203c027796d24e7411a9dbd8d03cb371e6f0378d Mon Sep 17 00:00:00 2001 From: Venky Iyer Date: Sun, 13 Dec 2020 16:47:14 -0800 Subject: [PATCH 02/11] Revert "wip version of __attrs_init_(). tests pass for py38" This reverts commit 9030d1ee5506eb3cc7ee23ce7d0029b1ebdf3810. --- src/attr/_make.py | 100 ++++++++++++++---------------------------- tests/test_dunders.py | 3 +- tests/test_make.py | 2 +- 3 files changed, 35 insertions(+), 70 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index e427fb7b1..49484f935 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -46,10 +46,6 @@ # Unique object for unequivocal getattr() defaults. _sentinel = object() -# Need a forward definition of Factory because _make_init() refers to -# it, but since Factory is an attrs class, it calls _make_init() -Factory = None - class _Nothing(object): """ @@ -852,38 +848,22 @@ def add_hash(self): return self - def add_init(self, init, auto_detect): - should_implement_init = _determine_whether_to_implement( - self._cls, init, auto_detect, ("__init__",) - ) - attrs_init_method, init_method = _make_init( - self._cls, - self._attrs, - self._has_post_init, - self._frozen, - self._slots, - self._cache_hash, - self._base_attr_map, - self._is_exc, - ( + def add_init(self): + self._cls_dict["__init__"] = self._add_method_dunders( + _make_init( + self._cls, + self._attrs, + self._has_post_init, + self._frozen, + self._slots, + self._cache_hash, + self._base_attr_map, + self._is_exc, self._on_setattr is not None - and self._on_setattr is not setters.NO_OP - ), - should_implement_init, + and self._on_setattr is not setters.NO_OP, + ) ) - self._cls_dict["__attrs_init__"] = self._add_method_dunders( - attrs_init_method - ) - if init_method is not None: - self._cls_dict["__init__"] = self._add_method_dunders(init_method) - else: - if self._cache_hash: - raise TypeError( - "Invalid value for cache_hash. To use hash caching," - " init must be True." - ) - return self def add_eq(self): @@ -1387,7 +1367,17 @@ def wrap(cls): ) builder.make_unhashable() - builder.add_init(init, auto_detect) + if _determine_whether_to_implement( + cls, init, auto_detect, ("__init__",) + ): + builder.add_init() + else: + if cache_hash: + raise TypeError( + "Invalid value for cache_hash. To use hash caching," + " init must be True." + ) + return builder.build_class() # maybe_cls's type depends on the usage of the decorator. It's a class @@ -1846,7 +1836,6 @@ def _make_init( base_attr_map, is_exc, has_global_on_setattr, - should_implement_init, ): if frozen and has_global_on_setattr: raise ValueError("Frozen classes can't use on_setattr.") @@ -1883,7 +1872,6 @@ def _make_init( is_exc, needs_cached_setattr, has_global_on_setattr, - should_implement_init, ) locs = {} bytecode = compile(script, unique_filename, "exec") @@ -1905,16 +1893,10 @@ def _make_init( unique_filename, ) - __attrs_init__ = locs["__attrs_init__"] - __attrs_init__.__annotations__ = annotations - - if "__init__" in locs: - __init__ = locs["__init__"] - __init__.__annotations__ = annotations - else: - __init__ = None + __init__ = locs["__init__"] + __init__.__annotations__ = annotations - return __attrs_init__, __init__ + return __init__ def _setattr(attr_name, value_var, has_on_setattr): @@ -2029,7 +2011,6 @@ def _attrs_to_init_script( is_exc, needs_cached_setattr, has_global_on_setattr, - should_implement_init, ): """ Return a script of an initializer for *attrs* and a dict of globals. @@ -2103,7 +2084,7 @@ def fmt_setter_with_converter( ) arg_name = a.name.lstrip("_") - has_factory = Factory is not None and isinstance(a.default, Factory) + has_factory = isinstance(a.default, Factory) if has_factory and a.default.takes_self: maybe_self = "self" else: @@ -2244,6 +2225,9 @@ def fmt_setter_with_converter( names_for_globals[val_name] = a.validator names_for_globals[attr_name] = a + if post_init: + lines.append("self.__attrs_post_init__()") + # because this is set only after __attrs_post_init is called, a crash # will result if post-init tries to access the hash code. This seemed # preferable to setting this beforehand, in which case alteration to @@ -2279,30 +2263,12 @@ def fmt_setter_with_converter( ", " if args else "", # leading comma ", ".join(kw_only_args), # kw_only args ) - - init_lines = "" - if should_implement_init: - init_lines = """\ - -def __init__(self, {args}): - args = locals() - del args['self'] - self.__attrs_init__(**args) - {post_init_call} -""".format( - args=args, - post_init_call="self.__attrs_post_init__()" if post_init else "", - ) - return ( """\ -def __attrs_init__(self, {args}): +def __init__(self, {args}): {lines} -{init_lines} """.format( - args=args, - lines="\n ".join(lines) if lines else "pass", - init_lines=init_lines, + args=args, lines="\n ".join(lines) if lines else "pass" ), names_for_globals, annotations, diff --git a/tests/test_dunders.py b/tests/test_dunders.py index 64b5e154f..2f1ebabdd 100644 --- a/tests/test_dunders.py +++ b/tests/test_dunders.py @@ -59,7 +59,7 @@ def _add_init(cls, frozen): This function used to be part of _make. It wasn't used anymore however the tests for it are still useful to test the behavior of _make_init. """ - cls.__attrs_init__, cls.__init__ = _make_init( + cls.__init__ = _make_init( cls, cls.__attrs_attrs__, getattr(cls, "__attrs_post_init__", False), @@ -69,7 +69,6 @@ def _add_init(cls, frozen): base_attr_map={}, is_exc=False, has_global_on_setattr=False, - should_implement_init=True, ) return cls diff --git a/tests/test_make.py b/tests/test_make.py index 97d4be201..fad4ec7e2 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -1526,7 +1526,7 @@ class C(object): b.add_eq() .add_order() .add_hash() - .add_init(True, True) + .add_init() .add_repr("ns") .add_str() .build_class() From 712e9871b3b763cefa58a1e91653bc5cc93cc685 Mon Sep 17 00:00:00 2001 From: Venky Iyer Date: Sun, 13 Dec 2020 16:51:26 -0800 Subject: [PATCH 03/11] inject __attrs_init__ when no __init__ --- src/attr/_make.py | 42 +++++++++++++++++++++++++++++++++++------- tests/test_dunders.py | 1 + 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 49484f935..8b530ab00 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -46,6 +46,8 @@ # Unique object for unequivocal getattr() defaults. _sentinel = object() +Factory = None + class _Nothing(object): """ @@ -861,6 +863,26 @@ def add_init(self): self._is_exc, self._on_setattr is not None and self._on_setattr is not setters.NO_OP, + attrs_init=False, + ) + ) + + return self + + def add_attrs_init(self): + self._cls_dict["__attrs_init__"] = self._add_method_dunders( + _make_init( + self._cls, + self._attrs, + self._has_post_init, + self._frozen, + self._slots, + self._cache_hash, + self._base_attr_map, + self._is_exc, + self._on_setattr is not None + and self._on_setattr is not setters.NO_OP, + attrs_init=True, ) ) @@ -1372,6 +1394,7 @@ def wrap(cls): ): builder.add_init() else: + builder.add_attrs_init() if cache_hash: raise TypeError( "Invalid value for cache_hash. To use hash caching," @@ -1836,6 +1859,7 @@ def _make_init( base_attr_map, is_exc, has_global_on_setattr, + attrs_init, ): if frozen and has_global_on_setattr: raise ValueError("Frozen classes can't use on_setattr.") @@ -1872,6 +1896,7 @@ def _make_init( is_exc, needs_cached_setattr, has_global_on_setattr, + attrs_init, ) locs = {} bytecode = compile(script, unique_filename, "exec") @@ -1893,10 +1918,10 @@ def _make_init( unique_filename, ) - __init__ = locs["__init__"] - __init__.__annotations__ = annotations + init = locs["__attrs_init__"] if attrs_init else locs["__init__"] + init.__annotations__ = annotations - return __init__ + return init def _setattr(attr_name, value_var, has_on_setattr): @@ -2011,6 +2036,7 @@ def _attrs_to_init_script( is_exc, needs_cached_setattr, has_global_on_setattr, + attrs_init, ): """ Return a script of an initializer for *attrs* and a dict of globals. @@ -2084,7 +2110,7 @@ def fmt_setter_with_converter( ) arg_name = a.name.lstrip("_") - has_factory = isinstance(a.default, Factory) + has_factory = Factory is not None and isinstance(a.default, Factory) if has_factory and a.default.takes_self: maybe_self = "self" else: @@ -2225,7 +2251,7 @@ def fmt_setter_with_converter( names_for_globals[val_name] = a.validator names_for_globals[attr_name] = a - if post_init: + if post_init and not attrs_init: lines.append("self.__attrs_post_init__()") # because this is set only after __attrs_post_init is called, a crash @@ -2265,10 +2291,12 @@ def fmt_setter_with_converter( ) return ( """\ -def __init__(self, {args}): +def {init_name}(self, {args}): {lines} """.format( - args=args, lines="\n ".join(lines) if lines else "pass" + init_name=("__attrs_init__" if attrs_init else "__init__"), + args=args, + lines="\n ".join(lines) if lines else "pass", ), names_for_globals, annotations, diff --git a/tests/test_dunders.py b/tests/test_dunders.py index 2f1ebabdd..16bafaff7 100644 --- a/tests/test_dunders.py +++ b/tests/test_dunders.py @@ -69,6 +69,7 @@ def _add_init(cls, frozen): base_attr_map={}, is_exc=False, has_global_on_setattr=False, + attrs_init=False, ) return cls From 7a6557db9e576febc550f275519e593ecbf71fd6 Mon Sep 17 00:00:00 2001 From: Venky Iyer Date: Sun, 13 Dec 2020 18:02:23 -0800 Subject: [PATCH 04/11] Add hypothesis strategy for __attrs_init__ --- src/attr/_make.py | 14 +++++++++----- tests/strategies.py | 12 ++++++++++++ tests/test_funcs.py | 9 ++++++++- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index 8b530ab00..6725b3d89 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -2703,11 +2703,15 @@ def make_class(name, attrs, bases=(object,), **attributes_arguments): raise TypeError("attrs argument must be a dict or a list.") post_init = cls_dict.pop("__attrs_post_init__", None) - type_ = type( - name, - bases, - {} if post_init is None else {"__attrs_post_init__": post_init}, - ) + user_init = cls_dict.pop("__init__", None) + + body = {} + if post_init is not None: + body["__attrs_post_init__"] = post_init + if user_init is not None: + body["__init__"] = user_init + + type_ = type(name, bases, body) # For pickling to work, the __module__ variable needs to be set to the # frame where the class is created. Bypass this step in environments where # sys._getframe is not defined (Jython for example) or sys._getframe is not diff --git a/tests/strategies.py b/tests/strategies.py index a9bc26408..271299bf8 100644 --- a/tests/strategies.py +++ b/tests/strategies.py @@ -154,6 +154,8 @@ class HypClass: cls_dict = dict(zip(attr_names, attrs)) post_init_flag = draw(st.booleans()) + init_flag = draw(st.booleans()) + if post_init_flag: def post_init(self): @@ -161,12 +163,22 @@ def post_init(self): cls_dict["__attrs_post_init__"] = post_init + if not init_flag: + + def init(self, *args, **kwargs): + self.__attrs_init__(*args, **kwargs) + if post_init_flag: + self.__attrs_post_init__() + + cls_dict["__init__"] = init + return make_class( "HypClass", cls_dict, slots=slots_flag if slots is None else slots, frozen=frozen_flag if frozen is None else frozen, weakref_slot=weakref_flag if weakref_slot is None else weakref_slot, + init=init_flag, ) diff --git a/tests/test_funcs.py b/tests/test_funcs.py index 20e2747e6..2fc73dced 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -544,7 +544,14 @@ def test_unknown(self, C): # No generated class will have a four letter attribute. with pytest.raises(TypeError) as e: evolve(C(), aaaa=2) - expected = "__init__() got an unexpected keyword argument 'aaaa'" + + if hasattr(C, "__attrs_init__"): + expected = ( + "__attrs_init__() got an unexpected keyword argument 'aaaa'" + ) + else: + expected = "__init__() got an unexpected keyword argument 'aaaa'" + assert (expected,) == e.value.args def test_validator_failure(self): From 6be6f66b72c82bdf927ac4ce80ec548e6ef22158 Mon Sep 17 00:00:00 2001 From: Venky Iyer Date: Sun, 13 Dec 2020 18:16:26 -0800 Subject: [PATCH 05/11] Add another test for init flag --- tests/test_make.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_make.py b/tests/test_make.py index fad4ec7e2..232e770e3 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -569,6 +569,19 @@ class C(object): assert sentinel == getattr(C, method_name) + @pytest.mark.parametrize("init", [True, False]) + def test_respects_init_attrs_init(self, init): + """ + If init=False, adds __attrs_init__ to the class. + Otherwise, it does not. + """ + + class C(object): + x = attr.ib() + + C = attr.s(init=init)(C) + assert hasattr(C, "__attrs_init__") != init + @pytest.mark.skipif(PY2, reason="__qualname__ is PY3-only.") @given(slots_outer=booleans(), slots_inner=booleans()) def test_repr_qualname(self, slots_outer, slots_inner): @@ -1527,6 +1540,7 @@ class C(object): .add_order() .add_hash() .add_init() + .add_attrs_init() .add_repr("ns") .add_str() .build_class() From 0784d3b6754be69f8ebecc2fc2a7795d0da2b8ea Mon Sep 17 00:00:00 2001 From: Venky Iyer Date: Sun, 13 Dec 2020 18:29:23 -0800 Subject: [PATCH 06/11] add changelog --- changelog.d/731.change.rst | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 changelog.d/731.change.rst diff --git a/changelog.d/731.change.rst b/changelog.d/731.change.rst new file mode 100644 index 000000000..495c981e9 --- /dev/null +++ b/changelog.d/731.change.rst @@ -0,0 +1,7 @@ +``__attrs__init__()`` will now be injected if ``init==False`` or if ``auto_detect=True`` and a user-defined ``__init__()`` exists. + +This enables users to do "pre-init" work in their ``__init__()`` (such as ``super().__init__()``). + +``__init__()`` can then delegate constructor argument processing to ``__attrs_init__(*args, **kwargs)``. + +Note that ``__attrs_post_init__()`` will need to be explicitly called from ``__init__()`` if appropriate. From 4f39440026d618110a32aa0072886627deb99407 Mon Sep 17 00:00:00 2001 From: Venky Iyer Date: Sun, 13 Dec 2020 18:37:18 -0800 Subject: [PATCH 07/11] more docs --- src/attr/_make.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/attr/_make.py b/src/attr/_make.py index 6725b3d89..dd026eeb2 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1146,6 +1146,11 @@ def attrs( ``attrs`` attributes. Leading underscores are stripped for the argument name. If a ``__attrs_post_init__`` method exists on the class, it will be called after the class is fully initialized. + + If ``init`` is ``False``, an ``__attrs_init__`` method will be + injected instead. This allows you to define a custom ``__init__`` + method that can do pre-init work such as ``super().__init__()``, + and then call ``__attrs_init__()`` and ``__attrs_post_init__()``. :param bool slots: Create a `slotted class ` that's more memory-efficient. Slotted classes are generally superior to the default dict classes, but have some gotchas you should know about, so we @@ -1285,6 +1290,7 @@ def attrs( .. versionadded:: 20.1.0 *getstate_setstate* .. versionadded:: 20.1.0 *on_setattr* .. versionadded:: 20.3.0 *field_transformer* + .. versionchanged:: 20.4.0 *``init=False`` injects ``__attrs_init__`` """ if auto_detect and PY2: raise PythonTooOldError( From 31abe2b25055a291e52ae9fa906fc47a1c3aa6da Mon Sep 17 00:00:00 2001 From: Venky Iyer Date: Sun, 13 Dec 2020 18:38:46 -0800 Subject: [PATCH 08/11] whitespace --- src/attr/_make.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index dd026eeb2..66effc2bc 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1290,7 +1290,8 @@ def attrs( .. versionadded:: 20.1.0 *getstate_setstate* .. versionadded:: 20.1.0 *on_setattr* .. versionadded:: 20.3.0 *field_transformer* - .. versionchanged:: 20.4.0 *``init=False`` injects ``__attrs_init__`` + .. versionchanged:: 20.4.0 + ``init=False`` injects ``__attrs_init__`` """ if auto_detect and PY2: raise PythonTooOldError( From 1256fd947cf3d3f1634769df05b2b592780c26f8 Mon Sep 17 00:00:00 2001 From: Venky Iyer Date: Tue, 19 Jan 2021 11:53:58 -0800 Subject: [PATCH 09/11] Revert to calling __attrs_post_init__() from __attrs_init__() --- changelog.d/731.change.rst | 2 -- src/attr/_make.py | 2 +- tests/strategies.py | 2 -- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/changelog.d/731.change.rst b/changelog.d/731.change.rst index 495c981e9..c49dba3d7 100644 --- a/changelog.d/731.change.rst +++ b/changelog.d/731.change.rst @@ -3,5 +3,3 @@ This enables users to do "pre-init" work in their ``__init__()`` (such as ``super().__init__()``). ``__init__()`` can then delegate constructor argument processing to ``__attrs_init__(*args, **kwargs)``. - -Note that ``__attrs_post_init__()`` will need to be explicitly called from ``__init__()`` if appropriate. diff --git a/src/attr/_make.py b/src/attr/_make.py index cc763f48e..df80c0bdf 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -2310,7 +2310,7 @@ def fmt_setter_with_converter( names_for_globals[val_name] = a.validator names_for_globals[attr_name] = a - if post_init and not attrs_init: + if post_init: lines.append("self.__attrs_post_init__()") # because this is set only after __attrs_post_init is called, a crash diff --git a/tests/strategies.py b/tests/strategies.py index 271299bf8..fab9716d2 100644 --- a/tests/strategies.py +++ b/tests/strategies.py @@ -167,8 +167,6 @@ def post_init(self): def init(self, *args, **kwargs): self.__attrs_init__(*args, **kwargs) - if post_init_flag: - self.__attrs_post_init__() cls_dict["__init__"] = init From 0effbc2852c6d22e6d351d10b56ed5bc12d001d8 Mon Sep 17 00:00:00 2001 From: Venky Iyer Date: Tue, 19 Jan 2021 11:59:15 -0800 Subject: [PATCH 10/11] Update src/attr/_make.py Co-authored-by: Hynek Schlawack --- src/attr/_make.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index df80c0bdf..7546850cc 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -1326,7 +1326,7 @@ def attrs( .. versionadded:: 20.1.0 *getstate_setstate* .. versionadded:: 20.1.0 *on_setattr* .. versionadded:: 20.3.0 *field_transformer* - .. versionchanged:: 20.4.0 + .. versionchanged:: 21.1.0 ``init=False`` injects ``__attrs_init__`` """ if auto_detect and PY2: From 655c281b4b1dc0cbd7002373bfdc5dd9409b8d6f Mon Sep 17 00:00:00 2001 From: Venky Iyer Date: Fri, 22 Jan 2021 14:17:41 -0800 Subject: [PATCH 11/11] don't use attrs to create Factory --- src/attr/_make.py | 40 ++++++++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/src/attr/_make.py b/src/attr/_make.py index df80c0bdf..b729344b7 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -51,8 +51,6 @@ # Unique object for unequivocal getattr() defaults. _sentinel = object() -Factory = None - class _Nothing(object): """ @@ -2153,7 +2151,7 @@ def fmt_setter_with_converter( ) arg_name = a.name.lstrip("_") - has_factory = Factory is not None and isinstance(a.default, Factory) + has_factory = isinstance(a.default, Factory) if has_factory and a.default.takes_self: maybe_self = "self" else: @@ -2701,7 +2699,6 @@ def default(self, meth): _CountingAttr = _add_eq(_add_repr(_CountingAttr)) -@attrs(slots=True, init=False, hash=True) class Factory(object): """ Stores a factory callable. @@ -2717,8 +2714,7 @@ class Factory(object): .. versionadded:: 17.1.0 *takes_self* """ - factory = attrib() - takes_self = attrib() + __slots__ = ("factory", "takes_self") def __init__(self, factory, takes_self=False): """ @@ -2728,6 +2724,38 @@ def __init__(self, factory, takes_self=False): self.factory = factory self.takes_self = takes_self + def __getstate__(self): + """ + Play nice with pickle. + """ + return tuple(getattr(self, name) for name in self.__slots__) + + def __setstate__(self, state): + """ + Play nice with pickle. + """ + for name, value in zip(self.__slots__, state): + setattr(self, name, value) + + +_f = [ + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=True, + cmp=None, + eq=True, + order=False, + hash=True, + init=True, + inherited=False, + ) + for name in Factory.__slots__ +] + +Factory = _add_hash(_add_eq(_add_repr(Factory, attrs=_f), attrs=_f), attrs=_f) + def make_class(name, attrs, bases=(object,), **attributes_arguments): """