Skip to content

Fixes issue #1427 #1428

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/1427.change.md
Original file line number Diff line number Diff line change
@@ -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).
12 changes: 8 additions & 4 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand All @@ -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,)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)}"
Expand All @@ -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.
Expand Down
43 changes: 43 additions & 0 deletions tests/test_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down