Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added min and max filters #475

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
84 changes: 64 additions & 20 deletions jinja2/filters.py
Expand Up @@ -13,7 +13,7 @@

from random import choice
from operator import itemgetter
from itertools import groupby
from itertools import groupby, chain
from jinja2.utils import Markup, escape, pformat, urlize, soft_unicode, \
unicode_urlencode
from jinja2.runtime import Undefined
Expand Down Expand Up @@ -51,21 +51,24 @@ def environmentfilter(f):
return f


def make_attrgetter(environment, attribute):
def make_attrgetter(environment, attribute, lowercase=False):
"""Returns a callable that looks up the given attribute from a
passed object with the rules of the environment. Dots are allowed
to access attributes of attributes. Integer parts in paths are
looked up as integers.
"""
if not isinstance(attribute, string_types) \
or ('.' not in attribute and not attribute.isdigit()):
return lambda x: environment.getitem(x, attribute)
attribute = attribute.split('.')
if attribute is None:
attribute = []
elif isinstance(attribute, string_types):
attribute = [int(x) if x.isdigit() else x for x in attribute.split('.')]
else:
attribute = [attribute]

def attrgetter(item):
for part in attribute:
if part.isdigit():
part = int(part)
item = environment.getitem(item, part)
if lowercase and isinstance(item, string_types):
item = item.lower()
return item
return attrgetter

Expand Down Expand Up @@ -251,18 +254,57 @@ def do_sort(environment, value, reverse=False, case_sensitive=False,
.. versionchanged:: 2.6
The `attribute` parameter was added.
"""
if not case_sensitive:
def sort_func(item):
if isinstance(item, string_types):
item = item.lower()
return item
else:
sort_func = None
if attribute is not None:
getter = make_attrgetter(environment, attribute)
def sort_func(item, processor=sort_func or (lambda x: x)):
return processor(getter(item))
return sorted(value, key=sort_func, reverse=reverse)
key_func = make_attrgetter(environment, attribute, not case_sensitive)
return sorted(value, key=key_func, reverse=reverse)


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):
Expand Down Expand Up @@ -969,6 +1011,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,
Expand Down
40 changes: 33 additions & 7 deletions tests/test_filters.py
Expand Up @@ -13,6 +13,15 @@
from jinja2._compat import text_type, implements_to_string


@implements_to_string
class Magic(object):
def __init__(self, value):
self.value = value

def __str__(self):
return text_type(self.value)


@pytest.mark.filter
class TestFilter():

Expand Down Expand Up @@ -348,16 +357,33 @@ def test_sort3(self, env):
assert tmpl.render() == "['Bar', 'blah', 'foo']"

def test_sort4(self, env):
@implements_to_string
class Magic(object):
def __init__(self, value):
self.value = value

def __str__(self):
return text_type(self.value)
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_groupby(self, env):
tmpl = env.from_string('''
{%- for grouper, list in [{'foo': 1, 'bar': 2},
Expand Down