From 5435d35f97d1e74eba2854651b8ef7258f31292b Mon Sep 17 00:00:00 2001 From: Sebastian Noack Date: Tue, 4 Aug 2015 22:11:55 +0200 Subject: [PATCH 1/2] Added min and max filters --- jinja2/filters.py | 53 ++++++++++++++++++++++++++++++++++++++++++- tests/test_filters.py | 24 ++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/jinja2/filters.py b/jinja2/filters.py index c0d105bd0..cdd12774a 100644 --- a/jinja2/filters.py +++ b/jinja2/filters.py @@ -12,7 +12,7 @@ import math import random -from itertools import groupby +from itertools import groupby, chain from collections import namedtuple from jinja2.utils import Markup, escape, pformat, urlize, soft_unicode, \ unicode_urlencode, htmlsafe_json_dumps @@ -313,6 +313,55 @@ def do_unique(environment, value, case_sensitive=False, attribute=None): yield item +def _min_or_max(func, value, environment, attribute, case_sensitive): + it = iter(value) + try: + first = next(it) + except StopIteration: + return environment.undefined('No aggregated item, sequence was empty') + + key_func = make_attrgetter(environment, attribute, not case_sensitive) + return func(chain([first], it), key=key_func) + + +@environmentfilter +def do_min(environment, value, attribute=None, case_sensitive=False): + """Return the smallest item from the sequence. + + .. sourcecode:: jinja + + {{ [1, 2, 3]|min }} + -> 1 + + It is also possible to get the item providing the smallest value for a + certain attribute: + + .. sourcecode:: jinja + + {{ users|min('last_login') }} + """ + return _min_or_max(min, value, environment, attribute, case_sensitive) + + +@environmentfilter +def do_max(environment, value, attribute=None, case_sensitive=False): + """Return the largest item from the sequence. + + .. sourcecode:: jinja + + {{ [1, 2, 3]|max }} + -> 3 + + It is also possible to get the item providing the largest value for a + certain attribute: + + .. sourcecode:: jinja + + {{ users|max('last_login') }} + """ + return _min_or_max(max, value, environment, attribute, case_sensitive) + + def do_default(value, default_value=u'', boolean=False): """If the value is undefined it will return the passed default value, otherwise the value of the variable: @@ -1097,6 +1146,8 @@ def select_or_reject(args, kwargs, modfunc, lookup_attr): 'list': do_list, 'lower': do_lower, 'map': do_map, + 'min': do_min, + 'max': do_max, 'pprint': do_pprint, 'random': do_random, 'reject': do_reject, diff --git a/tests/test_filters.py b/tests/test_filters.py index 01e4c3c15..9c4504a73 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -391,6 +391,30 @@ def test_sort4(self, env): tmpl = env.from_string('''{{ items|sort(attribute='value')|join }}''') assert tmpl.render(items=map(Magic, [3, 2, 4, 1])) == '1234' + def test_min1(self, env): + tmpl = env.from_string('{{ ["a", "B"]|min }}') + assert tmpl.render() == 'a' + + def test_min2(self, env): + tmpl = env.from_string('{{ []|min }}') + assert tmpl.render() == '' + + def test_min3(self, env): + tmpl = env.from_string('{{ items|min("value") }}') + assert tmpl.render(items=map(Magic, [5, 1, 9])) == '1' + + def test_max1(self, env): + tmpl = env.from_string('{{ ["a", "B"]|max }}') + assert tmpl.render() == 'B' + + def test_max2(self, env): + tmpl = env.from_string('{{ []|max }}') + assert tmpl.render() == '' + + def test_max3(self, env): + tmpl = env.from_string('{{ items|max("value") }}') + assert tmpl.render(items=map(Magic, [5, 9, 1])) == '9' + def test_unique(self, env): t = env.from_string('{{ "".join(["b", "A", "a", "b"]|unique) }}') assert t.render() == "bA" From 52dcb4753ac0f33ca9cd1027fb8831fe86af8fc4 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 6 Jul 2017 08:37:17 -0700 Subject: [PATCH 2/2] parametrize tests argument order consistent with existing filters add changelog --- CHANGES | 2 ++ jinja2/filters.py | 58 +++++++++++++++---------------------------- tests/test_filters.py | 44 +++++++++++++++----------------- 3 files changed, 42 insertions(+), 62 deletions(-) diff --git a/CHANGES b/CHANGES index cc43aa55e..31a5bb8aa 100644 --- a/CHANGES +++ b/CHANGES @@ -25,8 +25,10 @@ Version 2.10 - The ``random`` filter is no longer incorrectly constant folded and will produce a new random choice each time the template is rendered. (`#478`_) - Add a ``unique`` filter. (`#469`_) +- Add ``min`` and ``max`` filters. (`#475`_) .. _#469: https://github.com/pallets/jinja/pull/469 +.. _#475: https://github.com/pallets/jinja/pull/475 .. _#478: https://github.com/pallets/jinja/pull/478 Version 2.9.6 diff --git a/jinja2/filters.py b/jinja2/filters.py index cdd12774a..cb3772832 100644 --- a/jinja2/filters.py +++ b/jinja2/filters.py @@ -279,25 +279,11 @@ def do_unique(environment, value, case_sensitive=False, attribute=None): {{ ['foo', 'bar', 'foobar', 'FooBar']|unique }} -> ['foo', 'bar', 'foobar'] - This filter complements the `groupby` filter, which sorts and groups an - iterable by a certain attribute. The `unique` filter groups the items - from the iterable by themselves instead and always returns a flat list of - unique items. That can be useful for example when you need to concatenate - that items: + The unique items are yielded in the same order as their first occurrence in + the iterable passed to the filter. - .. sourcecode:: jinja - - {{ ['foo', 'bar', 'foobar', 'FooBar']|unique|join(',') }} - -> foo,bar,foobar - - Also note that the resulting list contains the items in the same order - as their first occurrence in the iterable passed to the filter. If sorting - is needed you can still chain the `unique` and `sort` filter: - - .. sourcecode:: jinja - - {{ ['foo', 'bar', 'foobar', 'FooBar']|unique|sort }} - -> ['bar', 'foo', 'foobar'] + :param case_sensitive: Treat upper and lower case strings as distinct. + :param attribute: Filter objects with unique values for this attribute. """ getter = make_attrgetter( environment, attribute, @@ -313,19 +299,23 @@ def do_unique(environment, value, case_sensitive=False, attribute=None): yield item -def _min_or_max(func, value, environment, attribute, case_sensitive): +def _min_or_max(environment, value, func, case_sensitive, attribute): it = iter(value) + try: first = next(it) except StopIteration: - return environment.undefined('No aggregated item, sequence was empty') + return environment.undefined('No aggregated item, sequence was empty.') - key_func = make_attrgetter(environment, attribute, not case_sensitive) + key_func = make_attrgetter( + environment, attribute, + ignore_case if not case_sensitive else None + ) return func(chain([first], it), key=key_func) @environmentfilter -def do_min(environment, value, attribute=None, case_sensitive=False): +def do_min(environment, value, case_sensitive=False, attribute=None): """Return the smallest item from the sequence. .. sourcecode:: jinja @@ -333,33 +323,25 @@ def do_min(environment, value, attribute=None, case_sensitive=False): {{ [1, 2, 3]|min }} -> 1 - It is also possible to get the item providing the smallest value for a - certain attribute: - - .. sourcecode:: jinja - - {{ users|min('last_login') }} + :param case_sensitive: Treat upper and lower case strings as distinct. + :param attribute: Get the object with the max value of this attribute. """ - return _min_or_max(min, value, environment, attribute, case_sensitive) + return _min_or_max(environment, value, min, case_sensitive, attribute) @environmentfilter -def do_max(environment, value, attribute=None, case_sensitive=False): - """Return the largest item from the sequence. +def do_max(environment, value, case_sensitive=False, attribute=None): + """Return the smallest item from the sequence. .. sourcecode:: jinja {{ [1, 2, 3]|max }} -> 3 - It is also possible to get the item providing the largest value for a - certain attribute: - - .. sourcecode:: jinja - - {{ users|max('last_login') }} + :param case_sensitive: Treat upper and lower case strings as distinct. + :param attribute: Get the object with the max value of this attribute. """ - return _min_or_max(max, value, environment, attribute, case_sensitive) + return _min_or_max(environment, value, max, case_sensitive, attribute) def do_default(value, default_value=u'', boolean=False): diff --git a/tests/test_filters.py b/tests/test_filters.py index 9c4504a73..84e77d9d4 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -391,30 +391,6 @@ def test_sort4(self, env): tmpl = env.from_string('''{{ items|sort(attribute='value')|join }}''') assert tmpl.render(items=map(Magic, [3, 2, 4, 1])) == '1234' - def test_min1(self, env): - tmpl = env.from_string('{{ ["a", "B"]|min }}') - assert tmpl.render() == 'a' - - def test_min2(self, env): - tmpl = env.from_string('{{ []|min }}') - assert tmpl.render() == '' - - def test_min3(self, env): - tmpl = env.from_string('{{ items|min("value") }}') - assert tmpl.render(items=map(Magic, [5, 1, 9])) == '1' - - def test_max1(self, env): - tmpl = env.from_string('{{ ["a", "B"]|max }}') - assert tmpl.render() == 'B' - - def test_max2(self, env): - tmpl = env.from_string('{{ []|max }}') - assert tmpl.render() == '' - - def test_max3(self, env): - tmpl = env.from_string('{{ items|max("value") }}') - assert tmpl.render(items=map(Magic, [5, 9, 1])) == '9' - def test_unique(self, env): t = env.from_string('{{ "".join(["b", "A", "a", "b"]|unique) }}') assert t.render() == "bA" @@ -427,6 +403,26 @@ def test_unique_attribute(self, env): t = env.from_string("{{ items|unique(attribute='value')|join }}") assert t.render(items=map(Magic, [3, 2, 4, 1, 2])) == '3241' + @pytest.mark.parametrize('source,expect', ( + ('{{ ["a", "B"]|min }}', 'a'), + ('{{ ["a", "B"]|min(case_sensitive=true) }}', 'B'), + ('{{ []|min }}', ''), + ('{{ ["a", "B"]|max }}', 'B'), + ('{{ ["a", "B"]|max(case_sensitive=true) }}', 'a'), + ('{{ []|max }}', ''), + )) + def test_min_max(self, env, source, expect): + t = env.from_string(source) + assert t.render() == expect + + @pytest.mark.parametrize('name,expect', ( + ('min', '1'), + ('max', '9'), + )) + def test_min_max_attribute(self, env, name, expect): + t = env.from_string('{{ items|' + name + '(attribute="value") }}') + assert t.render(items=map(Magic, [5, 1, 9])) == expect + def test_groupby(self, env): tmpl = env.from_string(''' {%- for grouper, list in [{'foo': 1, 'bar': 2},