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:
Victor Song 2023-09-29 01:47:51 -05:00 committed by GitHub
parent 46a03dcef6
commit c2824ac30f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 108 additions and 5 deletions

View File

@ -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__()`.

View File

@ -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.

View File

@ -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"

View File

@ -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),

View File

@ -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):
"""