def _delete_mergeref_keys(self, data: CommentedMap) -> None: """ Delete all YAML merge reference keys from a CommentedMap. This is necessary when using the insert() method of a CommentedMap because doing so converts all YAML merge references (key-value pairs merged into a YAML Hash via the `<<:` operator) to concrete key-value pairs of the affected Hash. """ concrete_keys = [] for local_key, _ in data.non_merged_items(): concrete_keys.append(local_key) reference_keys = set(data.keys()).difference(concrete_keys) for key in reference_keys: self.logger.debug("Deleting key from LHS:", data=key, prefix="Merger::_delete_mergeref_keys: ", header="!" * 50) del data[key]
def _merge_dicts( self, lhs: CommentedMap, rhs: CommentedMap, path: YAMLPath ) -> CommentedMap: """ Merge two YAML maps (CommentedMap-wrapped dicts). Parameters: 1. lhs (CommentedMap) The merge target. 2. rhs (CommentedMap) The merge source. 3. path (YAMLPath) Location within the DOM where this merge is taking place. Returns: (CommentedMap) The merged result. Raises: - `MergeException` when a clean merge is impossible. """ if not isinstance(lhs, CommentedMap): raise MergeException( "Impossible to add Hash data to non-Hash destination.", path) self.logger.debug( "Merging INTO dict with keys: {}:".format(", ".join([ str(k.value) if isinstance(k, TaggedScalar) else str(k) for k in lhs.keys()])), data=lhs, prefix="Merger::_merge_dicts: ", header="--------------------") self.logger.debug( "Merging FROM dict with keys: {}:".format(", ".join([ str(k.value) if isinstance(k, TaggedScalar) else str(k) for k in rhs.keys()])), data=rhs, prefix="Merger::_merge_dicts: ", footer="====================") # Delete all internal YAML merge reference keys lest any later # .insert() operation on LHS inexplicably convert them from reference # to concrete keys. This seems like a bug in ruamel.yaml... self._delete_mergeref_keys(lhs) # Assume deep merge until a node's merge rule indicates otherwise buffer: List[Tuple[Any, Any]] = [] buffer_pos = 0 for key, val in rhs.non_merged_items(): path_next = (path + YAMLPath.escape_path_section(key, path.seperator)) if key in lhs: # Write the buffer if populated for b_key, b_val in buffer: self.logger.debug( "Merger::_merge_dicts: Inserting key, {}, from" " buffer to position, {}, at path, {}." .format(b_key, buffer_pos, path_next), header="INSERT " * 15) self.logger.debug( "Before INSERT, the LHS document was:", data=lhs, prefix="Merger::_merge_dicts: ") self.logger.debug( "... and before INSERT, the incoming value will be:", data=b_val, prefix="Merger::_merge_dicts: ") lhs.insert(buffer_pos, b_key, b_val) self.logger.debug( "After INSERT, the LHS document became:", data=lhs, prefix="Merger::_merge_dicts: ") buffer_pos += 1 buffer = [] # Short-circuit the deep merge if a different merge rule # applies to this node. node_coord = NodeCoords(val, rhs, key) merge_mode = ( self.config.hash_merge_mode(node_coord) if isinstance(val, CommentedMap) else self.config.set_merge_mode(node_coord) if isinstance(val, CommentedSet) else self.config.aoh_merge_mode(node_coord) ) self.logger.debug("Merger::_merge_dicts: Got merge mode, {}." .format(merge_mode)) if merge_mode in ( HashMergeOpts.LEFT, AoHMergeOpts.LEFT, SetMergeOpts.LEFT ): continue if merge_mode in ( HashMergeOpts.RIGHT, AoHMergeOpts.RIGHT, SetMergeOpts.RIGHT ): self.logger.debug( "Merger::_merge_dicts: Overwriting key, {}, at path," " {}.".format(key, path_next), header="OVERWRITE " * 15) lhs[key] = val continue if isinstance(val, CommentedMap): lhs[key] = self._merge_dicts(lhs[key], val, path_next) # Synchronize any YAML Tag self.logger.debug( "Merger::_merge_dicts: Setting LHS tag from {} to {}." .format(lhs[key].tag.value, val.tag.value)) lhs[key].yaml_set_tag(val.tag.value) self.logger.debug( "Document BEFORE calling combine_merge_anchors:", data=lhs, prefix="Merger::_merge_dicts: ", header="+------------------+") Anchors.combine_merge_anchors(lhs[key], val) self.logger.debug( "Document AFTER calling combine_merge_anchors:", data=lhs, prefix="Merger::_merge_dicts: ", footer="+==================+") elif isinstance(val, CommentedSeq): lhs[key] = self._merge_lists( lhs[key], val, path_next, parent=rhs, parentref=key) # Synchronize any YAML Tag self.logger.debug( "Merger::_merge_dicts: Setting LHS tag from {} to {}." .format(lhs[key].tag.value, val.tag.value)) lhs[key].yaml_set_tag(val.tag.value) elif isinstance(val, CommentedSet): lhs[key] = self._merge_sets( lhs[key], val, path_next, node_coord) # Synchronize any YAML Tag self.logger.debug( "Merger::_merge_dicts: Setting LHS tag from {} to {}." .format(lhs[key].tag.value, val.tag.value)) lhs[key].yaml_set_tag(val.tag.value) else: self.logger.debug( "Merger::_merge_dicts: Updating key, {}, at path," " {}.".format(key, path_next), header="UPDATE " * 15) self.logger.debug( "Before UPDATE, the LHS document was:", data=lhs, prefix="Merger::_merge_dicts: ") self.logger.debug( "... and before UPDATE, the incoming value will be:", data=val, prefix="Merger::_merge_dicts: ") lhs[key] = val self.logger.debug( "After UPDATE, the LHS document became:", data=lhs, prefix="Merger::_merge_dicts: ") else: # LHS lacks the RHS key. Buffer this key-value pair in order # to insert it ahead of whatever key(s) follow this one in RHS # to keep anchor definitions before their aliases. buffer.append((key, val)) buffer_pos += 1 # Write any remaining buffered content to the end of LHS for b_key, b_val in buffer: self.logger.debug( "Merger::_merge_dicts: Appending key, {}, from buffer at" " path, {}.".format(b_key, path), header="APPEND " * 15) lhs[b_key] = b_val self.logger.debug( "Completed merge result for path, {}:".format(path), data=lhs, prefix="Merger::_merge_dicts: ") return lhs