diff options
author | David Lord <davidism@gmail.com> | 2022-03-08 06:57:02 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-03-08 06:57:02 -0800 |
commit | a12ad40d56302c90ccb72471ff34ad320c070ed0 (patch) | |
tree | 554c8d0fb61dac24efdce10f21e31d6681367b0e | |
parent | 20eb7f5e33120f1717b78bdf85540ab75631187a (diff) | |
parent | e690f7c5962aea8de43cd09689dcca12bcf29014 (diff) | |
download | jinja2-a12ad40d56302c90ccb72471ff34ad320c070ed0.tar.gz |
Merge pull request #1465 from wombatonfire/groupby-case-sensitive
Add case_sensitive parameter to groupby() filter
-rw-r--r-- | CHANGES.rst | 3 | ||||
-rw-r--r-- | src/jinja2/filters.py | 50 | ||||
-rw-r--r-- | tests/test_async_filters.py | 20 | ||||
-rw-r--r-- | tests/test_filters.py | 19 |
4 files changed, 86 insertions, 6 deletions
diff --git a/CHANGES.rst b/CHANGES.rst index d67381b..88e167d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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 diff --git a/src/jinja2/filters.py b/src/jinja2/filters.py index 80ea650..7e09709 100644 --- a/src/jinja2/filters.py +++ b/src/jinja2/filters.py @@ -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( diff --git a/tests/test_async_filters.py b/tests/test_async_filters.py index 5d4f332..f5b2627 100644 --- a/tests/test_async_filters.py +++ b/tests/test_async_filters.py @@ -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( diff --git a/tests/test_filters.py b/tests/test_filters.py index 43ddf59..73f0f0b 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -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 %}" |