diff --git a/changelog.d/1427.change.md b/changelog.d/1427.change.md new file mode 100644 index 000000000..4c60e88ac --- /dev/null +++ b/changelog.d/1427.change.md @@ -0,0 +1 @@ +Values passed to the `__init__()` method of `attrs` classes are now correctly passed to `__attrs_pre_init__()` instead of their default values (in cases where *kw_only* was not specified). diff --git a/src/attr/_make.py b/src/attr/_make.py index e84d9792a..4b3ec7d03 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -2126,8 +2126,9 @@ def _attrs_to_init_script( ) lines.extend(extra_lines) - args = [] - kw_only_args = [] + args = [] # Parameters in the definition of __init__ + pre_init_args = [] # Parameters in the call to __attrs_pre_init__ + kw_only_args = [] # Used for both 'args' and 'pre_init_args' above attrs_to_validate = [] # This is a dictionary of names to validator and converter callables. @@ -2205,6 +2206,7 @@ def _attrs_to_init_script( kw_only_args.append(arg) else: args.append(arg) + pre_init_args.append(arg_name) if converter is not None: lines.append( @@ -2224,6 +2226,7 @@ def _attrs_to_init_script( kw_only_args.append(arg) else: args.append(arg) + pre_init_args.append(arg_name) lines.append(f"if {arg_name} is not NOTHING:") init_factory_name = _INIT_FACTORY_PAT % (a.name,) @@ -2266,6 +2269,7 @@ def _attrs_to_init_script( kw_only_args.append(arg_name) else: args.append(arg_name) + pre_init_args.append(arg_name) if converter is not None: lines.append( @@ -2322,7 +2326,7 @@ def _attrs_to_init_script( lines.append(f"BaseException.__init__(self, {vals})") args = ", ".join(args) - pre_init_args = args + pre_init_args = ", ".join(pre_init_args) if kw_only_args: # leading comma & kw_only args args += f"{', ' if args else ''}*, {', '.join(kw_only_args)}" @@ -2337,7 +2341,7 @@ def _attrs_to_init_script( pre_init_args += pre_init_kw_only_args if call_pre_init and pre_init_has_args: - # If pre init method has arguments, pass same arguments as `__init__`. + # If pre init method has arguments, pass the values given to __init__. lines[0] = f"self.__attrs_pre_init__({pre_init_args})" # Python <3.12 doesn't allow backslashes in f-strings. diff --git a/tests/test_make.py b/tests/test_make.py index 80c00662b..2e350577c 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -715,6 +715,49 @@ def __attrs_pre_init__(self, *, kw_and_default): assert 3 == val == inst.kw_and_default + @pytest.mark.usefixtures("with_and_without_validation") + def test_pre_init_with_mixture_of_defaults_and_kw_only(self): + """ + Attrs should properly handle a mixture of positional, positional with + default, keyword-only, and keyword-only with default attributes when + passing values to __attrs_pre_init__. + """ + g_val1 = None + g_val2 = None + g_val3 = None + g_val4 = None + g_val5 = None + g_val6 = None + + @attr.define + class MixtureClass: + val1: int + val2: int = 100 + val3: int = attr.field(factory=int) + val4: int = attr.field(kw_only=True) + val5: int = attr.field(default=100, kw_only=True) + val6: int = attr.field(factory=int, kw_only=True) + + def __attrs_pre_init__(self, val1, val2, val3, val4, val5, val6): + nonlocal g_val1, g_val2, g_val3, g_val4, g_val5, g_val6 + g_val1 = val1 + g_val2 = val2 + g_val3 = val3 + g_val4 = val4 + g_val5 = val5 + g_val6 = val6 + + inst = MixtureClass( + val1=200, val2=200, val3=200, val4=200, val5=200, val6=200 + ) + + assert 200 == g_val1 == inst.val1 + assert 200 == g_val2 == inst.val2 + assert 200 == g_val3 == inst.val3 + assert 200 == g_val4 == inst.val4 + assert 200 == g_val5 == inst.val5 + assert 200 == g_val6 == inst.val6 + @pytest.mark.usefixtures("with_and_without_validation") def test_post_init(self): """