def _insert_scalar( self, insert_at: YAMLPath, lhs: Any, lhs_proc: Processor, rhs: Any ) -> bool: """Insert an RHS scalar into the LHS document.""" merge_performed = False if isinstance(lhs, CommentedSeq): self.logger.debug( "Merger::_insert_scalar: Merging a scalar into a list.") Nodes.append_list_element(lhs, rhs) merge_performed = True elif isinstance(lhs, CommentedSet): self.logger.debug( "Merger::_insert_scalar: Merging a scalar into a set.") self._merge_sets( lhs, CommentedSet([rhs]), insert_at, NodeCoords(rhs, None, None)) merge_performed = True elif isinstance(lhs, CommentedMap): ex_message = ( "Impossible to add Scalar value, {}, to a Hash without" " a key. Change the value to a 'key: value' pair, a" " '{{key: value}}' Hash, or change the merge target to" " an Array or other Scalar value." ).format(rhs) if len(str(rhs)) < 1 and not sys.stdin.isatty(): ex_message += ( " You may be seeing this because your workflow" " inadvertently opened a STDIN handle to {}. If" " this may be the case, try adding --nostdin or -S" " so as to block unintentional STDIN reading." ).format(basename(sys.argv[0])) raise MergeException(ex_message, insert_at) else: lhs_proc.set_value(insert_at, rhs) merge_performed = True return merge_performed
def merge_with(self, rhs: Any) -> None: """ Merge this document with another. Parameters: 1. rhs (Any) The document to merge into this one. Returns: N/A Raises: - `MergeException` when a clean merge is impossible. """ # Do nothing when RHS is None (empty document) if rhs is None: return # Remove all comments (no sensible way to merge them) Parsers.delete_all_comments(rhs) # When LHS is None (empty document), just dump all of RHS into it, # honoring any --mergeat|-m location as best as possible. insert_at = self.config.get_insertion_point() if self.data is None: self.logger.debug("Replacing None data with:", prefix="Merger::merge_with: ", data=rhs, data_header=" *****") self.data = Nodes.build_next_node(insert_at, 0, rhs) self.logger.debug("Merged document is now:", prefix="Merger::merge_with: ", data=self.data, footer=" ***** ***** *****") if isinstance(rhs, (dict, list)): # Only Scalar values need further processing return # Resolve any anchor conflicts self._resolve_anchor_conflicts(rhs) # Prepare the merge rules self.config.prepare(rhs) # Identify a reasonable default should a DOM need to be built up to # receive the RHS data. default_val = rhs if isinstance(rhs, CommentedMap): default_val = {} elif isinstance(rhs, CommentedSeq): default_val = [] # Loop through all insertion points and the elements in RHS merge_performed = False nodes: List[NodeCoords] = [] lhs_proc = Processor(self.logger, self.data) for node_coord in lhs_proc.get_nodes(insert_at, default_value=default_val): nodes.append(node_coord) for node_coord in nodes: target_node = (node_coord.node if isinstance( node_coord.node, (CommentedMap, CommentedSeq)) else node_coord.parent) Parsers.set_flow_style(rhs, (target_node.fa.flow_style() if hasattr( target_node, "fa") else None)) if isinstance(rhs, CommentedMap): # The RHS document root is a map if isinstance(target_node, CommentedSeq): # But the destination is a list self._merge_lists(target_node, CommentedSeq([rhs]), insert_at) else: self._merge_dicts(target_node, rhs, insert_at) # Synchronize YAML Tags self.logger.debug( "Merger::merge_with: Setting LHS tag from {} to {}.". format(target_node.tag.value, rhs.tag.value)) target_node.yaml_set_tag(rhs.tag.value) merge_performed = True elif isinstance(rhs, CommentedSeq): # The RHS document root is a list self._merge_lists(target_node, rhs, insert_at) merge_performed = True # Synchronize any YAML Tag self.logger.debug( "Merger::merge_with: Setting LHS tag from {} to {}.". format(target_node.tag.value, rhs.tag.value)) target_node.yaml_set_tag(rhs.tag.value) else: # The RHS document root is a Scalar value target_node = node_coord.node if isinstance(target_node, CommentedSeq): Nodes.append_list_element(target_node, rhs) merge_performed = True elif isinstance(target_node, CommentedMap): raise MergeException( "Impossible to add Scalar value, {}, to a Hash without" " a key. Change the value to a 'key: value' pair, a" " '{{key: value}}' Hash, or change the merge target to" " an Array or other Scalar value.".format(rhs), insert_at) else: lhs_proc.set_value(insert_at, rhs) merge_performed = True self.logger.debug("Completed merge operation, resulting in document:", prefix="Merger::merge_with: ", data=self.data) if not merge_performed: raise MergeException( "A merge was not performed. Ensure your target path matches" " at least one node in the left document(s).", insert_at)
def _merge_arrays_of_hashes( self, lhs: CommentedSeq, rhs: CommentedSeq, path: YAMLPath, node_coord: NodeCoords ) -> CommentedSeq: """ Merge two Arrays-of-Hashes. This is a deep merge operation. Each dict is treated as a record with an identity key. RHS records are merged with LHS records for which the identity key matches. As such, an identity key is required in both LHS and RHS records. This key is configurable. When there is no LHS match for an RHS key, the RHS record is appended to the LHS list. Parameters: 1. lhs (CommentedSeq) The merge target. 2. rhs (CommentedSeq) The merge source. 3. path (YAMLPath) Location within the DOM where this merge is taking place. 4. node_coord (NodeCoords) The RHS root node, its parent, and reference within its parent; used for config lookups. Returns: (CommentedSeq) The merged result. Raises: - `MergeException` when a clean merge is impossible. """ if not isinstance(lhs, CommentedSeq): raise MergeException( "Impossible to add Array-of-Hash data to non-Array" " destination." , path) self.logger.debug( "Merging {} Hash(es) at {}.".format(len(rhs), path), prefix="Merger::_merge_arrays_of_hashes: ", data=rhs) id_key: str = "" if len(rhs) > 0 and isinstance(rhs[0], CommentedMap): id_key = self.config.aoh_merge_key( NodeCoords(rhs[0], rhs, 0), rhs[0]) self.logger.debug( "Merger::_merge_arrays_of_hashes: RHS AoH yielded id_key:" " {}.".format(id_key)) merge_mode = self.config.aoh_merge_mode(node_coord) for idx, ele in enumerate(rhs): path_next = path + "[{}]".format(idx) self.logger.debug( "Processing element #{} at {}.".format(idx, path_next), prefix="Merger::_merge_arrays_of_hashes: ", data=ele) if merge_mode is AoHMergeOpts.DEEP: if id_key in ele: id_val = Nodes.tagless_value(ele[id_key]) else: raise MergeException( "Mandatory identity key, {}, not present in Hash with" " keys: {}." .format(id_key, ", ".join(ele.keys())) , path_next ) merged_hash = False for lhs_hash in ( lhs_hash for lhs_hash in lhs if isinstance(lhs_hash, CommentedMap) and id_key in lhs_hash and Nodes.tagless_value(lhs_hash[id_key]) == id_val ): self._merge_dicts(lhs_hash, ele, path_next) merged_hash = True # Synchronize YAML Tags lhs_hash.yaml_set_tag(ele.tag.value) break if not merged_hash: Nodes.append_list_element(lhs, ele, ele.anchor.value if hasattr(ele, "anchor") else None) elif merge_mode is AoHMergeOpts.UNIQUE: if ele not in lhs: Nodes.append_list_element( lhs, ele, ele.anchor.value if hasattr(ele, "anchor") else None) else: Nodes.append_list_element(lhs, ele, ele.anchor.value if hasattr(ele, "anchor") else None) return lhs
def append_list_element(*args): """Relay function call to static method.""" return Nodes.append_list_element(*args)