Add options to filter lists_mergeby (#4058)

* Update filter lists_mergeby #4057

* Added options 'recursive' and 'list_merge'. The functionality of the
  added options is the same as in the filter 'combine'.
* Allow the user to do [list1, list2, ...]|lists_mergeby('index')
* Use the function merge_hash from ansible.utils.vars

* Add merge_hash_wrapper to test Ansible version

* Enable Ansible 2.9 and lower versions with default options of
  lists_mergeby only.
* Non-default options of lists_mergeby trigger error in 2.9 and lower
  versions.
* Update messages and tests.

* Fix tests.

* Use LooseVersion instead of SpecifierSet.

* Update docs 'Filter Guide' section 'Merging lists of dictionaries'.

* Added changelog fragment.

* Update changelogs/fragments/4058-lists_mergeby-add-parameters.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update docs/docsite/rst/filter_guide.rst

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update docs/docsite/rst/filter_guide.rst

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update docs/docsite/rst/filter_guide.rst

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update docs/docsite/rst/filter_guide.rst

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update docs/docsite/rst/filter_guide.rst

Co-authored-by: Felix Fontein <felix@fontein.de>

* Added examples; moved to rst/examples; fixes.

* Improve error message testing sequence.

* Removed .yamllint

* Update docs/docsite/rst/examples/lists_mergeby/example-003.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update docs/docsite/rst/examples/lists_mergeby/example-004.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update docs/docsite/rst/examples/lists_mergeby/example-005.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update docs/docsite/rst/examples/lists_mergeby/example-006.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update docs/docsite/rst/examples/lists_mergeby/example-007.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update docs/docsite/rst/filter_guide.rst

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update docs/docsite/rst/filter_guide.rst

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update docs/docsite/rst/filter_guide.rst

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update tests/integration/targets/filter_list/tasks/lists_mergeby_default.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update docs/docsite/rst/examples/lists_mergeby/example-008.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

* Fix docs. Antsibull only copies .rst files.

* Fix examples in-line.

* Update docs/docsite/rst/filter_guide.rst

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update docs/docsite/rst/examples/lists_mergeby/examples.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update docs/docsite/rst/examples/lists_mergeby/examples.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update docs/docsite/rst/examples/lists_mergeby/examples.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update docs/docsite/rst/examples/lists_mergeby/examples.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update docs/docsite/rst/examples/lists_mergeby/examples.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update docs/docsite/rst/examples/lists_mergeby/examples.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update docs lists_mergeby. Remove rubbish.

* Emphasized labes of examples in filter_guide.rst
* Removed temporary file examples/lists_mergeby/examples.rst
* Removed tests/integration/targets/filter_list/runme.*

* Fix docs. Description of the lists_merge options.

* Move helper files out of rst/ directory.

Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
Vladimir Botka 2022-01-28 08:19:19 +01:00 committed by GitHub
parent 7ca60e0177
commit 71fb3984db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1426 additions and 123 deletions

View file

@ -0,0 +1,8 @@
{% for i in examples %}
{{ i.label }}
.. code-block:: {{ i.lang }}
{{ lookup('file', source_path ~ i.file)|indent(2) }}
{% endfor %}

View file

@ -0,0 +1,38 @@
---
examples:
- label: 'Example ``list_merge=replace`` (default):'
file: example-003.yml
lang: 'yaml+jinja'
- label: 'This produces:'
file: example-003.out
lang: 'yaml'
- label: 'Example ``list_merge=keep``:'
file: example-004.yml
lang: 'yaml+jinja'
- label: 'This produces:'
file: example-004.out
lang: 'yaml'
- label: 'Example ``list_merge=append``:'
file: example-005.yml
lang: 'yaml+jinja'
- label: 'This produces:'
file: example-005.out
lang: 'yaml'
- label: 'Example ``list_merge=prepend``:'
file: example-006.yml
lang: 'yaml+jinja'
- label: 'This produces:'
file: example-006.out
lang: 'yaml'
- label: 'Example ``list_merge=append_rp``:'
file: example-007.yml
lang: 'yaml+jinja'
- label: 'This produces:'
file: example-007.out
lang: 'yaml'
- label: 'Example ``list_merge=prepend_rp``:'
file: example-008.yml
lang: 'yaml+jinja'
- label: 'This produces:'
file: example-008.out
lang: 'yaml'

View file

@ -0,0 +1,41 @@
---
# The following runs all examples:
#
# ANSIBLE_STDOUT_CALLBACK=community.general.yaml ansible-playbook playbook.yml -e examples=true
#
# You need to copy the YAML output of example-XXX.yml into example-XXX.out.
#
# The following generates examples.rst out of the .out files:
#
# ansible-playbook playbook.yml -e template=true
- hosts: localhost
gather_facts: false
vars:
source_path: ../../rst/examples/lists_mergeby/
tasks:
- block:
- import_tasks: '{{ source_path }}example-001.yml'
tags: t001
- import_tasks: '{{ source_path }}example-002.yml'
tags: t002
- import_tasks: '{{ source_path }}example-003.yml'
tags: t003
- import_tasks: '{{ source_path }}example-004.yml'
tags: t004
- import_tasks: '{{ source_path }}example-005.yml'
tags: t005
- import_tasks: '{{ source_path }}example-006.yml'
tags: t006
- import_tasks: '{{ source_path }}example-007.yml'
tags: t007
- import_tasks: '{{ source_path }}example-008.yml'
tags: t008
when: examples|d(false)|bool
- block:
- include_vars: examples.yml
- template:
src: examples.rst.j2
dest: examples.rst
when: template|d(false)|bool

View file

@ -0,0 +1,10 @@
list3:
- extra: false
name: bar
- name: baz
path: /baz
- extra: true
name: foo
path: /foo
- extra: true
name: meh

View file

@ -0,0 +1,20 @@
---
- name: Merge two lists by common attribute 'name'
set_fact:
list3: "{{ list1|
community.general.lists_mergeby(list2, 'name') }}"
vars:
list1:
- name: foo
extra: true
- name: bar
extra: false
- name: meh
extra: true
list2:
- name: foo
path: /foo
- name: baz
path: /baz
- debug:
var: list3

View file

@ -0,0 +1,10 @@
list3:
- extra: false
name: bar
- name: baz
path: /baz
- extra: true
name: foo
path: /foo
- extra: true
name: meh

View file

@ -0,0 +1,20 @@
---
- name: Merge two lists by common attribute 'name'
set_fact:
list3: "{{ [list1, list2]|
community.general.lists_mergeby('name') }}"
vars:
list1:
- name: foo
extra: true
- name: bar
extra: false
- name: meh
extra: true
list2:
- name: foo
path: /foo
- name: baz
path: /baz
- debug:
var: list3

View file

@ -0,0 +1,14 @@
list3:
- name: myname01
param01:
list:
- patch_value
x: default_value
y: patch_value
z: patch_value
- name: myname02
param01:
- 3
- 4
- 4
- key: value

View file

@ -0,0 +1,28 @@
---
- name: Merge recursive by 'name', replace lists (default)
set_fact:
list3: "{{ [list1, list2]|
community.general.lists_mergeby('name',
recursive=true) }}"
vars:
list1:
- name: myname01
param01:
x: default_value
y: default_value
list:
- default_value
- name: myname02
param01: [1, 1, 2, 3]
list2:
- name: myname01
param01:
y: patch_value
z: patch_value
list:
- patch_value
- name: myname02
param01: [3, 4, 4, {key: value}]
- debug:
var: list3

View file

@ -0,0 +1,14 @@
list3:
- name: myname01
param01:
list:
- default_value
x: default_value
y: patch_value
z: patch_value
- name: myname02
param01:
- 1
- 1
- 2
- 3

View file

@ -0,0 +1,29 @@
---
- name: Merge recursive by 'name', keep lists
set_fact:
list3: "{{ [list1, list2]|
community.general.lists_mergeby('name',
recursive=true,
list_merge='keep') }}"
vars:
list1:
- name: myname01
param01:
x: default_value
y: default_value
list:
- default_value
- name: myname02
param01: [1, 1, 2, 3]
list2:
- name: myname01
param01:
y: patch_value
z: patch_value
list:
- patch_value
- name: myname02
param01: [3, 4, 4, {key: value}]
- debug:
var: list3

View file

@ -0,0 +1,19 @@
list3:
- name: myname01
param01:
list:
- default_value
- patch_value
x: default_value
y: patch_value
z: patch_value
- name: myname02
param01:
- 1
- 1
- 2
- 3
- 3
- 4
- 4
- key: value

View file

@ -0,0 +1,29 @@
---
- name: Merge recursive by 'name', append lists
set_fact:
list3: "{{ [list1, list2]|
community.general.lists_mergeby('name',
recursive=true,
list_merge='append') }}"
vars:
list1:
- name: myname01
param01:
x: default_value
y: default_value
list:
- default_value
- name: myname02
param01: [1, 1, 2, 3]
list2:
- name: myname01
param01:
y: patch_value
z: patch_value
list:
- patch_value
- name: myname02
param01: [3, 4, 4, {key: value}]
- debug:
var: list3

View file

@ -0,0 +1,19 @@
list3:
- name: myname01
param01:
list:
- patch_value
- default_value
x: default_value
y: patch_value
z: patch_value
- name: myname02
param01:
- 3
- 4
- 4
- key: value
- 1
- 1
- 2
- 3

View file

@ -0,0 +1,29 @@
---
- name: Merge recursive by 'name', prepend lists
set_fact:
list3: "{{ [list1, list2]|
community.general.lists_mergeby('name',
recursive=true,
list_merge='prepend') }}"
vars:
list1:
- name: myname01
param01:
x: default_value
y: default_value
list:
- default_value
- name: myname02
param01: [1, 1, 2, 3]
list2:
- name: myname01
param01:
y: patch_value
z: patch_value
list:
- patch_value
- name: myname02
param01: [3, 4, 4, {key: value}]
- debug:
var: list3

View file

@ -0,0 +1,18 @@
list3:
- name: myname01
param01:
list:
- default_value
- patch_value
x: default_value
y: patch_value
z: patch_value
- name: myname02
param01:
- 1
- 1
- 2
- 3
- 4
- 4
- key: value

View file

@ -0,0 +1,29 @@
---
- name: Merge recursive by 'name', append lists 'remove present'
set_fact:
list3: "{{ [list1, list2]|
community.general.lists_mergeby('name',
recursive=true,
list_merge='append_rp') }}"
vars:
list1:
- name: myname01
param01:
x: default_value
y: default_value
list:
- default_value
- name: myname02
param01: [1, 1, 2, 3]
list2:
- name: myname01
param01:
y: patch_value
z: patch_value
list:
- patch_value
- name: myname02
param01: [3, 4, 4, {key: value}]
- debug:
var: list3

View file

@ -0,0 +1,18 @@
list3:
- name: myname01
param01:
list:
- patch_value
- default_value
x: default_value
y: patch_value
z: patch_value
- name: myname02
param01:
- 3
- 4
- 4
- key: value
- 1
- 1
- 2

View file

@ -0,0 +1,29 @@
---
- name: Merge recursive by 'name', prepend lists 'remove present'
set_fact:
list3: "{{ [list1, list2]|
community.general.lists_mergeby('name',
recursive=true,
list_merge='prepend_rp') }}"
vars:
list1:
- name: myname01
param01:
x: default_value
y: default_value
list:
- default_value
- name: myname02
param01: [1, 1, 2, 3]
list2:
- name: myname01
param01:
y: patch_value
z: patch_value
list:
- patch_value
- name: myname02
param01: [3, 4, 4, {key: value}]
- debug:
var: list3

View file

@ -1,3 +1,4 @@
.. _ansible_collections.community.general.docsite.filter_guide:
community.general Filter Guide
@ -247,55 +248,437 @@ This produces:
Merging lists of dictionaries
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If you have two lists of dictionaries and want to combine them into a list of merged dictionaries, where two dictionaries are merged if they coincide in one attribute, you can use the ``lists_mergeby`` filter.
If you have two or more lists of dictionaries and want to combine them into a list of merged dictionaries, where the dictionaries are merged by an attribute, you can use the ``lists_mergeby`` filter.
.. note:: The output of the examples in this section use the YAML callback plugin. Quoting: "Ansible output that can be quite a bit easier to read than the default JSON formatting." See :ref:`the documentation for the community.general.yaml callback plugin <ansible_collections.community.general.yaml_callback>`.
In the example below the lists are merged by the attribute ``name``:
.. code-block:: yaml+jinja
- name: Merge two lists by common attribute 'name'
debug:
var: list1 | community.general.lists_mergeby(list2, 'name')
vars:
list1:
- name: foo
extra: true
- name: bar
extra: false
- name: meh
extra: true
list2:
- name: foo
path: /foo
- name: baz
path: /bazzz
---
- name: Merge two lists by common attribute 'name'
set_fact:
list3: "{{ list1|
community.general.lists_mergeby(list2, 'name') }}"
vars:
list1:
- name: foo
extra: true
- name: bar
extra: false
- name: meh
extra: true
list2:
- name: foo
path: /foo
- name: baz
path: /baz
- debug:
var: list3
This produces:
.. code-block:: ansible-output
.. code-block:: yaml
TASK [Merge two lists by common attribute 'name'] ****************************************
ok: [localhost] => {
"list1 | community.general.lists_mergeby(list2, 'name')": [
{
"extra": false,
"name": "bar"
},
{
"name": "baz",
"path": "/bazzz"
},
{
"extra": true,
"name": "foo",
"path": "/foo"
},
{
"extra": true,
"name": "meh"
}
]
}
list3:
- extra: false
name: bar
- name: baz
path: /baz
- extra: true
name: foo
path: /foo
- extra: true
name: meh
.. versionadded:: 2.0.0
It is possible to use a list of lists as an input of the filter:
.. code-block:: yaml+jinja
---
- name: Merge two lists by common attribute 'name'
set_fact:
list3: "{{ [list1, list2]|
community.general.lists_mergeby('name') }}"
vars:
list1:
- name: foo
extra: true
- name: bar
extra: false
- name: meh
extra: true
list2:
- name: foo
path: /foo
- name: baz
path: /baz
- debug:
var: list3
This produces the same result as in the previous example:
.. code-block:: yaml
list3:
- extra: false
name: bar
- name: baz
path: /baz
- extra: true
name: foo
path: /foo
- extra: true
name: meh
The filter also accepts two optional parameters: ``recursive`` and ``list_merge``. These parameters are only supported when used with ansible-base 2.10 or ansible-core, but not with Ansible 2.9. This is available since community.general 4.4.0.
**recursive**
Is a boolean, default to ``False``. Should the ``community.general.lists_mergeby`` recursively merge nested hashes. Note: It does not depend on the value of the ``hash_behaviour`` setting in ``ansible.cfg``.
**list_merge**
Is a string, its possible values are ``replace`` (default), ``keep``, ``append``, ``prepend``, ``append_rp`` or ``prepend_rp``. It modifies the behaviour of ``community.general.lists_mergeby`` when the hashes to merge contain arrays/lists.
The examples below set ``recursive=true`` and display the differences among all six options of ``list_merge``. Functionality of the parameters is exactly the same as in the filter ``combine``. See :ref:`Combining hashes/dictionaries <combine_filter>` to learn details about these options.
Example ``list_merge=replace`` (default):
.. code-block:: yaml+jinja
---
- name: Merge recursive by 'name', replace lists (default)
set_fact:
list3: "{{ [list1, list2]|
community.general.lists_mergeby('name',
recursive=true) }}"
vars:
list1:
- name: myname01
param01:
x: default_value
y: default_value
list:
- default_value
- name: myname02
param01: [1, 1, 2, 3]
list2:
- name: myname01
param01:
y: patch_value
z: patch_value
list:
- patch_value
- name: myname02
param01: [3, 4, 4, {key: value}]
- debug:
var: list3
This produces:
.. code-block:: yaml
list3:
- name: myname01
param01:
list:
- patch_value
x: default_value
y: patch_value
z: patch_value
- name: myname02
param01:
- 3
- 4
- 4
- key: value
Example ``list_merge=keep``:
.. code-block:: yaml+jinja
---
- name: Merge recursive by 'name', keep lists
set_fact:
list3: "{{ [list1, list2]|
community.general.lists_mergeby('name',
recursive=true,
list_merge='keep') }}"
vars:
list1:
- name: myname01
param01:
x: default_value
y: default_value
list:
- default_value
- name: myname02
param01: [1, 1, 2, 3]
list2:
- name: myname01
param01:
y: patch_value
z: patch_value
list:
- patch_value
- name: myname02
param01: [3, 4, 4, {key: value}]
- debug:
var: list3
This produces:
.. code-block:: yaml
list3:
- name: myname01
param01:
list:
- default_value
x: default_value
y: patch_value
z: patch_value
- name: myname02
param01:
- 1
- 1
- 2
- 3
Example ``list_merge=append``:
.. code-block:: yaml+jinja
---
- name: Merge recursive by 'name', append lists
set_fact:
list3: "{{ [list1, list2]|
community.general.lists_mergeby('name',
recursive=true,
list_merge='append') }}"
vars:
list1:
- name: myname01
param01:
x: default_value
y: default_value
list:
- default_value
- name: myname02
param01: [1, 1, 2, 3]
list2:
- name: myname01
param01:
y: patch_value
z: patch_value
list:
- patch_value
- name: myname02
param01: [3, 4, 4, {key: value}]
- debug:
var: list3
This produces:
.. code-block:: yaml
list3:
- name: myname01
param01:
list:
- default_value
- patch_value
x: default_value
y: patch_value
z: patch_value
- name: myname02
param01:
- 1
- 1
- 2
- 3
- 3
- 4
- 4
- key: value
Example ``list_merge=prepend``:
.. code-block:: yaml+jinja
---
- name: Merge recursive by 'name', prepend lists
set_fact:
list3: "{{ [list1, list2]|
community.general.lists_mergeby('name',
recursive=true,
list_merge='prepend') }}"
vars:
list1:
- name: myname01
param01:
x: default_value
y: default_value
list:
- default_value
- name: myname02
param01: [1, 1, 2, 3]
list2:
- name: myname01
param01:
y: patch_value
z: patch_value
list:
- patch_value
- name: myname02
param01: [3, 4, 4, {key: value}]
- debug:
var: list3
This produces:
.. code-block:: yaml
list3:
- name: myname01
param01:
list:
- patch_value
- default_value
x: default_value
y: patch_value
z: patch_value
- name: myname02
param01:
- 3
- 4
- 4
- key: value
- 1
- 1
- 2
- 3
Example ``list_merge=append_rp``:
.. code-block:: yaml+jinja
---
- name: Merge recursive by 'name', append lists 'remove present'
set_fact:
list3: "{{ [list1, list2]|
community.general.lists_mergeby('name',
recursive=true,
list_merge='append_rp') }}"
vars:
list1:
- name: myname01
param01:
x: default_value
y: default_value
list:
- default_value
- name: myname02
param01: [1, 1, 2, 3]
list2:
- name: myname01
param01:
y: patch_value
z: patch_value
list:
- patch_value
- name: myname02
param01: [3, 4, 4, {key: value}]
- debug:
var: list3
This produces:
.. code-block:: yaml
list3:
- name: myname01
param01:
list:
- default_value
- patch_value
x: default_value
y: patch_value
z: patch_value
- name: myname02
param01:
- 1
- 1
- 2
- 3
- 4
- 4
- key: value
Example ``list_merge=prepend_rp``:
.. code-block:: yaml+jinja
---
- name: Merge recursive by 'name', prepend lists 'remove present'
set_fact:
list3: "{{ [list1, list2]|
community.general.lists_mergeby('name',
recursive=true,
list_merge='prepend_rp') }}"
vars:
list1:
- name: myname01
param01:
x: default_value
y: default_value
list:
- default_value
- name: myname02
param01: [1, 1, 2, 3]
list2:
- name: myname01
param01:
y: patch_value
z: patch_value
list:
- patch_value
- name: myname02
param01: [3, 4, 4, {key: value}]
- debug:
var: list3
This produces:
.. code-block:: yaml
list3:
- name: myname01
param01:
list:
- patch_value
- default_value
x: default_value
y: patch_value
z: patch_value
- name: myname02
param01:
- 3
- 4
- 4
- key: value
- 1
- 1
- 2
.. versionadded: 2.0.0
Counting elements in a sequence
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^