Repository: pallets/jinja (python)

Issue:
unique filter does not work when chain with a filter that returns async generator
`unique()` filter does not work when chain with a filter that returns async generator

Environment:

- Python version: 3.11
- Jinja version: 3.1.2

The template:
```
{%- set commit_authors = commits | rejectattr("author", "eq", "mergify[bot]") | unique(false, "email_author")| list -%}
```

The backtrace:
```
  File "xxxxxx.py", line 2314, in render_template
    return await env.from_string(template).render_async(**infos)
  File "jinja2/environment.py", line 1324, in render_async
    return self.environment.handle_exception()
  File "jinja2/environment.py", line 936, in handle_exception
    raise rewrite_traceback_stack(source=source)
  File "jinja2/environment.py", line 1321, in <listcomp>
    [n async for n in self.root_render_func(ctx)]  # type: ignore
  File "<template>", line 7, in top-level template code
  File "jinja2/async_utils.py", line 65, in auto_await
    return await t.cast("t.Awaitable[V]", value)
  File "jinja2/filters.py", line 1329, in do_list
    return await auto_to_list(value)
  File "jinja2/async_utils.py", line 84, in auto_to_list
    return [x async for x in auto_aiter(value)]
  File "jinja2/async_utils.py", line 84, in <listcomp>
    return [x async for x in auto_aiter(value)]
  File "jinja2/async_utils.py", line 77, in auto_aiter
    for item in t.cast("t.Iterable[V]", iterable):
  File "jinja2/filters.py", line 437, in do_unique
    for item in value:
TypeError: 'async_generator' object is not iterable
```

Pull request description:
make unique filter async aware
unique() filter doesn't work chained after a filter that return an async generator.

This introduces a async variant of the filter transform the async
generator into a list to be able to apply the unique filter.

Fixes #1781

Checklist:

- [X] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [X] 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.
- [X] 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 540d5ccc..e4bffbfb 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -16,6 +16,8 @@ Unreleased
     :pr:`1960`
 -   The runtime uses the correct ``concat`` function for the current environment
     when calling block references. :issue:`1701`
+-   Make ``|unique`` async-aware, allowing it to be used after another
+    async-aware filter. :issue:`1781`
 
 
 Version 3.1.4
diff --git a/src/jinja2/filters.py b/src/jinja2/filters.py
index 4c949cbd..af9f6bc0 100644
--- a/src/jinja2/filters.py
+++ b/src/jinja2/filters.py
@@ -438,7 +438,7 @@ def do_sort(
 
 
 @pass_environment
-def do_unique(
+def sync_do_unique(
     environment: "Environment",
     value: "t.Iterable[V]",
     case_sensitive: bool = False,
@@ -470,6 +470,18 @@ def do_unique(
             yield item
 
 
+@async_variant(sync_do_unique)  # type: ignore
+async def do_unique(
+    environment: "Environment",
+    value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
+    case_sensitive: bool = False,
+    attribute: t.Optional[t.Union[str, int]] = None,
+) -> "t.Iterator[V]":
+    return sync_do_unique(
+        environment, await auto_to_list(value), case_sensitive, attribute
+    )
+
+
 def _min_or_max(
     environment: "Environment",
     value: "t.Iterable[V]",


Test patch:
diff --git a/tests/test_async_filters.py b/tests/test_async_filters.py
index e8cc350d..e9892f1e 100644
--- a/tests/test_async_filters.py
+++ b/tests/test_async_filters.py
@@ -277,6 +277,13 @@ def test_slice(env_async, items):
     )
 
 
+def test_unique_with_async_gen(env_async):
+    items = ["a", "b", "c", "c", "a", "d", "z"]
+    tmpl = env_async.from_string("{{ items|reject('==', 'z')|unique|list }}")
+    out = tmpl.render(items=items)
+    assert out == "['a', 'b', 'c', 'd']"
+
+
 def test_custom_async_filter(env_async, run_async_fn):
     async def customfilter(val):
         return str(val)