summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Lord <davidism@gmail.com>2022-03-08 06:57:02 -0800
committerGitHub <noreply@github.com>2022-03-08 06:57:02 -0800
commita12ad40d56302c90ccb72471ff34ad320c070ed0 (patch)
tree554c8d0fb61dac24efdce10f21e31d6681367b0e
parent20eb7f5e33120f1717b78bdf85540ab75631187a (diff)
parente690f7c5962aea8de43cd09689dcca12bcf29014 (diff)
downloadjinja2-a12ad40d56302c90ccb72471ff34ad320c070ed0.tar.gz
Merge pull request #1465 from wombatonfire/groupby-case-sensitive
Add case_sensitive parameter to groupby() filter
-rw-r--r--CHANGES.rst3
-rw-r--r--src/jinja2/filters.py50
-rw-r--r--tests/test_async_filters.py20
-rw-r--r--tests/test_filters.py19
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 %}"