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

Add support for merging list with dict #256

Merged
merged 5 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 63 additions & 2 deletions docs/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,72 @@ Append a ``+`` sign to the attribute name to add given value::
/download:
time+: 3

This operation is possible only for attributes of the same type.
Exception ``MergeError`` is raised if types are different. When
This operation is possible only for attributes of the same type
or when merging a dictionary with a list.
Exception ``MergeError`` is raised if types are not compatible. When
the ``+`` suffix is applied on dictionaries ``update()`` method is
used to merge content of given dictionary instead of replacing it.

Example: Merging dictionary with a list::

discover:
how: fmf
filter: "tier:1"

/path:
discover+:
- name: upstream
url: https://some.url
- name: downstream
url: https://other.url

results in::

/path:
discover:
- name: upstream
url: https://some.url
how: fmf
filter: "tier:1"
- name: downstream
url: https://other.url
how: fmf
filter: "tier:1"

Example: Merging list with a dictionary::

discover:
- how: fmf
url: https://github.com/project1
- how: fmf
url: https://github.com/project2

/tier1:
discover+:
filter: "tier:1"
/tier2:
discover+:
filter: "tier:2"

results in::

/tier1:
discover:
- how: fmf
url: https://github.com/project1
filter: "tier:1"
- how: fmf
url: https://github.com/project2
filter: "tier:1"
/tier2:
discover:
- how: fmf
url: https://github.com/project1
filter: "tier:2"
- how: fmf
url: https://github.com/project2
filter: "tier:2"

The special suffix ``+<`` can be used to prepend values instead of
appending them. This might be handy when adjusting lists::

Expand Down
12 changes: 12 additions & 0 deletions examples/merge/parent-dict.fmf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
discover:
how: fmf
summary: test.

/path:
discover+:
- name: upstream
url: https://some.url
summary+: upstream
- name: downstream
url: https://other.url
summary+: downstream
19 changes: 19 additions & 0 deletions examples/merge/parent-list.fmf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
discover:
- how: fmf
url: https://github.com/project1
summary: project1.
- how: fmf
url: https://github.com/project2
summary: project2.

/tier1:
summary: basic tests
discover+:
filter: "tier: 1"
summary+: tier1

/tier2:
summary: detailed tests
discover+:
filter: "tier: 2"
summary+: tier2
29 changes: 29 additions & 0 deletions fmf/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,14 +172,43 @@ def _initialize(self, path):

def _merge_plus(self, data, key, value, prepend=False):
""" Handle extending attributes using the '+' suffix """

# Nothing to do if key not in parent
if key not in data:
data[key] = value
return

# Parent is a list of dict and child is a dict
# (just update every parent list item using the special merge)
if isinstance(data[key], list) and isinstance(value, dict):
for list_item in data[key]:
if not isinstance(list_item, dict):
raise utils.MergeError(
"MergeError: Item '{0}' in {1} must be a dictionary.".format(
list_item, self.name))
self._merge_special(list_item, value)
return

# Parent is a dict and child is a list of dict
# (replace parent dict with the list of its special-merged copies)
if isinstance(data[key], dict) and isinstance(value, list):
result_list = []
for list_item in value:
if not isinstance(list_item, dict):
raise utils.MergeError(
"MergeError: Item '{0}' in {1} must be a dictionary.".format(
list_item, self.name))
result_dict = copy.deepcopy(data[key])
self._merge_special(result_dict, list_item)
result_list.append(result_dict)
data[key] = result_list
return

# Use the special merge for merging dictionaries
if type(data[key]) == type(value) == dict:
self._merge_special(data[key], value)
return

# Attempt to apply the plus operator
try:
if prepend:
Expand Down
26 changes: 26 additions & 0 deletions tests/unit/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,32 @@ def test_merge_plus(self):
child.data["time+"] = "string"
child.inherit()

def test_merge_plus_parent_dict(self):
""" Merging parent dict with child list """
child = self.merge.find('/parent-dict/path')
assert len(child.data['discover']) == 2
assert child.data['discover'][0]['how'] == 'fmf'
assert child.data['discover'][0]['name'] == 'upstream'
assert child.data['discover'][0]['url'] == 'https://some.url'
assert child.data['discover'][0]['summary'] == 'test.upstream'
assert child.data['discover'][1]['how'] == 'fmf'
assert child.data['discover'][1]['name'] == 'downstream'
assert child.data['discover'][1]['url'] == 'https://other.url'
assert child.data['discover'][1]['summary'] == 'test.downstream'

def test_merge_plus_parent_list(self):
""" Merging parent list with child dict """
for i in [1, 2]:
child = self.merge.find(f'/parent-list/tier{i}')
assert child.data['summary'] == 'basic tests' if i == 1 else 'detailed tests'
assert len(child.data['discover']) == 2
assert child.data['discover'][0]['filter'] == f'tier: {i}'
assert child.data['discover'][0]['url'] == 'https://github.com/project1'
assert child.data['discover'][0]['summary'] == f'project1.tier{i}'
assert child.data['discover'][1]['filter'] == f'tier: {i}'
assert child.data['discover'][1]['url'] == 'https://github.com/project2'
assert child.data['discover'][1]['summary'] == f'project2.tier{i}'

def test_merge_minus(self):
""" Reducing attributes using the '-' suffix """
child = self.merge.find('/parent/reduced')
Expand Down