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 unique filter #469

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
75 changes: 56 additions & 19 deletions jinja2/filters.py
Expand Up @@ -51,21 +51,24 @@ def environmentfilter(f):
return f


def make_attrgetter(environment, attribute):
def make_attrgetter(environment, attribute, lowercase=False):

This comment was marked as off-topic.

This comment was marked as off-topic.

"""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,51 @@ 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)


@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)

This comment was marked as off-topic.


return rv


def do_default(value, default_value=u'', boolean=False):
Expand Down Expand Up @@ -987,6 +1023,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
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