diff --git a/changelogs/fragments/604-lists_mergeby-new-filter.yml b/changelogs/fragments/604-lists_mergeby-new-filter.yml new file mode 100644 index 0000000000..9f2d19c99f --- /dev/null +++ b/changelogs/fragments/604-lists_mergeby-new-filter.yml @@ -0,0 +1,23 @@ +--- +minor_changes: + - | + A new filter ``lists_mergeby`` to merge two lists of dictionaries by an attribute. + For example: + + .. code-block:: yaml + + [{'n': 'n1', 'p1': 'A', 'p2': 'F'}, + {'n': 'n2', 'p2': 'B'}] | community.general.lists_mergeby( + [{'n': 'n1', 'p1': 'C'}, + {'n': 'n2', 'p2': 'D'}, + {'n': 'n3', 'p3': 'E'}], 'n') | list + + evaluates to + + .. code-block:: yaml + + [{'n': 'n1', 'p1': 'C', 'p2': 'F'}, + {'n': 'n2', 'p2': 'D'}, + {'n': 'n3', 'p3': 'E'}] + + (https://github.com/ansible-collections/community.general/pull/604). diff --git a/plugins/filter/list.py b/plugins/filter/list.py new file mode 100644 index 0000000000..460e45194f --- /dev/null +++ b/plugins/filter/list.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Vladimir Botka +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from ansible.errors import AnsibleError, AnsibleFilterError +from ansible.module_utils.six import string_types +from ansible.module_utils.common._collections_compat import Mapping, Sequence +from collections import defaultdict +from operator import itemgetter + + +def lists_mergeby(l1, l2, index): + ''' merge lists by attribute index. Example: + - debug: msg="{{ l1|community.general.lists_mergeby(l2, 'index')|list }}" ''' + + if not isinstance(l1, Sequence): + raise AnsibleFilterError('First argument for community.general.lists_mergeby must be list. %s is %s' % + (l1, type(l1))) + + if not isinstance(l2, Sequence): + raise AnsibleFilterError('Second argument for community.general.lists_mergeby must be list. %s is %s' % + (l2, type(l2))) + + if not isinstance(index, string_types): + raise AnsibleFilterError('Third argument for community.general.lists_mergeby must be string. %s is %s' % + (index, type(index))) + + d = defaultdict(dict) + for l in (l1, l2): + for elem in l: + if not isinstance(elem, Mapping): + raise AnsibleFilterError('Elements of list arguments for lists_mergeby must be dictionaries. Found {0!r}.'.format(elem)) + if index in elem.keys(): + d[elem[index]].update(elem) + return sorted(d.values(), key=itemgetter(index)) + + +class FilterModule(object): + ''' Ansible list filters ''' + + def filters(self): + return { + 'lists_mergeby': lists_mergeby, + } diff --git a/tests/integration/targets/filter_list/aliases b/tests/integration/targets/filter_list/aliases new file mode 100644 index 0000000000..f04737b845 --- /dev/null +++ b/tests/integration/targets/filter_list/aliases @@ -0,0 +1,2 @@ +shippable/posix/group2 +skip/python2.6 # filters are controller only, and we no longer support Python 2.6 on the controller diff --git a/tests/integration/targets/filter_list/tasks/main.yml b/tests/integration/targets/filter_list/tasks/main.yml new file mode 100644 index 0000000000..4abf98c897 --- /dev/null +++ b/tests/integration/targets/filter_list/tasks/main.yml @@ -0,0 +1,6 @@ +--- +- name: Test lists merged by attribute name + assert: + that: + - "(list1 | community.general.lists_mergeby(list2, 'name') | list | + difference(list3) | length) == 0" diff --git a/tests/integration/targets/filter_list/vars/main.yml b/tests/integration/targets/filter_list/vars/main.yml new file mode 100644 index 0000000000..1867653441 --- /dev/null +++ b/tests/integration/targets/filter_list/vars/main.yml @@ -0,0 +1,22 @@ +list1: + - name: myname01 + param01: myparam01 + - name: myname02 + param01: myparam02 + +list2: + - name: myname01 + param01: myparam03 + - name: myname02 + param02: myparam04 + - name: myname03 + param03: myparam03 + +list3: + - name: myname01 + param01: myparam03 + - name: myname02 + param01: myparam02 + param02: myparam04 + - name: myname03 + param03: myparam03