Repository: python-attrs/attrs

Issue:
Defaults are passed to `__attrs_pre_init__` instead of the init values themselves
The documentation says:
>If `__attrs_pre_init__` takes more than the `self` argument, the attrs-generated `__init__` will call it with the same arguments it received itself.

However, if those attributes are specified with default values (and not specified as keyword only), then their default value is passed into `__attrs_pre_init__` even when a user has given them init values:
```py
@attrs.define
class Example:
    val1: int
    val2: int = 100
    val3: dict = attrs.field(factory=dict)
    val4: int = attrs.field(kw_only=True)
    val5: int = attrs.field(default=100, kw_only=True)

    def __attrs_pre_init__(self, val1, val2, val3, val4, val5):
        print("val1", val1)
        print("val2", val2)
        print("val3", val3)
        print("val4", val4)
        print("val5", val5)

e = Example(val1=200, val2=200, val3={"blah": "blah"}, val4=200, val5=200)
```
```
val1 200
val2 100              # <-- Incorrect
val3 _Nothing.NOTHING # <-- Incorrect
val4 200
val5 200
```
If we inspect the generated __init__ method:
```py
import inspect
print(inspect.getsource(Example.__init__))
```
We get:
```py
def __init__(self, val1, val2=attr_dict['val2'].default, val3=NOTHING, *, val4, val5=attr_dict['val5'].default):
    self.__attrs_pre_init__(val1, val2=attr_dict['val2'].default, val3=NOTHING, val4=val4, val5=val5)
    ...
```
When I would expect either:
```py
def __init__(self, val1, val2=attr_dict['val2'].default, val3=NOTHING, *, val4, val5=attr_dict['val5'].default):
    self.__attrs_pre_init__(val1, val2, val3, val4=val4, val5=val5)
    # Or:
    self.__attrs_pre_init__(val1, val2=val2, val3=val3, val4=val4, val5=val5)
    ...
```
Assuming this is a bug, this is probably a simple fix.
Attrs version: `25.3.0`


Discussion:

Patch:
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,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.


Test patch:
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):
         """