]> git.ipfire.org Git - thirdparty/jinja.git/commitdiff
add case_sensitive parameter groupby filter 1465/head
authorDavid Lord <davidism@gmail.com>
Mon, 14 Jun 2021 15:57:28 +0000 (18:57 +0300)
committerDavid Lord <davidism@gmail.com>
Tue, 8 Mar 2022 14:55:05 +0000 (06:55 -0800)
Co-authored-by: Yuri Sukhov <yuri.sukhov@gmail.com>
CHANGES.rst
src/jinja2/filters.py
tests/test_async_filters.py
tests/test_filters.py

index d67381b1bb8025d0f6ab693577321b42b0fa3cfe..88e167d26b9d6e2d61eb0c620aa085289c6a9edc 100644 (file)
@@ -33,6 +33,9 @@ Unreleased
 -   Add ``items`` filter. :issue:`1561`
 -   Subscriptions (``[0]``, etc.) can be used after filters, tests, and
     calls when the environment is in async mode. :issue:`1573`
+-   The ``groupby`` filter is case-insensitive by default, matching
+    other comparison filters. Added the ``case_sensitive`` parameter to
+    control this. :issue:`1463`
 
 
 Version 3.0.3
index 80ea6504e3cec85baa3ea49534f35c047cd6b39b..7e0970988c3bb17bc2775d5fec04513196d39e33 100644 (file)
@@ -1163,7 +1163,8 @@ def sync_do_groupby(
     value: "t.Iterable[V]",
     attribute: t.Union[str, int],
     default: t.Optional[t.Any] = None,
-) -> "t.List[t.Tuple[t.Any, t.List[V]]]":
+    case_sensitive: bool = False,
+) -> "t.List[_GroupTuple]":
     """Group a sequence of objects by an attribute using Python's
     :func:`itertools.groupby`. The attribute can use dot notation for
     nested access, like ``"address.city"``. Unlike Python's ``groupby``,
@@ -1203,18 +1204,42 @@ def sync_do_groupby(
           <li>{{ city }}: {{ items|map(attribute="name")|join(", ") }}</li>
         {% endfor %}</ul>
 
+    Like the :func:`~jinja-filters.sort` filter, sorting and grouping is
+    case-insensitive by default. The ``key`` for each group will have
+    the case of the first item in that group of values. For example, if
+    a list of users has cities ``["CA", "NY", "ca"]``, the "CA" group
+    will have two values. This can be disabled by passing
+    ``case_sensitive=True``.
+
+    .. versionchanged:: 3.1
+        Added the ``case_sensitive`` parameter. Sorting and grouping is
+        case-insensitive by default, matching other filters that do
+        comparisons.
+
     .. versionchanged:: 3.0
         Added the ``default`` parameter.
 
     .. versionchanged:: 2.6
         The attribute supports dot notation for nested access.
     """
-    expr = make_attrgetter(environment, attribute, default=default)
-    return [
+    expr = make_attrgetter(
+        environment,
+        attribute,
+        postprocess=ignore_case if not case_sensitive else None,
+        default=default,
+    )
+    out = [
         _GroupTuple(key, list(values))
         for key, values in groupby(sorted(value, key=expr), expr)
     ]
 
+    if not case_sensitive:
+        # Return the real key from the first value instead of the lowercase key.
+        output_expr = make_attrgetter(environment, attribute, default=default)
+        out = [_GroupTuple(output_expr(values[0]), values) for _, values in out]
+
+    return out
+
 
 @async_variant(sync_do_groupby)  # type: ignore
 async def do_groupby(
@@ -1222,13 +1247,26 @@ async def do_groupby(
     value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]",
     attribute: t.Union[str, int],
     default: t.Optional[t.Any] = None,
-) -> "t.List[t.Tuple[t.Any, t.List[V]]]":
-    expr = make_attrgetter(environment, attribute, default=default)
-    return [
+    case_sensitive: bool = False,
+) -> "t.List[_GroupTuple]":
+    expr = make_attrgetter(
+        environment,
+        attribute,
+        postprocess=ignore_case if not case_sensitive else None,
+        default=default,
+    )
+    out = [
         _GroupTuple(key, await auto_to_list(values))
         for key, values in groupby(sorted(await auto_to_list(value), key=expr), expr)
     ]
 
+    if not case_sensitive:
+        # Return the real key from the first value instead of the lowercase key.
+        output_expr = make_attrgetter(environment, attribute, default=default)
+        out = [_GroupTuple(output_expr(values[0]), values) for _, values in out]
+
+    return out
+
 
 @pass_environment
 def sync_do_sum(
index 5d4f332e5de17b82ce3fc990b2ef268ab702ae37..f5b2627ad8778a0bed311515f2c5f8db3b3c674b 100644 (file)
@@ -57,6 +57,26 @@ def test_groupby(env_async, items):
     ]
 
 
+@pytest.mark.parametrize(
+    ("case_sensitive", "expect"),
+    [
+        (False, "a: 1, 3\nb: 2\n"),
+        (True, "A: 3\na: 1\nb: 2\n"),
+    ],
+)
+def test_groupby_case(env_async, case_sensitive, expect):
+    tmpl = env_async.from_string(
+        "{% for k, vs in data|groupby('k', case_sensitive=cs) %}"
+        "{{ k }}: {{ vs|join(', ', attribute='v') }}\n"
+        "{% endfor %}"
+    )
+    out = tmpl.render(
+        data=[{"k": "a", "v": 1}, {"k": "b", "v": 2}, {"k": "A", "v": 3}],
+        cs=case_sensitive,
+    )
+    assert out == expect
+
+
 @mark_dualiter("items", lambda: [("a", 1), ("a", 2), ("b", 1)])
 def test_groupby_tuple_index(env_async, items):
     tmpl = env_async.from_string(
index 43ddf59cfdad9896015b466651fc148fa8a89e33..73f0f0be3cc2f516ab2396993536c0523cbf2bee 100644 (file)
@@ -619,6 +619,25 @@ class TestFilter:
         )
         assert out == "NY: emma, john\nWA: smith\n"
 
+    @pytest.mark.parametrize(
+        ("case_sensitive", "expect"),
+        [
+            (False, "a: 1, 3\nb: 2\n"),
+            (True, "A: 3\na: 1\nb: 2\n"),
+        ],
+    )
+    def test_groupby_case(self, env, case_sensitive, expect):
+        tmpl = env.from_string(
+            "{% for k, vs in data|groupby('k', case_sensitive=cs) %}"
+            "{{ k }}: {{ vs|join(', ', attribute='v') }}\n"
+            "{% endfor %}"
+        )
+        out = tmpl.render(
+            data=[{"k": "a", "v": 1}, {"k": "b", "v": 2}, {"k": "A", "v": 3}],
+            cs=case_sensitive,
+        )
+        assert out == expect
+
     def test_filtertag(self, env):
         tmpl = env.from_string(
             "{% filter upper|replace('FOO', 'foo') %}foobar{% endfilter %}"