Repository: python-attrs/attrs (python)

Issue:
Pass all args to `__attrs_pre_init__()`
I am subclassing a class that I have no control over, which is written in standard Python.  The base class A has a number of arguments that I want to set.  Infact there are 3 classes.  C (attrs class) is a sublcass of B (attrs class) which is a subclass of A (non-attrs class).

When I instantiate C, I want to pass arguments to B and A.

I implemented class based factory functions (vai `@classmethod`), which works well for a single level inheritance, but I'm not sure how to use the classmethod of B from the classmethod of A.  I figured I'm stuck with duplicating of the B initialisation within the C classmethod and the instantiating class C by passing all the necessary arguments.

All this involves defining a custom `__init__()` method (https://www.attrs.org/en/stable/init.html#custom-init), but it seems to me that could be avoided by using `__attrs_pre_init__()`, but unfortunately it seems no arguments are passed to that (i.e. no `*args` or `**kwargs`).

Is there a reason why arguments are not passed to `attrs_pre_init__()`, and only passed to `__init__()`?

I think it makes sense to pass all arguments to `__attrs_pre_init__()` so the user can have the flexibility to do what is required (or nothing at all).

```python

    def __attrs_pre_init__(self, *args, **kwargs):
        ...
        super().__init__(*args, **kwargs)
        ...
```

I can see why it is less relevant for `__attrs_post_init__()`, but I'm sure someone can think of a use-case where the arguments could be useful (e.g. for arguments that aren't stored as instanace attributes).

Pull request:
Pass args from `__init__` to `__attrs_pre_init__` if requested
# Summary

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. This closes #1102.

This is my first contribution to this project, so I would greatly appreciate any feedback.


# Pull Request Check List

<!--
This is just a friendly reminder about the most common mistakes.
Please make sure that you tick all boxes.
But please read our [contribution guide](https://github.com/python-attrs/attrs/blob/main/.github/CONTRIBUTING.md) at least once, it will save you unnecessary review cycles!

If an item doesn't apply to your pull request, **check it anyway** to make it apparent that there's nothing left to do.
If your pull request is a documentation fix or a trivial typo, feel free to delete the whole thing.
-->

- [x] Added **tests** for changed code.
  Our CI fails if coverage is not 100%.
- [ ] New features have been added to our [Hypothesis testing strategy](https://github.com/python-attrs/attrs/blob/main/tests/strategies.py).
- [ ] Changes or additions to public APIs are reflected in our type stubs (files ending in ``.pyi``).
    - [ ] ...and used in the stub test file `tests/typing_example.py`.
    - [ ] If they've been added to `attr/__init__.pyi`, they've *also* been re-imported in `attrs/__init__.pyi`.
- [x] Updated **documentation** for changed code.
    - [ ] New functions/classes have to be added to `docs/api.rst` by hand.
    - [ ] Changes to the signature of `@attr.s()` have to be added by hand too.
    - [ ] Changed/added classes/methods/functions have appropriate `versionadded`, `versionchanged`, or `deprecated` [directives](http://www.sphinx-doc.org/en/stable/markup/para.html#directive-versionadded).
          The next version is the second number in the current release + 1.
          The first number represents the current year.
          So if the current version on PyPI is 22.2.0, the next version is gonna be 22.3.0.
          If the next version is the first in the new year, it'll be 23.1.0.
- [x] Documentation in `.rst` files is written using [semantic newlines](https://rhodesmill.org/brandon/2012/one-sentence-per-line/).
- [x] Changes (and possible deprecations) have news fragments in [`changelog.d`](https://github.com/python-attrs/attrs/blob/main/changelog.d).
- [x] Consider granting [push permissions to the PR branch](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork), so maintainers can fix minor issues themselves without pestering you.

<!--
If you have *any* questions to *any* of the points above, just **submit and ask**!
This checklist is here to *help* you, not to deter you from contributing!
-->


Patch:
diff --git a/changelog.d/1187.change.md b/changelog.d/1187.change.md
new file mode 100644
index 0000000..b06f115
--- /dev/null
+++ b/changelog.d/1187.change.md
@@ -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__()`.
diff --git a/docs/init.md b/docs/init.md
index 2320294..3b2e665 100644
--- a/docs/init.md
+++ b/docs/init.md
@@ -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.
diff --git a/src/attr/_make.py b/src/attr/_make.py
index 1c58eaf..eb7a1fe 100644
--- a/src/attr/_make.py
+++ b/src/attr/_make.py
@@ -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"


Test patch:
diff --git a/tests/test_dunders.py b/tests/test_dunders.py
index 29673c4..d0d289d 100644
--- a/tests/test_dunders.py
+++ b/tests/test_dunders.py
@@ -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),
diff --git a/tests/test_make.py b/tests/test_make.py
index a00922c..286f18a 100644
--- a/tests/test_make.py
+++ b/tests/test_make.py
@@ -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):
         """