Pass args from `__init__` to `__attrs_pre_init__` if requested (#1187)
* Pass args from `__init__` to `__attrs_pre_init__` if requested Detect if `__attrs_pre_init__` has arguments besides `self` using `inspect.signature`. If so, pass `__attrs_pre_init__` the same arguments that `__init__` (or `__attrs_init__`) expects. * Add changelog entry for `__attrs_pre_init__` args changes * Don't use monkeypatch in new code * Clarify docs * Missed one monkeypatching --------- Co-authored-by: Hynek Schlawack <hs@ox.cx>
This commit is contained in:
parent
46a03dcef6
commit
c2824ac30f
|
@ -0,0 +1,2 @@
|
|||
If *attrs* detects that `__attrs_pre_init__` accepts more than just `self`, it will call it with the same arguments as `__init__` was called.
|
||||
This allows you to, for example, pass arguments to `super().__init__()`.
|
|
@ -357,7 +357,8 @@ However, sometimes you need to do that one quick thing before or after your clas
|
|||
For that purpose, *attrs* offers the following options:
|
||||
|
||||
- `__attrs_pre_init__` is automatically detected and run *before* *attrs* starts initializing.
|
||||
This is useful if you need to inject a call to `super().__init__()`.
|
||||
If `__attrs_pre_init__` takes more than the `self` argument, the *attrs*-generated `__init__` will call it with the same arguments it received itself.
|
||||
This is useful if you need to inject a call to `super().__init__()` -- with or without arguments.
|
||||
|
||||
- `__attrs_post_init__` is automatically detected and run *after* *attrs* is done initializing your instance.
|
||||
This is useful if you want to derive some attribute from others or perform some kind of validation over the whole instance.
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import contextlib
|
||||
import copy
|
||||
import enum
|
||||
import inspect
|
||||
import linecache
|
||||
import sys
|
||||
import types
|
||||
|
@ -624,6 +625,7 @@ class _ClassBuilder:
|
|||
"_delete_attribs",
|
||||
"_frozen",
|
||||
"_has_pre_init",
|
||||
"_pre_init_has_args",
|
||||
"_has_post_init",
|
||||
"_is_exc",
|
||||
"_on_setattr",
|
||||
|
@ -670,6 +672,13 @@ class _ClassBuilder:
|
|||
self._weakref_slot = weakref_slot
|
||||
self._cache_hash = cache_hash
|
||||
self._has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False))
|
||||
self._pre_init_has_args = False
|
||||
if self._has_pre_init:
|
||||
# Check if the pre init method has more arguments than just `self`
|
||||
# We want to pass arguments if pre init expects arguments
|
||||
pre_init_func = cls.__attrs_pre_init__
|
||||
pre_init_signature = inspect.signature(pre_init_func)
|
||||
self._pre_init_has_args = len(pre_init_signature.parameters) > 1
|
||||
self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False))
|
||||
self._delete_attribs = not bool(these)
|
||||
self._is_exc = is_exc
|
||||
|
@ -974,6 +983,7 @@ class _ClassBuilder:
|
|||
self._cls,
|
||||
self._attrs,
|
||||
self._has_pre_init,
|
||||
self._pre_init_has_args,
|
||||
self._has_post_init,
|
||||
self._frozen,
|
||||
self._slots,
|
||||
|
@ -1000,6 +1010,7 @@ class _ClassBuilder:
|
|||
self._cls,
|
||||
self._attrs,
|
||||
self._has_pre_init,
|
||||
self._pre_init_has_args,
|
||||
self._has_post_init,
|
||||
self._frozen,
|
||||
self._slots,
|
||||
|
@ -1984,6 +1995,7 @@ def _make_init(
|
|||
cls,
|
||||
attrs,
|
||||
pre_init,
|
||||
pre_init_has_args,
|
||||
post_init,
|
||||
frozen,
|
||||
slots,
|
||||
|
@ -2027,6 +2039,7 @@ def _make_init(
|
|||
frozen,
|
||||
slots,
|
||||
pre_init,
|
||||
pre_init_has_args,
|
||||
post_init,
|
||||
cache_hash,
|
||||
base_attr_map,
|
||||
|
@ -2107,6 +2120,7 @@ def _attrs_to_init_script(
|
|||
frozen,
|
||||
slots,
|
||||
pre_init,
|
||||
pre_init_has_args,
|
||||
post_init,
|
||||
cache_hash,
|
||||
base_attr_map,
|
||||
|
@ -2361,11 +2375,23 @@ def _attrs_to_init_script(
|
|||
lines.append(f"BaseException.__init__(self, {vals})")
|
||||
|
||||
args = ", ".join(args)
|
||||
pre_init_args = args
|
||||
if kw_only_args:
|
||||
args += "%s*, %s" % (
|
||||
", " if args else "", # leading comma
|
||||
", ".join(kw_only_args), # kw_only args
|
||||
)
|
||||
pre_init_kw_only_args = ", ".join(
|
||||
["%s=%s" % (kw_arg, kw_arg) for kw_arg in kw_only_args]
|
||||
)
|
||||
pre_init_args += (
|
||||
", " if pre_init_args else ""
|
||||
) # handle only kwargs and no regular args
|
||||
pre_init_args += pre_init_kw_only_args
|
||||
|
||||
if pre_init and pre_init_has_args:
|
||||
# If pre init method has arguments, pass same arguments as `__init__`
|
||||
lines[0] = "self.__attrs_pre_init__(%s)" % pre_init_args
|
||||
|
||||
return (
|
||||
"def %s(self, %s):\n %s\n"
|
||||
|
|
|
@ -6,6 +6,7 @@ Tests for dunder methods from `attrib._make`.
|
|||
|
||||
|
||||
import copy
|
||||
import inspect
|
||||
import pickle
|
||||
|
||||
import pytest
|
||||
|
@ -84,10 +85,15 @@ 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.
|
||||
"""
|
||||
has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False))
|
||||
|
||||
cls.__init__ = _make_init(
|
||||
cls,
|
||||
cls.__attrs_attrs__,
|
||||
getattr(cls, "__attrs_pre_init__", False),
|
||||
has_pre_init,
|
||||
len(inspect.signature(cls.__attrs_pre_init__).parameters) > 1
|
||||
if has_pre_init
|
||||
else False,
|
||||
getattr(cls, "__attrs_post_init__", False),
|
||||
frozen,
|
||||
_is_slot_cls(cls),
|
||||
|
|
|
@ -613,21 +613,89 @@ class TestAttributes:
|
|||
assert C.D.__qualname__ == C.__qualname__ + ".D"
|
||||
|
||||
@pytest.mark.parametrize("with_validation", [True, False])
|
||||
def test_pre_init(self, with_validation, monkeypatch):
|
||||
def test_pre_init(self, with_validation):
|
||||
"""
|
||||
Verify that __attrs_pre_init__ gets called if defined.
|
||||
"""
|
||||
monkeypatch.setattr(_config, "_run_validators", with_validation)
|
||||
|
||||
@attr.s
|
||||
class C:
|
||||
def __attrs_pre_init__(self2):
|
||||
self2.z = 30
|
||||
|
||||
c = C()
|
||||
try:
|
||||
attr.validators.set_disabled(not with_validation)
|
||||
c = C()
|
||||
finally:
|
||||
attr.validators.set_disabled(False)
|
||||
|
||||
assert 30 == getattr(c, "z", None)
|
||||
|
||||
@pytest.mark.parametrize("with_validation", [True, False])
|
||||
def test_pre_init_args(self, with_validation):
|
||||
"""
|
||||
Verify that __attrs_pre_init__ gets called with extra args if defined.
|
||||
"""
|
||||
|
||||
@attr.s
|
||||
class C:
|
||||
x = attr.ib()
|
||||
|
||||
def __attrs_pre_init__(self2, x):
|
||||
self2.z = x + 1
|
||||
|
||||
try:
|
||||
attr.validators.set_disabled(not with_validation)
|
||||
c = C(x=10)
|
||||
finally:
|
||||
attr.validators.set_disabled(False)
|
||||
|
||||
assert 11 == getattr(c, "z", None)
|
||||
|
||||
@pytest.mark.parametrize("with_validation", [True, False])
|
||||
def test_pre_init_kwargs(self, with_validation):
|
||||
"""
|
||||
Verify that __attrs_pre_init__ gets called with extra args and kwargs if defined.
|
||||
"""
|
||||
|
||||
@attr.s
|
||||
class C:
|
||||
x = attr.ib()
|
||||
y = attr.field(kw_only=True)
|
||||
|
||||
def __attrs_pre_init__(self2, x, y):
|
||||
self2.z = x + y + 1
|
||||
|
||||
try:
|
||||
attr.validators.set_disabled(not with_validation)
|
||||
c = C(10, y=11)
|
||||
finally:
|
||||
attr.validators.set_disabled(False)
|
||||
|
||||
assert 22 == getattr(c, "z", None)
|
||||
|
||||
@pytest.mark.parametrize("with_validation", [True, False])
|
||||
def test_pre_init_kwargs_only(self, with_validation):
|
||||
"""
|
||||
Verify that __attrs_pre_init__ gets called with extra kwargs only if
|
||||
defined.
|
||||
"""
|
||||
|
||||
@attr.s
|
||||
class C:
|
||||
y = attr.field(kw_only=True)
|
||||
|
||||
def __attrs_pre_init__(self2, y):
|
||||
self2.z = y + 1
|
||||
|
||||
try:
|
||||
attr.validators.set_disabled(not with_validation)
|
||||
c = C(y=11)
|
||||
finally:
|
||||
attr.validators.set_disabled(False)
|
||||
|
||||
assert 12 == getattr(c, "z", None)
|
||||
|
||||
@pytest.mark.parametrize("with_validation", [True, False])
|
||||
def test_post_init(self, with_validation, monkeypatch):
|
||||
"""
|
||||
|
|
Loading…
Reference in New Issue