__attrs_pre_init__ (#750)

* add pre-init following post-init pattern

* add tests

* add changelog

* some typos
This commit is contained in:
Venky Iyer 2021-01-24 22:31:37 -08:00 committed by GitHub
parent 654aa92475
commit efcbae51cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 67 additions and 3 deletions

View File

@ -0,0 +1 @@
Allow for a ``__attrs_pre_init__()`` method that -- if defined -- will get called at the beginning of the ``attrs``-generated ``__init__()`` method.

View File

@ -588,6 +588,7 @@ class _ClassBuilder(object):
"_cls_dict",
"_delete_attribs",
"_frozen",
"_has_pre_init",
"_has_post_init",
"_is_exc",
"_on_setattr",
@ -633,6 +634,7 @@ class _ClassBuilder(object):
self._frozen = frozen
self._weakref_slot = weakref_slot
self._cache_hash = cache_hash
self._has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False))
self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False))
self._delete_attribs = not bool(these)
self._is_exc = is_exc
@ -889,6 +891,7 @@ class _ClassBuilder(object):
_make_init(
self._cls,
self._attrs,
self._has_pre_init,
self._has_post_init,
self._frozen,
self._slots,
@ -908,6 +911,7 @@ class _ClassBuilder(object):
_make_init(
self._cls,
self._attrs,
self._has_pre_init,
self._has_post_init,
self._frozen,
self._slots,
@ -1177,9 +1181,11 @@ def attrs(
behavior <https://github.com/python-attrs/attrs/issues/136>`_ for more
details.
:param bool init: Create a ``__init__`` method that initializes the
``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.
``attrs`` attributes. Leading underscores are stripped for the argument
name. If a ``__attrs_pre_init__`` method exists on the class, it will
be called before the class is initialized. 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__``
@ -1326,6 +1332,8 @@ def attrs(
.. versionadded:: 20.3.0 *field_transformer*
.. versionchanged:: 21.1.0
``init=False`` injects ``__attrs_init__``
.. versionchanged:: 21.1.0 Support for ``__attrs_pre_init__``
"""
if auto_detect and PY2:
raise PythonTooOldError(
@ -1893,6 +1901,7 @@ def _is_slot_attr(a_name, base_attr_map):
def _make_init(
cls,
attrs,
pre_init,
post_init,
frozen,
slots,
@ -1931,6 +1940,7 @@ def _make_init(
filtered_attrs,
frozen,
slots,
pre_init,
post_init,
cache_hash,
base_attr_map,
@ -2071,6 +2081,7 @@ def _attrs_to_init_script(
attrs,
frozen,
slots,
pre_init,
post_init,
cache_hash,
base_attr_map,
@ -2088,6 +2099,9 @@ def _attrs_to_init_script(
a cached ``object.__setattr__``.
"""
lines = []
if pre_init:
lines.append("self.__attrs_pre_init__()")
if needs_cached_setattr:
lines.append(
# Circumvent the __setattr__ descriptor to save one lookup per
@ -2789,10 +2803,13 @@ def make_class(name, attrs, bases=(object,), **attributes_arguments):
else:
raise TypeError("attrs argument must be a dict or a list.")
pre_init = cls_dict.pop("__attrs_pre_init__", None)
post_init = cls_dict.pop("__attrs_post_init__", None)
user_init = cls_dict.pop("__init__", None)
body = {}
if pre_init is not None:
body["__attrs_pre_init__"] = pre_init
if post_init is not None:
body["__attrs_post_init__"] = post_init
if user_init is not None:

View File

@ -153,9 +153,17 @@ def simple_classes(
attr_names = gen_attr_names()
cls_dict = dict(zip(attr_names, attrs))
pre_init_flag = draw(st.booleans())
post_init_flag = draw(st.booleans())
init_flag = draw(st.booleans())
if pre_init_flag:
def pre_init(self):
pass
cls_dict["__attrs_pre_init__"] = pre_init
if post_init_flag:
def post_init(self):

View File

@ -62,6 +62,7 @@ def _add_init(cls, frozen):
cls.__init__ = _make_init(
cls,
cls.__attrs_attrs__,
getattr(cls, "__attrs_pre_init__", False),
getattr(cls, "__attrs_post_init__", False),
frozen,
_is_slot_cls(cls),

View File

@ -628,6 +628,22 @@ class TestAttributes(object):
assert C.D.__name__ == "D"
assert C.D.__qualname__ == C.__qualname__ + ".D"
@pytest.mark.parametrize("with_validation", [True, False])
def test_pre_init(self, with_validation, monkeypatch):
"""
Verify that __attrs_pre_init__ gets called if defined.
"""
monkeypatch.setattr(_config, "_run_validators", with_validation)
@attr.s
class C(object):
def __attrs_pre_init__(self2):
self2.z = 30
c = C()
assert 30 == getattr(c, "z", None)
@pytest.mark.parametrize("with_validation", [True, False])
def test_post_init(self, with_validation, monkeypatch):
"""
@ -647,6 +663,27 @@ class TestAttributes(object):
assert 30 == getattr(c, "z", None)
@pytest.mark.parametrize("with_validation", [True, False])
def test_pre_post_init_order(self, with_validation, monkeypatch):
"""
Verify that __attrs_post_init__ gets called if defined.
"""
monkeypatch.setattr(_config, "_run_validators", with_validation)
@attr.s
class C(object):
x = attr.ib()
def __attrs_pre_init__(self2):
self2.z = 30
def __attrs_post_init__(self2):
self2.z += self2.x
c = C(x=10)
assert 40 == getattr(c, "z", None)
def test_types(self):
"""
Sets the `Attribute.type` attr from type argument.