Selectively shallow merge certain mappings or sequences when including configuration files (#672).

This commit is contained in:
Dan Helfman 2023-04-11 21:49:10 -07:00
parent 4c0e2cab78
commit 1ea4433aa9
4 changed files with 374 additions and 70 deletions

3
NEWS
View File

@ -7,6 +7,9 @@
"match_archives" configuration option for the "transfer", "list", "rlist", and "info" actions. "match_archives" configuration option for the "transfer", "list", "rlist", and "info" actions.
* #668: Fix error when running the "prune" action with both "archive_name_format" and "prefix" * #668: Fix error when running the "prune" action with both "archive_name_format" and "prefix"
options set. options set.
* #672: Selectively shallow merge certain mappings or sequences when including configuration files.
See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/#shallow-merge
* #673: View the results of configuration file merging via "validate-borgmatic-config --show" flag. * #673: View the results of configuration file merging via "validate-borgmatic-config --show" flag.
See the documentation for more information: See the documentation for more information:
https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/#debugging-includes https://torsion.org/borgmatic/docs/how-to/make-per-application-backups/#debugging-includes

View File

@ -38,6 +38,24 @@ def include_configuration(loader, filename_node, include_directory):
return load_configuration(include_filename) return load_configuration(include_filename)
def retain_node_error(loader, node):
'''
Given a ruamel.yaml.loader.Loader and a YAML node, raise an error.
Raise ValueError if a mapping or sequence node is given, as that indicates that "!retain" was
used in a configuration file without a merge. In configuration files with a merge, mapping and
sequence nodes with "!retain" tags are handled by deep_merge_nodes() below.
Also raise ValueError if a scalar node is given, as "!retain" is not supported on scalar nodes.
'''
if isinstance(node, (ruamel.yaml.nodes.MappingNode, ruamel.yaml.nodes.SequenceNode)):
raise ValueError(
'The !retain tag may only be used within a configuration file containing a merged !include tag.'
)
raise ValueError('The !retain tag may only be used on a YAML mapping or sequence.')
class Include_constructor(ruamel.yaml.SafeConstructor): class Include_constructor(ruamel.yaml.SafeConstructor):
''' '''
A YAML "constructor" (a ruamel.yaml concept) that supports a custom "!include" tag for including A YAML "constructor" (a ruamel.yaml concept) that supports a custom "!include" tag for including
@ -50,6 +68,7 @@ class Include_constructor(ruamel.yaml.SafeConstructor):
'!include', '!include',
functools.partial(include_configuration, include_directory=include_directory), functools.partial(include_configuration, include_directory=include_directory),
) )
self.add_constructor('!retain', retain_node_error)
def flatten_mapping(self, node): def flatten_mapping(self, node):
''' '''
@ -176,6 +195,8 @@ def deep_merge_nodes(nodes):
), ),
] ]
If a mapping or sequence node has a YAML "!retain" tag, then that node is not merged.
The purpose of deep merging like this is to support, for instance, merging one borgmatic The purpose of deep merging like this is to support, for instance, merging one borgmatic
configuration file into another for reuse, such that a configuration section ("retention", configuration file into another for reuse, such that a configuration section ("retention",
etc.) does not completely replace the corresponding section in a merged file. etc.) does not completely replace the corresponding section in a merged file.
@ -198,32 +219,42 @@ def deep_merge_nodes(nodes):
# If we're dealing with MappingNodes, recurse and merge its values as well. # If we're dealing with MappingNodes, recurse and merge its values as well.
if isinstance(b_value, ruamel.yaml.nodes.MappingNode): if isinstance(b_value, ruamel.yaml.nodes.MappingNode):
replaced_nodes[(b_key, b_value)] = ( # A "!retain" tag says to skip deep merging for this node. Replace the tag so
b_key, # downstream schema validation doesn't break on our application-specific tag.
ruamel.yaml.nodes.MappingNode( if b_value.tag == '!retain':
tag=b_value.tag, b_value.tag = 'tag:yaml.org,2002:map'
value=deep_merge_nodes(a_value.value + b_value.value), else:
start_mark=b_value.start_mark, replaced_nodes[(b_key, b_value)] = (
end_mark=b_value.end_mark, b_key,
flow_style=b_value.flow_style, ruamel.yaml.nodes.MappingNode(
comment=b_value.comment, tag=b_value.tag,
anchor=b_value.anchor, value=deep_merge_nodes(a_value.value + b_value.value),
), start_mark=b_value.start_mark,
) end_mark=b_value.end_mark,
flow_style=b_value.flow_style,
comment=b_value.comment,
anchor=b_value.anchor,
),
)
# If we're dealing with SequenceNodes, merge by appending one sequence to the other. # If we're dealing with SequenceNodes, merge by appending one sequence to the other.
elif isinstance(b_value, ruamel.yaml.nodes.SequenceNode): elif isinstance(b_value, ruamel.yaml.nodes.SequenceNode):
replaced_nodes[(b_key, b_value)] = ( # A "!retain" tag says to skip deep merging for this node. Replace the tag so
b_key, # downstream schema validation doesn't break on our application-specific tag.
ruamel.yaml.nodes.SequenceNode( if b_value.tag == '!retain':
tag=b_value.tag, b_value.tag = 'tag:yaml.org,2002:seq'
value=a_value.value + b_value.value, else:
start_mark=b_value.start_mark, replaced_nodes[(b_key, b_value)] = (
end_mark=b_value.end_mark, b_key,
flow_style=b_value.flow_style, ruamel.yaml.nodes.SequenceNode(
comment=b_value.comment, tag=b_value.tag,
anchor=b_value.anchor, value=a_value.value + b_value.value,
), start_mark=b_value.start_mark,
) end_mark=b_value.end_mark,
flow_style=b_value.flow_style,
comment=b_value.comment,
anchor=b_value.anchor,
),
)
return [ return [
replaced_nodes.get(node, node) for node in nodes if replaced_nodes.get(node) != DELETED_NODE replaced_nodes.get(node, node) for node in nodes if replaced_nodes.get(node) != DELETED_NODE

View File

@ -276,6 +276,65 @@ include, the local file's option takes precedence.
list values are appended together. list values are appended together.
### Shallow merge
Even though deep merging is generally pretty handy for included files,
sometimes you want specific sections in the local file to take precedence over
included sections—without any merging occuring for them.
<span class="minilink minilink-addedin">New in version 1.7.12</span> That's
where the `!retain` tag comes in. Whenever you're merging an included file
into your configuration file, you can optionally add the `!retain` tag to
particular local mappings or sequences to retain the local values and ignore
included values.
For instance, start with this configuration file containing the `!retain` tag
on the `retention` mapping:
```yaml
<<: !include /etc/borgmatic/common.yaml
location:
repositories:
- repo.borg
retention: !retain
keep_daily: 5
```
And `common.yaml` like this:
```yaml
location:
repositories:
- common.borg
retention:
keep_hourly: 24
keep_daily: 7
```
Once this include gets merged in, the resulting configuration will have a
`keep_daily` value of `5` and nothing else in the `retention` section. That's
because the `!retain` tag says to retain the local version of `retention` and
ignore any values coming in from the include. But because the `repositories`
sequence doesn't have a `!retain` tag, that sequence still gets merged
together to contain both `common.borg` and `repo.borg`.
The `!retain` tag can only be placed on mapping and sequence nodes, and it
goes right after the name of the option (and its colon) on the same line. The
effects of `!retain` are recursive, meaning that if you place a `!retain` tag
on a top-level mapping, even deeply nested values within it will not be
merged.
Additionally, the `!retain` tag only works in a configuration file that also
performs a merge include with `<<: !include`. It doesn't make sense within,
for instance, an included configuration file itself (unless it in turn
performs its own merge include). That's because `!retain` only applies to the
file doing the include; it doesn't work in reverse or propagate through
includes.
## Debugging includes ## Debugging includes
<span class="minilink minilink-addedin">New in version 1.7.12</span> If you'd <span class="minilink minilink-addedin">New in version 1.7.12</span> If you'd

View File

@ -2,7 +2,6 @@ import io
import sys import sys
import pytest import pytest
import ruamel.yaml
from flexmock import flexmock from flexmock import flexmock
from borgmatic.config import load as module from borgmatic.config import load as module
@ -150,6 +149,99 @@ def test_load_configuration_merges_include():
assert module.load_configuration('config.yaml') == {'foo': 'override', 'baz': 'quux'} assert module.load_configuration('config.yaml') == {'foo': 'override', 'baz': 'quux'}
def test_load_configuration_with_retain_tag_merges_include_but_keeps_local_values():
builtins = flexmock(sys.modules['builtins'])
flexmock(module.os).should_receive('getcwd').and_return('/tmp')
flexmock(module.os.path).should_receive('isabs').and_return(False)
flexmock(module.os.path).should_receive('exists').and_return(True)
include_file = io.StringIO(
'''
stuff:
foo: bar
baz: quux
other:
a: b
c: d
'''
)
include_file.name = 'include.yaml'
builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
config_file = io.StringIO(
'''
stuff: !retain
foo: override
other:
a: override
<<: !include include.yaml
'''
)
config_file.name = 'config.yaml'
builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
assert module.load_configuration('config.yaml') == {
'stuff': {'foo': 'override'},
'other': {'a': 'override', 'c': 'd'},
}
def test_load_configuration_with_retain_tag_but_without_merge_include_raises():
builtins = flexmock(sys.modules['builtins'])
flexmock(module.os).should_receive('getcwd').and_return('/tmp')
flexmock(module.os.path).should_receive('isabs').and_return(False)
flexmock(module.os.path).should_receive('exists').and_return(True)
include_file = io.StringIO(
'''
stuff: !retain
foo: bar
baz: quux
'''
)
include_file.name = 'include.yaml'
builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
config_file = io.StringIO(
'''
stuff:
foo: override
<<: !include include.yaml
'''
)
config_file.name = 'config.yaml'
builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
with pytest.raises(ValueError):
assert module.load_configuration('config.yaml')
def test_load_configuration_with_retain_tag_on_scalar_raises():
builtins = flexmock(sys.modules['builtins'])
flexmock(module.os).should_receive('getcwd').and_return('/tmp')
flexmock(module.os.path).should_receive('isabs').and_return(False)
flexmock(module.os.path).should_receive('exists').and_return(True)
include_file = io.StringIO(
'''
stuff:
foo: bar
baz: quux
'''
)
include_file.name = 'include.yaml'
builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
config_file = io.StringIO(
'''
stuff:
foo: !retain override
<<: !include include.yaml
'''
)
config_file.name = 'config.yaml'
builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
with pytest.raises(ValueError):
assert module.load_configuration('config.yaml')
def test_load_configuration_does_not_merge_include_list(): def test_load_configuration_does_not_merge_include_list():
builtins = flexmock(sys.modules['builtins']) builtins = flexmock(sys.modules['builtins'])
flexmock(module.os).should_receive('getcwd').and_return('/tmp') flexmock(module.os).should_receive('getcwd').and_return('/tmp')
@ -173,42 +265,59 @@ def test_load_configuration_does_not_merge_include_list():
config_file.name = 'config.yaml' config_file.name = 'config.yaml'
builtins.should_receive('open').with_args('config.yaml').and_return(config_file) builtins.should_receive('open').with_args('config.yaml').and_return(config_file)
with pytest.raises(ruamel.yaml.error.YAMLError): with pytest.raises(module.ruamel.yaml.error.YAMLError):
assert module.load_configuration('config.yaml') assert module.load_configuration('config.yaml')
@pytest.mark.parametrize(
'node_class',
(
module.ruamel.yaml.nodes.MappingNode,
module.ruamel.yaml.nodes.SequenceNode,
module.ruamel.yaml.nodes.ScalarNode,
),
)
def test_retain_node_error_raises(node_class):
with pytest.raises(ValueError):
module.retain_node_error(
loader=flexmock(), node=node_class(tag=flexmock(), value=flexmock())
)
def test_deep_merge_nodes_replaces_colliding_scalar_values(): def test_deep_merge_nodes_replaces_colliding_scalar_values():
node_values = [ node_values = [
( (
ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'), module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
ruamel.yaml.nodes.MappingNode( module.ruamel.yaml.nodes.MappingNode(
tag='tag:yaml.org,2002:map', tag='tag:yaml.org,2002:map',
value=[ value=[
( (
ruamel.yaml.nodes.ScalarNode( module.ruamel.yaml.nodes.ScalarNode(
tag='tag:yaml.org,2002:str', value='keep_hourly' tag='tag:yaml.org,2002:str', value='keep_hourly'
), ),
ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='24'), module.ruamel.yaml.nodes.ScalarNode(
tag='tag:yaml.org,2002:int', value='24'
),
), ),
( (
ruamel.yaml.nodes.ScalarNode( module.ruamel.yaml.nodes.ScalarNode(
tag='tag:yaml.org,2002:str', value='keep_daily' tag='tag:yaml.org,2002:str', value='keep_daily'
), ),
ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='7'), module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='7'),
), ),
], ],
), ),
), ),
( (
ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'), module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
ruamel.yaml.nodes.MappingNode( module.ruamel.yaml.nodes.MappingNode(
tag='tag:yaml.org,2002:map', tag='tag:yaml.org,2002:map',
value=[ value=[
( (
ruamel.yaml.nodes.ScalarNode( module.ruamel.yaml.nodes.ScalarNode(
tag='tag:yaml.org,2002:str', value='keep_daily' tag='tag:yaml.org,2002:str', value='keep_daily'
), ),
ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='5'), module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='5'),
), ),
], ],
), ),
@ -230,35 +339,39 @@ def test_deep_merge_nodes_replaces_colliding_scalar_values():
def test_deep_merge_nodes_keeps_non_colliding_scalar_values(): def test_deep_merge_nodes_keeps_non_colliding_scalar_values():
node_values = [ node_values = [
( (
ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'), module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
ruamel.yaml.nodes.MappingNode( module.ruamel.yaml.nodes.MappingNode(
tag='tag:yaml.org,2002:map', tag='tag:yaml.org,2002:map',
value=[ value=[
( (
ruamel.yaml.nodes.ScalarNode( module.ruamel.yaml.nodes.ScalarNode(
tag='tag:yaml.org,2002:str', value='keep_hourly' tag='tag:yaml.org,2002:str', value='keep_hourly'
), ),
ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='24'), module.ruamel.yaml.nodes.ScalarNode(
tag='tag:yaml.org,2002:int', value='24'
),
), ),
( (
ruamel.yaml.nodes.ScalarNode( module.ruamel.yaml.nodes.ScalarNode(
tag='tag:yaml.org,2002:str', value='keep_daily' tag='tag:yaml.org,2002:str', value='keep_daily'
), ),
ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='7'), module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='7'),
), ),
], ],
), ),
), ),
( (
ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'), module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
ruamel.yaml.nodes.MappingNode( module.ruamel.yaml.nodes.MappingNode(
tag='tag:yaml.org,2002:map', tag='tag:yaml.org,2002:map',
value=[ value=[
( (
ruamel.yaml.nodes.ScalarNode( module.ruamel.yaml.nodes.ScalarNode(
tag='tag:yaml.org,2002:str', value='keep_minutely' tag='tag:yaml.org,2002:str', value='keep_minutely'
), ),
ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='10'), module.ruamel.yaml.nodes.ScalarNode(
tag='tag:yaml.org,2002:int', value='10'
),
), ),
], ],
), ),
@ -282,28 +395,28 @@ def test_deep_merge_nodes_keeps_non_colliding_scalar_values():
def test_deep_merge_nodes_keeps_deeply_nested_values(): def test_deep_merge_nodes_keeps_deeply_nested_values():
node_values = [ node_values = [
( (
ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='storage'), module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='storage'),
ruamel.yaml.nodes.MappingNode( module.ruamel.yaml.nodes.MappingNode(
tag='tag:yaml.org,2002:map', tag='tag:yaml.org,2002:map',
value=[ value=[
( (
ruamel.yaml.nodes.ScalarNode( module.ruamel.yaml.nodes.ScalarNode(
tag='tag:yaml.org,2002:str', value='lock_wait' tag='tag:yaml.org,2002:str', value='lock_wait'
), ),
ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='5'), module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='5'),
), ),
( (
ruamel.yaml.nodes.ScalarNode( module.ruamel.yaml.nodes.ScalarNode(
tag='tag:yaml.org,2002:str', value='extra_borg_options' tag='tag:yaml.org,2002:str', value='extra_borg_options'
), ),
ruamel.yaml.nodes.MappingNode( module.ruamel.yaml.nodes.MappingNode(
tag='tag:yaml.org,2002:map', tag='tag:yaml.org,2002:map',
value=[ value=[
( (
ruamel.yaml.nodes.ScalarNode( module.ruamel.yaml.nodes.ScalarNode(
tag='tag:yaml.org,2002:str', value='init' tag='tag:yaml.org,2002:str', value='init'
), ),
ruamel.yaml.nodes.ScalarNode( module.ruamel.yaml.nodes.ScalarNode(
tag='tag:yaml.org,2002:str', value='--init-option' tag='tag:yaml.org,2002:str', value='--init-option'
), ),
), ),
@ -314,22 +427,22 @@ def test_deep_merge_nodes_keeps_deeply_nested_values():
), ),
), ),
( (
ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='storage'), module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='storage'),
ruamel.yaml.nodes.MappingNode( module.ruamel.yaml.nodes.MappingNode(
tag='tag:yaml.org,2002:map', tag='tag:yaml.org,2002:map',
value=[ value=[
( (
ruamel.yaml.nodes.ScalarNode( module.ruamel.yaml.nodes.ScalarNode(
tag='tag:yaml.org,2002:str', value='extra_borg_options' tag='tag:yaml.org,2002:str', value='extra_borg_options'
), ),
ruamel.yaml.nodes.MappingNode( module.ruamel.yaml.nodes.MappingNode(
tag='tag:yaml.org,2002:map', tag='tag:yaml.org,2002:map',
value=[ value=[
( (
ruamel.yaml.nodes.ScalarNode( module.ruamel.yaml.nodes.ScalarNode(
tag='tag:yaml.org,2002:str', value='prune' tag='tag:yaml.org,2002:str', value='prune'
), ),
ruamel.yaml.nodes.ScalarNode( module.ruamel.yaml.nodes.ScalarNode(
tag='tag:yaml.org,2002:str', value='--prune-option' tag='tag:yaml.org,2002:str', value='--prune-option'
), ),
), ),
@ -361,32 +474,32 @@ def test_deep_merge_nodes_keeps_deeply_nested_values():
def test_deep_merge_nodes_appends_colliding_sequence_values(): def test_deep_merge_nodes_appends_colliding_sequence_values():
node_values = [ node_values = [
( (
ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'), module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'),
ruamel.yaml.nodes.MappingNode( module.ruamel.yaml.nodes.MappingNode(
tag='tag:yaml.org,2002:map', tag='tag:yaml.org,2002:map',
value=[ value=[
( (
ruamel.yaml.nodes.ScalarNode( module.ruamel.yaml.nodes.ScalarNode(
tag='tag:yaml.org,2002:str', value='before_backup' tag='tag:yaml.org,2002:str', value='before_backup'
), ),
ruamel.yaml.nodes.SequenceNode( module.ruamel.yaml.nodes.SequenceNode(
tag='tag:yaml.org,2002:int', value=['echo 1', 'echo 2'] tag='tag:yaml.org,2002:seq', value=['echo 1', 'echo 2']
), ),
), ),
], ],
), ),
), ),
( (
ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'), module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'),
ruamel.yaml.nodes.MappingNode( module.ruamel.yaml.nodes.MappingNode(
tag='tag:yaml.org,2002:map', tag='tag:yaml.org,2002:map',
value=[ value=[
( (
ruamel.yaml.nodes.ScalarNode( module.ruamel.yaml.nodes.ScalarNode(
tag='tag:yaml.org,2002:str', value='before_backup' tag='tag:yaml.org,2002:str', value='before_backup'
), ),
ruamel.yaml.nodes.SequenceNode( module.ruamel.yaml.nodes.SequenceNode(
tag='tag:yaml.org,2002:int', value=['echo 3', 'echo 4'] tag='tag:yaml.org,2002:seq', value=['echo 3', 'echo 4']
), ),
), ),
], ],
@ -402,3 +515,101 @@ def test_deep_merge_nodes_appends_colliding_sequence_values():
assert len(options) == 1 assert len(options) == 1
assert options[0][0].value == 'before_backup' assert options[0][0].value == 'before_backup'
assert options[0][1].value == ['echo 1', 'echo 2', 'echo 3', 'echo 4'] assert options[0][1].value == ['echo 1', 'echo 2', 'echo 3', 'echo 4']
def test_deep_merge_nodes_keeps_mapping_values_tagged_with_retain():
node_values = [
(
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
module.ruamel.yaml.nodes.MappingNode(
tag='tag:yaml.org,2002:map',
value=[
(
module.ruamel.yaml.nodes.ScalarNode(
tag='tag:yaml.org,2002:str', value='keep_hourly'
),
module.ruamel.yaml.nodes.ScalarNode(
tag='tag:yaml.org,2002:int', value='24'
),
),
(
module.ruamel.yaml.nodes.ScalarNode(
tag='tag:yaml.org,2002:str', value='keep_daily'
),
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='7'),
),
],
),
),
(
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='retention'),
module.ruamel.yaml.nodes.MappingNode(
tag='!retain',
value=[
(
module.ruamel.yaml.nodes.ScalarNode(
tag='tag:yaml.org,2002:str', value='keep_daily'
),
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:int', value='5'),
),
],
),
),
]
result = module.deep_merge_nodes(node_values)
assert len(result) == 1
(section_key, section_value) = result[0]
assert section_key.value == 'retention'
assert section_value.tag == 'tag:yaml.org,2002:map'
options = section_value.value
assert len(options) == 1
assert options[0][0].value == 'keep_daily'
assert options[0][1].value == '5'
def test_deep_merge_nodes_keeps_sequence_values_tagged_with_retain():
node_values = [
(
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'),
module.ruamel.yaml.nodes.MappingNode(
tag='tag:yaml.org,2002:map',
value=[
(
module.ruamel.yaml.nodes.ScalarNode(
tag='tag:yaml.org,2002:str', value='before_backup'
),
module.ruamel.yaml.nodes.SequenceNode(
tag='tag:yaml.org,2002:seq', value=['echo 1', 'echo 2']
),
),
],
),
),
(
module.ruamel.yaml.nodes.ScalarNode(tag='tag:yaml.org,2002:str', value='hooks'),
module.ruamel.yaml.nodes.MappingNode(
tag='tag:yaml.org,2002:map',
value=[
(
module.ruamel.yaml.nodes.ScalarNode(
tag='tag:yaml.org,2002:str', value='before_backup'
),
module.ruamel.yaml.nodes.SequenceNode(
tag='!retain', value=['echo 3', 'echo 4']
),
),
],
),
),
]
result = module.deep_merge_nodes(node_values)
assert len(result) == 1
(section_key, section_value) = result[0]
assert section_key.value == 'hooks'
options = section_value.value
assert len(options) == 1
assert options[0][0].value == 'before_backup'
assert options[0][1].tag == 'tag:yaml.org,2002:seq'
assert options[0][1].value == ['echo 3', 'echo 4']