Repository: pallets/jinja (python)

Issue:
Filters decorated with @async_variant are not pickleable 
<!--
This issue tracker is a tool to address bugs in Jinja itself. Please
use Pallets Discord or Stack Overflow for questions about your own code.

Replace this comment with a clear outline of what the bug is.
-->
The [`jinja2.async_utils.async_variant`][async_variant] decorator puts the wrong `__name__` and `__qualname__` on the decorated function.  One result of this is that functions which it decorates are not pickleable.

<!--
Describe how to replicate the bug.

Include a minimal reproducible example that demonstrates the bug.
Include the full traceback if there was an exception.
-->
Here's a doctest script which illustrates the issue.
```pycon
>>> import pickle
>>> import jinja2.filters

>>> pickle.dumps(jinja2.filters.do_first)
Traceback (most recent call last):
    ...
_pickle.PicklingError: Can't pickle <function sync_do_first at 0x...>: it's not the same object as jinja2.filters.sync_do_first

# Here's the crux:
>>> jinja2.filters.do_first.__name__
'sync_do_first'
```

[async_variant]: https://github.com/pallets/jinja/blob/09907ef37891be76ab0a1a3dc1a3f7df667db959/src/jinja2/async_utils.py#L11

<!--
Describe the expected behavior that should have happened but didn't.
-->

Environment:

- Python version: 3.10 — but I don't think that's important
- Jinja version: git main (09907ef37891be76ab0a1a3dc1a3f7df667db959) — but I think this affects all versions >= 3

I will submit a PR with one possible fix shortly.

Pull request description:
Fix pickleability of filters decorated with @async_variant
<!--
Before opening a PR, open a ticket describing the issue or feature the PR will address. Follow the steps in CONTRIBUTING.rst.

Replace this comment with a description of the change. Describe how it addresses the linked ticket.
-->

<!--
Link to relevant issues or previous PRs, one per line. Use "fixes" to automatically close an issue.
-->

- fixes #1612 

<!--
Ensure each step in CONTRIBUTING.rst is complete by adding an "x" to each box below.

If only docs were changed, these aren't relevant and can be removed.
-->

Checklist:

- [x] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [n/a] Add or update relevant docs, in the docs folder and in code.
- [x] Add an entry in `CHANGES.rst` summarizing the change and linking to the issue.
- [n/a] Add `.. versionchanged::` entries in any relevant code docs.
- [x] Run `pre-commit` hooks and fix any issues.
- [x] Run `pytest` and `tox`, no tests failed.


Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 3a779d7b..3e8b56e4 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -28,6 +28,8 @@ Unreleased
     ``{% trans "title" %}``. :issue:`1430`
 -   Update valid identifier characters from Python 3.6 to 3.7.
     :pr:`1571`
+-   Filters and tests decorated with ``@async_variant`` are pickleable.
+    :pr:`1612`
 
 
 Version 3.0.3
diff --git a/src/jinja2/async_utils.py b/src/jinja2/async_utils.py
index 35e6cb10..1a4f3892 100644
--- a/src/jinja2/async_utils.py
+++ b/src/jinja2/async_utils.py
@@ -1,5 +1,6 @@
 import inspect
 import typing as t
+from functools import WRAPPER_ASSIGNMENTS
 from functools import wraps
 
 from .utils import _PassArg
@@ -23,7 +24,15 @@ def async_variant(normal_func):  # type: ignore
             def is_async(args: t.Any) -> bool:
                 return t.cast(bool, args[0].environment.is_async)
 
-        @wraps(normal_func)
+        # Take the doc and annotations from the sync function, but the
+        # name from the async function. Pallets-Sphinx-Themes
+        # build_function_directive expects __wrapped__ to point to the
+        # sync function.
+        async_func_attrs = ("__module__", "__name__", "__qualname__")
+        normal_func_attrs = tuple(set(WRAPPER_ASSIGNMENTS).difference(async_func_attrs))
+
+        @wraps(normal_func, assigned=normal_func_attrs)
+        @wraps(async_func, assigned=async_func_attrs, updated=())
         def wrapper(*args, **kwargs):  # type: ignore
             b = is_async(args)
 


Test patch:
diff --git a/tests/test_pickle.py b/tests/test_pickle.py
new file mode 100644
index 00000000..b0f6bcfe
--- /dev/null
+++ b/tests/test_pickle.py
@@ -0,0 +1,6 @@
+import pickle
+
+
+def test_environment(env):
+    env = pickle.loads(pickle.dumps(env))
+    assert env.from_string("x={{ x }}").render(x=42) == "x=42"