From 384655e15c7e36e5b4c56578c534053404f9f1d1 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Thu, 13 May 2021 21:49:57 +0200 Subject: [PATCH] Add groupby_as_dict filter (#2323) * Add groupby_as_dict filter. * Test all error cases. --- .../fragments/2323-groupby_as_dict-filter.yml | 3 ++ plugins/filter/groupby.py | 42 +++++++++++++++++ .../targets/filter_groupby/aliases | 2 + .../targets/filter_groupby/tasks/main.yml | 45 +++++++++++++++++++ .../targets/filter_groupby/vars/main.yml | 31 +++++++++++++ 5 files changed, 123 insertions(+) create mode 100644 changelogs/fragments/2323-groupby_as_dict-filter.yml create mode 100644 plugins/filter/groupby.py create mode 100644 tests/integration/targets/filter_groupby/aliases create mode 100644 tests/integration/targets/filter_groupby/tasks/main.yml create mode 100644 tests/integration/targets/filter_groupby/vars/main.yml diff --git a/changelogs/fragments/2323-groupby_as_dict-filter.yml b/changelogs/fragments/2323-groupby_as_dict-filter.yml new file mode 100644 index 0000000000..e72f323a60 --- /dev/null +++ b/changelogs/fragments/2323-groupby_as_dict-filter.yml @@ -0,0 +1,3 @@ +add plugin.filter: + - name: groupby_as_dict + description: Transform a sequence of dictionaries to a dictionary where the dictionaries are indexed by an attribute diff --git a/plugins/filter/groupby.py b/plugins/filter/groupby.py new file mode 100644 index 0000000000..a2a85aa905 --- /dev/null +++ b/plugins/filter/groupby.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Felix Fontein +# 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 AnsibleFilterError +from ansible.module_utils.common._collections_compat import Mapping, Sequence + + +def groupby_as_dict(sequence, attribute): + ''' + Given a sequence of dictionaries and an attribute name, returns a dictionary mapping + the value of this attribute to the dictionary. + + If multiple dictionaries in the sequence have the same value for this attribute, + the filter will fail. + ''' + if not isinstance(sequence, Sequence): + raise AnsibleFilterError('Input is not a sequence') + + result = dict() + for list_index, element in enumerate(sequence): + if not isinstance(element, Mapping): + raise AnsibleFilterError('Sequence element #{0} is not a mapping'.format(list_index)) + if attribute not in element: + raise AnsibleFilterError('Attribute not contained in element #{0} of sequence'.format(list_index)) + result_index = element[attribute] + if result_index in result: + raise AnsibleFilterError('Multiple sequence entries have attribute value {0!r}'.format(result_index)) + result[result_index] = element + return result + + +class FilterModule(object): + ''' Ansible list filters ''' + + def filters(self): + return { + 'groupby_as_dict': groupby_as_dict, + } diff --git a/tests/integration/targets/filter_groupby/aliases b/tests/integration/targets/filter_groupby/aliases new file mode 100644 index 0000000000..6e79abdd02 --- /dev/null +++ b/tests/integration/targets/filter_groupby/aliases @@ -0,0 +1,2 @@ +shippable/posix/group4 +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_groupby/tasks/main.yml b/tests/integration/targets/filter_groupby/tasks/main.yml new file mode 100644 index 0000000000..29036a3bc5 --- /dev/null +++ b/tests/integration/targets/filter_groupby/tasks/main.yml @@ -0,0 +1,45 @@ +--- +- name: Test functionality + assert: + that: + - list1 | community.general.groupby_as_dict('name') == dict1 + +- name: 'Test error: not a list' + set_fact: + test: "{{ list_no_list | community.general.groupby_as_dict('name') }}" + ignore_errors: true + register: result + +- assert: + that: + - result.msg == 'Input is not a sequence' + +- name: 'Test error: list element not a mapping' + set_fact: + test: "{{ list_no_dict | community.general.groupby_as_dict('name') }}" + ignore_errors: true + register: result + +- assert: + that: + - "result.msg == 'Sequence element #0 is not a mapping'" + +- name: 'Test error: list element does not have attribute' + set_fact: + test: "{{ list_no_attribute | community.general.groupby_as_dict('name') }}" + ignore_errors: true + register: result + +- assert: + that: + - "result.msg == 'Attribute not contained in element #1 of sequence'" + +- name: 'Test error: attribute collision' + set_fact: + test: "{{ list_collision | community.general.groupby_as_dict('name') }}" + ignore_errors: true + register: result + +- assert: + that: + - result.msg == "Multiple sequence entries have attribute value 'a'" diff --git a/tests/integration/targets/filter_groupby/vars/main.yml b/tests/integration/targets/filter_groupby/vars/main.yml new file mode 100644 index 0000000000..15d38a351a --- /dev/null +++ b/tests/integration/targets/filter_groupby/vars/main.yml @@ -0,0 +1,31 @@ +--- +list1: + - name: a + x: y + - name: b + z: 1 + +dict1: + a: + name: a + x: y + b: + name: b + z: 1 + +list_no_list: + a: + name: a + +list_no_dict: + - [] + - 1 + +list_no_attribute: + - name: a + foo: baz + - foo: bar + +list_collision: + - name: a + - name: a