Repository: python-attrs/attrs (python)

Issue:
Error occurs when using converters.optional and converters.pipe together
23.2.0 is OK.
```Python
Python 3.10.13 (main, Dec  6 2023, 12:01:00) [GCC 13.2.1 20230801] on linux
>>> import attr
>>> attr.__version__
'23.2.0'
>>> @attr.define
... class A:
...     abc: int | None = attr.field(converter=attr.converters.optional(attr.converters.pipe(str, int)), default=None)
...     
>>> A()
A(abc=None)
>>> A('1')
A(abc=1)
```

after 24.1.0, error occurs:
```Python
Python 3.10.13 (main, Dec  6 2023, 12:01:00) [GCC 13.2.1 20230801] on linux
>>> import attr
>>> attr.__version__
'24.2.0'
>>> @attr.define
... class A:
...     abc: int | None = attr.field(converter=attr.converters.optional(attr.converters.pipe(str, int)), default=None)
...     
>>> A()
A(abc=None)
>>> A('1')
Traceback (most recent call last):
  File "/opt/pycharm-professional/plugins/python-ce/helpers/pydev/pydevconsole.py", line 364, in runcode
    coro = func()
  File "<input>", line 1, in <module>
  File "<attrs generated init __main__.A>", line 3, in __init__
  File "/home/mio/Work/venv/calliper310N/lib/python3.10/site-packages/attr/converters.py", line 40, in optional_converter
    return converter(val)
TypeError: Converter.__init__.<locals>.<lambda>() missing 2 required positional arguments: 'instance' and 'field'

```

Pull request:
Allow converter.optional to take a Converter such as converter.pipe as its argument
# Summary

<!-- Please tell us what your pull request is about here. -->
Allow `converter.optional` to take a Converter such as `converter.pipe` as its argument.

Fixes #1348


# 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] Do **not** open pull requests from your `main` branch – **use a separate branch**!
  - There's a ton of footguns waiting if you don't heed this warning. You can still go back to your project, create a branch from your main branch, push it, and open the pull request from the new branch.
  - This is not a pre-requisite for your pull request to be accepted, but **you have been warned**.
- [x] Added **tests** for changed code.
  Our CI fails if coverage is not 100%.
- (N/A) New features have been added to our [Hypothesis testing strategy](https://github.com/python-attrs/attrs/blob/main/tests/strategies.py).
- (N/A) Changes or additions to public APIs are reflected in our type stubs (files ending in ``.pyi``).
- (N/A) Updated **documentation** for changed code.
- [x] Documentation in `.rst` and `.md` 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/1372.change.md b/changelog.d/1372.change.md
new file mode 100644
index 0000000..fcb9434
--- /dev/null
+++ b/changelog.d/1372.change.md
@@ -0,0 +1,1 @@
+`attrs.converters.optional()` works again when taking `attrs.converters.pipe()` or another Converter as its argument.
diff --git a/src/attr/converters.py b/src/attr/converters.py
index cac62dd..0a79dee 100644
--- a/src/attr/converters.py
+++ b/src/attr/converters.py
@@ -7,7 +7,7 @@ Commonly useful converters.
 import typing
 
 from ._compat import _AnnotationExtractor
-from ._make import NOTHING, Factory, pipe
+from ._make import NOTHING, Converter, Factory, pipe
 
 
 __all__ = [
@@ -33,10 +33,19 @@ def optional(converter):
     .. versionadded:: 17.1.0
     """
 
-    def optional_converter(val):
-        if val is None:
-            return None
-        return converter(val)
+    if isinstance(converter, Converter):
+
+        def optional_converter(val, inst, field):
+            if val is None:
+                return None
+            return converter(val, inst, field)
+
+    else:
+
+        def optional_converter(val):
+            if val is None:
+                return None
+            return converter(val)
 
     xtr = _AnnotationExtractor(converter)
 
@@ -48,6 +57,9 @@ def optional(converter):
     if rt:
         optional_converter.__annotations__["return"] = typing.Optional[rt]
 
+    if isinstance(converter, Converter):
+        return Converter(optional_converter, takes_self=True, takes_field=True)
+
     return optional_converter
 
 


Test patch:
diff --git a/tests/test_converters.py b/tests/test_converters.py
index 164fb0f..d4aca43 100644
--- a/tests/test_converters.py
+++ b/tests/test_converters.py
@@ -143,6 +143,14 @@ class TestOptional:
         with pytest.raises(ValueError):
             c("not_an_int")
 
+    def test_converter_instance(self):
+        """
+        Works when passed a Converter instance as argument.
+        """
+        c = optional(Converter(to_bool))
+
+        assert True is c("yes", None, None)
+
 
 class TestDefaultIfNone:
     def test_missing_default(self):
@@ -272,6 +280,48 @@ class TestPipe:
         )
 
 
+class TestOptionalPipe:
+    def test_optional(self):
+        """
+        Nothing happens if None.
+        """
+        c = optional(pipe(str, Converter(to_bool), bool))
+
+        assert None is c.converter(None, None, None)
+
+    def test_pipe(self):
+        """
+        A value is given, run it through all wrapped converters.
+        """
+        c = optional(pipe(str, Converter(to_bool), bool))
+
+        assert (
+            True
+            is c.converter("True", None, None)
+            is c.converter(True, None, None)
+        )
+
+    def test_instance(self):
+        """
+        Should work when set as an attrib.
+        """
+
+        @attr.s
+        class C:
+            x = attrib(
+                converter=optional(pipe(str, Converter(to_bool), bool)),
+                default=None,
+            )
+
+        c1 = C()
+
+        assert None is c1.x
+
+        c2 = C("True")
+
+        assert True is c2.x
+
+
 class TestToBool:
     def test_unhashable(self):
         """