diff --git a/CHANGES b/CHANGES index c99fd8b9..42d6de2a 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,10 @@ +3.8.1 +Bug Fixes: +* Fixed issue #220 reported by https://github.com/abramov-oleg wherein novel + mergeat paths given to the yaml-merge tool with Array and Array-of-Hash data + in the RHS document would cause an interminable loop that would max out CPU + and eventually exhaust RAM. + 3.8.0 Enhancements: * The yaml-set and yaml-merge command-line tools now support a new option: diff --git a/tests/test_merger_merger.py b/tests/test_merger_merger.py index ba2d425d..c92acac5 100644 --- a/tests/test_merger_merger.py +++ b/tests/test_merger_merger.py @@ -3329,6 +3329,89 @@ def test_bad_merge_hash_into_set( assert -1 < str(ex.value).find( "Merging a Hash into a Set is destructive") + def test_merge_with_novel_mergeat_array( + self, quiet_logger, tmp_path, tmp_path_factory + ): + # Credit: https://github.com/abramov-oleg + # Reported: https://github.com/wwkimball/yamlpath/issues/220 + lhs_yaml_file = create_temp_yaml_file(tmp_path_factory, """--- +key: value +""") + rhs_yaml_file = create_temp_yaml_file(tmp_path_factory, """array: + - element +""") + merged_yaml = create_temp_yaml_file(tmp_path_factory, """--- +key: value +new_key: + array: + - element +""") + + output_dir = tmp_path / "test_merge_with_novel_mergeat_array" + output_dir.mkdir() + output_file = output_dir / "output.yaml" + + lhs_yaml = get_yaml_editor() + rhs_yaml = get_yaml_editor() + (lhs_data, lhs_loaded) = get_yaml_data(lhs_yaml, quiet_logger, lhs_yaml_file) + (rhs_data, rhs_loaded) = get_yaml_data(rhs_yaml, quiet_logger, rhs_yaml_file) + + args = SimpleNamespace(mergeat="new_key") + mc = MergerConfig(quiet_logger, args) + merger = Merger(quiet_logger, lhs_data, mc) + merger.merge_with(rhs_data) + + with open(output_file, 'w') as yaml_dump: + lhs_yaml.dump(merger.data, yaml_dump) + + assert ( + (os.path.getsize(output_file) == os.path.getsize(merged_yaml)) + and (open(output_file,'r').read() == open(merged_yaml,'r').read()) + ) + + def test_merge_with_novel_mergeat_aoh( + self, quiet_logger, tmp_path, tmp_path_factory + ): + # Credit: https://github.com/abramov-oleg + # Reported: https://github.com/wwkimball/yamlpath/issues/220 + lhs_yaml_file = create_temp_yaml_file(tmp_path_factory, """--- +key: value +""") + rhs_yaml_file = create_temp_yaml_file(tmp_path_factory, """--- +array_of_hash: + - name: one + val: some +""") + merged_yaml = create_temp_yaml_file(tmp_path_factory, """--- +key: value +new_key: + array_of_hash: + - name: one + val: some +""") + + output_dir = tmp_path / "test_merge_with_novel_mergeat_array" + output_dir.mkdir() + output_file = output_dir / "output.yaml" + + lhs_yaml = get_yaml_editor() + rhs_yaml = get_yaml_editor() + (lhs_data, lhs_loaded) = get_yaml_data(lhs_yaml, quiet_logger, lhs_yaml_file) + (rhs_data, rhs_loaded) = get_yaml_data(rhs_yaml, quiet_logger, rhs_yaml_file) + + args = SimpleNamespace(mergeat="new_key") + mc = MergerConfig(quiet_logger, args) + merger = Merger(quiet_logger, lhs_data, mc) + merger.merge_with(rhs_data) + + with open(output_file, 'w') as yaml_dump: + lhs_yaml.dump(merger.data, yaml_dump) + + assert ( + (os.path.getsize(output_file) == os.path.getsize(merged_yaml)) + and (open(output_file,'r').read() == open(merged_yaml,'r').read()) + ) + ### # set_flow_style diff --git a/yamlpath/__init__.py b/yamlpath/__init__.py index 9fc66065..d2c6db2f 100644 --- a/yamlpath/__init__.py +++ b/yamlpath/__init__.py @@ -1,6 +1,6 @@ """Core YAML Path classes.""" # Establish the version number common to all components -__version__ = "3.8.0" +__version__ = "3.8.1" from yamlpath.yamlpath import YAMLPath from yamlpath.processor import Processor diff --git a/yamlpath/merger/merger.py b/yamlpath/merger/merger.py index ac735acb..420dba3f 100644 --- a/yamlpath/merger/merger.py +++ b/yamlpath/merger/merger.py @@ -1,3 +1,4 @@ +#pylint: disable=too-many-lines """ Implement YAML document Merger. @@ -860,8 +861,10 @@ def merge_with(self, rhs: Any) -> None: if hasattr(target_node, "fa") else None)) - if isinstance(rhs, CommentedMap): - # The RHS document root is a dict + if target_node is rhs: + # _get_merge_target_nodes already inserted RHS (novel mergeat) + merge_performed = True + elif isinstance(rhs, CommentedMap): merge_performed = self._insert_dict( insert_at, target_node, rhs) elif isinstance(rhs, CommentedSeq):