Skip to content

Commit

Permalink
Added unique filter
Browse files Browse the repository at this point in the history
  • Loading branch information
snoack committed Aug 10, 2015
1 parent 6bf78bc commit 0717c00
Show file tree
Hide file tree
Showing 2 changed files with 79 additions and 25 deletions.
76 changes: 58 additions & 18 deletions jinja2/filters.py
Original file line number Diff line number Diff line change
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,54 @@ 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
if not case_sensitive or attribute is not None:
key_func = make_attrgetter(environment, attribute, not case_sensitive)
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 = None
return sorted(value, key=key_func, reverse=reverse)


@environmentfilter
def do_unique(environment, value, case_sensitive=False, attribute=None):
"""Returns a list of unique items from the the given iterable.
.. sourcecode:: jinja
{{ ['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 themself instead and always returns a flat list of
unique items. That can be useuful for example when you need to concatenate
that items:
.. 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 occurence 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']
"""
getter = make_attrgetter(environment, attribute, not case_sensitive)

seen = set()
rv = []

for item in value:
key = getter(item)
if key not in seen:
seen.add(key)
rv.append(item)

return rv


def do_default(value, default_value=u'', boolean=False):
Expand Down Expand Up @@ -987,6 +1026,7 @@ def _select_or_reject(args, kwargs, modfunc, lookup_attr):
'title': do_title,
'trim': do_trim,
'truncate': do_truncate,
'unique': do_unique,
'upper': do_upper,
'urlencode': do_urlencode,
'urlize': do_urlize,
Expand Down
28 changes: 21 additions & 7 deletions tests/test_filters.py
Original file line number Diff line number Diff line change
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,21 @@ 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_unique1(self, env):
tmpl = env.from_string('{{ "".join(["b", "A", "a", "b"]|unique) }}')
assert tmpl.render() == "bA"

def test_unique2(self, env):
tmpl = env.from_string('{{ "".join(["b", "A", "a", "b"]|unique(true)) }}')
assert tmpl.render() == "bAa"

def test_unique3(self, env):
tmpl = env.from_string("{{ items|unique(attribute='value')|join }}")
assert tmpl.render(items=map(Magic, [3, 2, 4, 1, 2])) == '3241'

def test_groupby(self, env):
tmpl = env.from_string('''
{%- for grouper, list in [{'foo': 1, 'bar': 2},
Expand Down

0 comments on commit 0717c00

Please sign in to comment.