From 4975ca03622caf598cacb6ef1ab971496235d5be Mon Sep 17 00:00:00 2001 From: Sebastian Noack Date: Tue, 4 Aug 2015 22:11:55 +0200 Subject: [PATCH] Added unique filter --- jinja2/filters.py | 75 ++++++++++++++++++++++++++++++++----------- tests/test_filters.py | 28 ++++++++++++---- 2 files changed, 77 insertions(+), 26 deletions(-) diff --git a/jinja2/filters.py b/jinja2/filters.py index e5c7a1ab4..0f7762d85 100644 --- a/jinja2/filters.py +++ b/jinja2/filters.py @@ -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 @@ -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) + + return rv def do_default(value, default_value=u'', boolean=False): @@ -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, diff --git a/tests/test_filters.py b/tests/test_filters.py index 741ef341b..d6c5a759c 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -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(): @@ -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},