def _purge_document(self, path: YAMLPath, data: Any) -> None: """ Record changes necessary to delete every node in the document. Parameters: 1. path (YAMLPath) YAML Path to the document element under evaluation 2. data (Any) The DOM element under evaluation Returns: N/A """ if isinstance(data, CommentedMap): lhs_iteration = -1 for key, val in data.items(): lhs_iteration += 1 next_path = (path + YAMLPath.escape_path_section(key, path.seperator)) self._diffs.append( DiffEntry(DiffActions.DELETE, next_path, val, None, lhs_parent=data, lhs_iteration=lhs_iteration)) elif isinstance(data, CommentedSeq): for idx, ele in enumerate(data): next_path = path + "[{}]".format(idx) self._diffs.append( DiffEntry(DiffActions.DELETE, next_path, ele, None, lhs_parent=data, lhs_iteration=idx)) elif isinstance(data, CommentedSet): for idx, ele in enumerate(data): next_path = (path + YAMLPath.escape_path_section(ele, path.seperator)) self._diffs.append( DiffEntry(DiffActions.DELETE, next_path, ele, None, lhs_parent=data, lhs_iteration=idx)) else: if data is not None: self._diffs.append( DiffEntry(DiffActions.DELETE, path, data, None))
def _find_eyaml_paths( self, data: Any, build_path: YAMLPath) -> Generator[YAMLPath, None, None]: """ Find every encrypted value and report each as a YAML Path. Recursively generates a set of stringified YAML Paths, each entry leading to an EYAML value within the evaluated YAML data. Parameters: 1. data (Any) The parsed YAML data to process 2. build_path (YAMLPath) A YAML Path under construction Returns: (Generator[Path, None, None]) each YAML Path entry as they are discovered Raises: N/A """ if isinstance(data, CommentedSeq): for idx, ele in enumerate(data): node_anchor = Anchors.get_node_anchor(ele) if node_anchor is not None: escaped_section = YAMLPath.escape_path_section( node_anchor, PathSeperators.DOT) tmp_path_segment = f"[&{escaped_section}]" else: tmp_path_segment = f"[{idx}]" tmp_path = build_path + tmp_path_segment if self.is_eyaml_value(ele): yield tmp_path else: for subpath in self._find_eyaml_paths(ele, tmp_path): yield subpath elif isinstance(data, CommentedMap): for key, val in data.non_merged_items(): tmp_path = build_path + YAMLPath.escape_path_section( key, PathSeperators.DOT) if self.is_eyaml_value(val): yield tmp_path else: for subpath in self._find_eyaml_paths(val, tmp_path): yield subpath
def _merge_sets( self, lhs: CommentedSet, rhs: CommentedSet, path: YAMLPath, node_coord: NodeCoords ) -> CommentedSet: """ Merge two YAML sets (CommentedSet-wrapped sets). Parameters: 1. lhs (CommentedSet) The merge target. 2. rhs (CommentedSet) 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: (CommentedSet) The merged result. Raises: - `MergeException` when a clean merge is impossible. """ merge_mode = self.config.set_merge_mode(node_coord) if merge_mode is SetMergeOpts.LEFT: return lhs if merge_mode is SetMergeOpts.RIGHT: return rhs tagless_lhs = Nodes.tagless_elements(list(lhs)) for ele in rhs: path_next = (path + YAMLPath.escape_path_section(ele, path.seperator)) self.logger.debug( "Processing set element {} at {}.".format(ele, path_next), prefix="Merger::_merge_sets: ", data=ele) cmp_val = ele if isinstance(ele, TaggedScalar): cmp_val = ele.value self.logger.debug( "Looking for comparison value, {}, in:".format(cmp_val), prefix="Merger::_merge_sets: ", data=tagless_lhs) if cmp_val in tagless_lhs: continue lhs.add(ele) return lhs
def _add_everything(self, path: YAMLPath, data: Any) -> None: """Add every node in the document.""" if isinstance(data, CommentedMap): rhs_iteration = -1 for key, val in data.items(): rhs_iteration += 1 next_path = (path + YAMLPath.escape_path_section(key, path.seperator)) self._diffs.append( DiffEntry( DiffActions.ADD, next_path, None, val, rhs_parent=data, rhs_iteration=rhs_iteration)) elif isinstance(data, CommentedSeq): for idx, ele in enumerate(data): next_path = path + "[{}]".format(idx) self._diffs.append( DiffEntry( DiffActions.ADD, next_path, None, ele, rhs_parent=data, rhs_iteration=idx)) else: if data is not None: self._diffs.append( DiffEntry(DiffActions.ADD, path, None, data) )
def _purge_document(self, path: YAMLPath, data: Any): """Delete every node in the document.""" if isinstance(data, CommentedMap): lhs_iteration = -1 for key, val in data.items(): lhs_iteration += 1 next_path = (path + YAMLPath.escape_path_section(key, path.seperator)) self._diffs.append( DiffEntry( DiffActions.DELETE, next_path, val, None, lhs_parent=data, lhs_iteration=lhs_iteration)) elif isinstance(data, CommentedSeq): for idx, ele in enumerate(data): next_path = path + "[{}]".format(idx) self._diffs.append( DiffEntry( DiffActions.DELETE, next_path, ele, None, lhs_parent=data, lhs_iteration=idx)) else: if data is not None: self._diffs.append( DiffEntry(DiffActions.DELETE, path, data, None) )
def search_for_paths(logger: ConsolePrinter, processor: EYAMLProcessor, data: Any, terms: SearchTerms, pathsep: PathSeperators = PathSeperators.DOT, build_path: str = "", seen_anchors: Optional[List[str]] = None, **kwargs: bool) -> Generator[YAMLPath, None, None]: """ Recursively search a data structure for nodes matching an expression. The nodes can be keys, values, and/or elements. When dealing with anchors and their aliases, the caller indicates whether to include only the original anchor or the anchor and all of its (duplicate) aliases. """ search_values: bool = kwargs.pop("search_values", True) search_keys: bool = kwargs.pop("search_keys", False) search_anchors: bool = kwargs.pop("search_anchors", False) include_key_aliases: bool = kwargs.pop("include_key_aliases", True) include_value_aliases: bool = kwargs.pop("include_value_aliases", False) decrypt_eyaml: bool = kwargs.pop("decrypt_eyaml", False) expand_children: bool = kwargs.pop("expand_children", False) strsep = str(pathsep) invert = terms.inverted method = terms.method term = terms.term if seen_anchors is None: seen_anchors = [] if isinstance(data, CommentedSeq): # Build the path if not build_path and pathsep is PathSeperators.FSLASH: build_path = strsep build_path += "[" for idx, ele in enumerate(data): # Any element may or may not have an Anchor/Alias anchor_matched = Searches.search_anchor( ele, terms, seen_anchors, search_anchors=search_anchors, include_aliases=include_value_aliases) logger.debug( ("yaml_paths::search_for_paths<list>:" + "anchor search => {}.") .format(anchor_matched) ) # Build the temporary YAML Path using either Anchor or Index if anchor_matched is AnchorMatches.NO_ANCHOR: # Not an anchor/alias, so ref this node by its index tmp_path = build_path + str(idx) + "]" else: tmp_path = "{}&{}]".format( build_path, YAMLPath.escape_path_section(ele.anchor.value, pathsep) ) if anchor_matched is AnchorMatches.ALIAS_EXCLUDED: continue if anchor_matched in [AnchorMatches.MATCH, AnchorMatches.ALIAS_INCLUDED]: logger.debug( ("yaml_paths::search_for_paths<list>:" + "yielding an Anchor/Alias match, {}.") .format(tmp_path) ) if expand_children: for path in yield_children( logger, ele, terms, pathsep, tmp_path, seen_anchors, search_anchors=search_anchors, include_key_aliases=include_key_aliases, include_value_aliases=include_value_aliases): yield path else: yield YAMLPath(tmp_path) continue if isinstance(ele, (CommentedSeq, CommentedMap)): logger.debug( "Recursing into complex data:", data=ele, prefix="yaml_paths::search_for_paths<list>: ", footer=">>>> >>>> >>>> >>>> >>>> >>>> >>>>") for subpath in search_for_paths( logger, processor, ele, terms, pathsep, tmp_path, seen_anchors, search_values=search_values, search_keys=search_keys, search_anchors=search_anchors, include_key_aliases=include_key_aliases, include_value_aliases=include_value_aliases, decrypt_eyaml=decrypt_eyaml, expand_children=expand_children ): logger.debug( "Yielding RECURSED match, {}.".format(subpath), prefix="yaml_paths::search_for_paths<list>: ", footer="<<<< <<<< <<<< <<<< <<<< <<<< <<<<" ) yield subpath elif search_values: if (anchor_matched is AnchorMatches.UNSEARCHABLE_ALIAS and not include_value_aliases): continue check_value = ele if decrypt_eyaml and processor.is_eyaml_value(ele): check_value = processor.decrypt_eyaml(ele) matches = Searches.search_matches(method, term, check_value) if (matches and not invert) or (invert and not matches): logger.debug( ("yaml_paths::search_for_paths<list>:" + "yielding VALUE match, {}: {}." ).format(check_value, tmp_path) ) yield YAMLPath(tmp_path) # pylint: disable=too-many-nested-blocks elif isinstance(data, CommentedMap): if build_path: build_path += strsep elif pathsep is PathSeperators.FSLASH: build_path = strsep pool = data.non_merged_items() if include_key_aliases or include_value_aliases: pool = data.items() for key, val in pool: tmp_path = build_path + YAMLPath.escape_path_section(key, pathsep) # Search the value anchor to have it on record, in case the key # anchor match would otherwise block the value anchor from # appearing in seen_anchors (which is important). val_anchor_matched = Searches.search_anchor( val, terms, seen_anchors, search_anchors=search_anchors, include_aliases=include_value_aliases) logger.debug( ("yaml_paths::search_for_paths<dict>:" + "VALUE anchor search => {}.") .format(val_anchor_matched) ) # Search the key when the caller wishes it. if search_keys: # The key itself may be an Anchor or Alias. Search it when the # caller wishes. key_anchor_matched = Searches.search_anchor( key, terms, seen_anchors, search_anchors=search_anchors, include_aliases=include_key_aliases) logger.debug( ("yaml_paths::search_for_paths<dict>:" + "KEY anchor search, {}: {}.") .format(key, key_anchor_matched) ) if key_anchor_matched in [AnchorMatches.MATCH, AnchorMatches.ALIAS_INCLUDED]: logger.debug( ("yaml_paths::search_for_paths<dict>:" + "yielding a KEY-ANCHOR match, {}." ).format(key, tmp_path) ) if expand_children: for path in yield_children( logger, val, terms, pathsep, tmp_path, seen_anchors, search_anchors=search_anchors, include_key_aliases=include_key_aliases, include_value_aliases=include_value_aliases): yield path else: yield YAMLPath(tmp_path) continue # Search the name of the key, itself matches = Searches.search_matches(method, term, key) if (matches and not invert) or (invert and not matches): logger.debug( ("yaml_paths::search_for_paths<dict>:" + "yielding KEY name match, {}: {}." ).format(key, tmp_path) ) if expand_children: # Include every non-excluded child node under this # matched parent node. for path in yield_children( logger, val, terms, pathsep, tmp_path, seen_anchors, search_anchors=search_anchors, include_key_aliases=include_key_aliases, include_value_aliases=include_value_aliases): yield path else: # No other matches within this node matter because they # are already in the result. yield YAMLPath(tmp_path) continue # The value may itself be anchored; search it if requested if val_anchor_matched is AnchorMatches.ALIAS_EXCLUDED: continue if val_anchor_matched in [AnchorMatches.MATCH, AnchorMatches.ALIAS_INCLUDED]: logger.debug( ("yaml_paths::search_for_paths<dict>:" + "yielding a VALUE-ANCHOR match, {}.") .format(tmp_path) ) if expand_children: for path in yield_children( logger, val, terms, pathsep, tmp_path, seen_anchors, search_anchors=search_anchors, include_key_aliases=include_key_aliases, include_value_aliases=include_value_aliases): yield path else: yield YAMLPath(tmp_path) continue if isinstance(val, (CommentedSeq, CommentedMap)): logger.debug( "Recursing into complex data:", data=val, prefix="yaml_paths::search_for_paths<dict>: ", footer=">>>> >>>> >>>> >>>> >>>> >>>> >>>>" ) for subpath in search_for_paths( logger, processor, val, terms, pathsep, tmp_path, seen_anchors, search_values=search_values, search_keys=search_keys, search_anchors=search_anchors, include_key_aliases=include_key_aliases, include_value_aliases=include_value_aliases, decrypt_eyaml=decrypt_eyaml, expand_children=expand_children ): logger.debug( "Yielding RECURSED match, {}.".format(subpath), prefix="yaml_paths::search_for_paths<dict>: ", footer="<<<< <<<< <<<< <<<< <<<< <<<< <<<<" ) yield subpath elif search_values: if (val_anchor_matched is AnchorMatches.UNSEARCHABLE_ALIAS and not include_value_aliases): continue check_value = val if decrypt_eyaml and processor.is_eyaml_value(val): check_value = processor.decrypt_eyaml(val) matches = Searches.search_matches(method, term, check_value) if (matches and not invert) or (invert and not matches): logger.debug( ("yaml_paths::search_for_paths<dict>:" + "yielding VALUE match, {}: {}." ).format(check_value, tmp_path) ) yield YAMLPath(tmp_path)
def yield_children(logger: ConsolePrinter, data: Any, terms: SearchTerms, pathsep: PathSeperators, build_path: str, seen_anchors: List[str], **kwargs: bool) -> Generator[YAMLPath, None, None]: """ Dump the YAML Path of every child node beneath a given parent. Except for unwanted aliases, the dump is unconditional. """ include_key_aliases: bool = kwargs.pop("include_key_aliases", True) include_value_aliases: bool = kwargs.pop("include_value_aliases", False) search_anchors: bool = kwargs.pop("search_anchors", False) logger.debug( "Dumping all children in data of type, {}:" .format(type(data)), data=data, prefix="yaml_paths::yield_children: ") exclude_alias_matchers = [AnchorMatches.UNSEARCHABLE_ALIAS, AnchorMatches.ALIAS_EXCLUDED] if isinstance(data, CommentedSeq): if not build_path and pathsep is PathSeperators.FSLASH: build_path = str(pathsep) build_path += "[" for idx, ele in enumerate(data): anchor_matched = Searches.search_anchor( ele, terms, seen_anchors, search_anchors=search_anchors, include_aliases=include_value_aliases) logger.debug( ("yaml_paths::yield_children<list>: " + "element[{}] has anchor search => {}.") .format(idx, anchor_matched)) # Build the temporary YAML Path using either Anchor or Index if anchor_matched is AnchorMatches.NO_ANCHOR: # Not an anchor/alias, so ref this node by its index tmp_path = build_path + str(idx) + "]" else: tmp_path = "{}&{}]".format( build_path, YAMLPath.escape_path_section(ele.anchor.value, pathsep) ) if (not include_value_aliases and anchor_matched in exclude_alias_matchers): continue if isinstance(ele, (CommentedMap, CommentedSeq)): for path in yield_children( logger, ele, terms, pathsep, tmp_path, seen_anchors, search_anchors=search_anchors, include_key_aliases=include_key_aliases, include_value_aliases=include_value_aliases): yield path else: yield YAMLPath(tmp_path) elif isinstance(data, CommentedMap): if build_path: build_path += str(pathsep) elif pathsep is PathSeperators.FSLASH: build_path = str(pathsep) pool = data.non_merged_items() if include_key_aliases or include_value_aliases: pool = data.items() for key, val in pool: tmp_path = build_path + YAMLPath.escape_path_section(key, pathsep) key_anchor_matched = Searches.search_anchor( key, terms, seen_anchors, search_anchors=search_anchors, include_aliases=include_key_aliases) val_anchor_matched = Searches.search_anchor( val, terms, seen_anchors, search_anchors=search_anchors, include_aliases=include_value_aliases) logger.debug( ("yaml_paths::yield_children<dict>: " + "key[{}]:value have value anchor search => {}:{}.") .format(key, key_anchor_matched, val_anchor_matched)) if ( (not include_key_aliases and key_anchor_matched in exclude_alias_matchers) or (not include_value_aliases and val_anchor_matched in exclude_alias_matchers) ): continue if isinstance(val, (CommentedSeq, CommentedMap)): for path in yield_children( logger, val, terms, pathsep, tmp_path, seen_anchors, search_anchors=search_anchors, include_key_aliases=include_key_aliases, include_value_aliases=include_value_aliases): yield path else: yield YAMLPath(tmp_path) else: if not build_path and pathsep is PathSeperators.FSLASH: build_path = str(pathsep) yield YAMLPath(build_path)
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
def min(data: Any, invert: bool, parameters: List[str], yaml_path: YAMLPath, **kwargs: Any) -> Generator[NodeCoords, None, None]: """ Find whichever nodes/elements have a minimum value. Parameters: 1. data (Any) The data to evaluate 2. invert (bool) Invert the evaluation 3. parameters (List[str]) Parsed parameters 4. yaml_path (YAMLPath) YAML Path begetting this operation Keyword Arguments: * parent (ruamel.yaml node) The parent node from which this query originates * parentref (Any) The Index or Key of data within parent * relay_segment (PathSegment) YAML Path segment presently under evaluation * translated_path (YAMLPath) YAML Path indicating precisely which node is being evaluated * ancestry (List[AncestryEntry]) Stack of ancestors preceding the present node under evaluation Returns: (Generator[NodeCoords, None, None]) each result as it is generated """ parent: Any = kwargs.pop("parent", None) parentref: Any = kwargs.pop("parentref", None) translated_path: YAMLPath = kwargs.pop("translated_path", YAMLPath("")) ancestry: List[AncestryEntry] = kwargs.pop("ancestry", []) relay_segment: PathSegment = kwargs.pop("relay_segment", None) # There may be 0 or 1 parameters param_count = len(parameters) if param_count > 1: raise YAMLPathException( ("Invalid parameter count to {}([NAME]); up to {} permitted, " " got {} in YAML Path").format(PathSearchKeywords.MIN, 1, param_count), str(yaml_path)) scan_node = parameters[0] if param_count > 0 else None match_value: Any = None match_nodes: List[NodeCoords] = [] discard_nodes: List[NodeCoords] = [] unwrapped_data: Any = NodeCoords.unwrap_node_coords(data) if Nodes.node_is_aoh(unwrapped_data, accept_nulls=True): # A named child node is mandatory if scan_node is None: raise YAMLPathException(( "The {}([NAME]) Search Keyword requires a key name to scan" " when evaluating an Array-of-Hashes in YAML Path").format( PathSearchKeywords.MIN), str(yaml_path)) for idx, wrapped_ele in enumerate(data): ele = NodeCoords.unwrap_node_coords(wrapped_ele) next_path = translated_path + "[{}]".format(idx) next_ancestry = ancestry + [(data, idx)] if ele is not None and scan_node in ele: eval_val = ele[scan_node] if (match_value is None or Searches.search_matches( PathSearchMethods.LESS_THAN, match_value, eval_val)): match_value = eval_val discard_nodes.extend(match_nodes) match_nodes = [ NodeCoords(ele, data, idx, next_path, next_ancestry, relay_segment) ] continue if (match_value is None or Searches.search_matches( PathSearchMethods.EQUALS, match_value, eval_val)): match_nodes.append( NodeCoords(ele, data, idx, next_path, next_ancestry, relay_segment)) continue discard_nodes.append( NodeCoords(ele, data, idx, next_path, next_ancestry, relay_segment)) elif isinstance(data, dict): # A named child node is mandatory if scan_node is None: raise YAMLPathException(( "The {}([NAME]) Search Keyword requires a key name to scan" " when comparing Hash/map/dict children in YAML Path" ).format(PathSearchKeywords.MIN), str(yaml_path)) for key, val in data.items(): next_ancestry = ancestry + [(data, key)] next_path = (translated_path + YAMLPath.escape_path_section( key, translated_path.seperator)) if isinstance(val, dict): if val is not None and scan_node in val: eval_val = val[scan_node] if (match_value is None or Searches.search_matches( PathSearchMethods.LESS_THAN, match_value, eval_val)): match_value = eval_val discard_nodes.extend(match_nodes) match_nodes = [ NodeCoords(val, data, key, next_path, next_ancestry, relay_segment) ] continue if (match_value is None or Searches.search_matches( PathSearchMethods.EQUALS, match_value, eval_val)): match_nodes.append( NodeCoords(val, data, key, next_path, next_ancestry, relay_segment)) continue elif scan_node in data: # The user probably meant to operate against the parent raise YAMLPathException( ("The {}([NAME]) Search Keyword operates against" " collections of data which share a common attribute" " yet there is only a single node to consider. Did" " you mean to evaluate the parent of the selected" " node? Please review your YAML Path").format( PathSearchKeywords.MIN), str(yaml_path)) discard_nodes.append( NodeCoords(val, data, key, next_path, next_ancestry, relay_segment)) elif isinstance(data, list): # A named child node is useless if scan_node is not None: raise YAMLPathException( ("The {}([NAME]) Search Keyword cannot utilize a key name" " when comparing Array/sequence/list elements to one" " another in YAML Path").format(PathSearchKeywords.MIN), str(yaml_path)) for idx, ele in enumerate(data): next_path = translated_path + "[{}]".format(idx) next_ancestry = ancestry + [(data, idx)] if (ele is not None and (match_value is None or Searches.search_matches( PathSearchMethods.LESS_THAN, match_value, ele))): match_value = ele discard_nodes.extend(match_nodes) match_nodes = [ NodeCoords(ele, data, idx, next_path, next_ancestry, relay_segment) ] continue if (ele is not None and Searches.search_matches( PathSearchMethods.EQUALS, match_value, ele)): match_nodes.append( NodeCoords(ele, data, idx, next_path, next_ancestry, relay_segment)) continue discard_nodes.append( NodeCoords(ele, data, idx, next_path, next_ancestry, relay_segment)) else: # Non-complex data is always its own maximum and does not invert match_value = data match_nodes = [ NodeCoords(data, parent, parentref, translated_path, ancestry, relay_segment) ] yield_nodes = discard_nodes if invert else match_nodes for node_coord in yield_nodes: yield node_coord
def _diff_sets(self, path: YAMLPath, lhs: CommentedSet, rhs: CommentedSet) -> None: """ Diff two sets. Parameters: 1. path (YAMLPath) YAML Path to the document element under evaluation 2. lhs (Any) The left-hand-side (original) document 3. rhs (Any) The right-hand-side (altered) document Keyword Arguments: * rhs_parent (Any) Parent data node of rhs * parentref (Any) Reference indicating rhs within rhs_parent Returns: N/A """ self.logger.debug("Comparing LHS:", prefix="Differ::_diff_sets: ", data=lhs) self.logger.debug("Against RHS:", prefix="Differ::_diff_sets: ", data=rhs) # Sets must allways have a tag of !!set lest PyYAML/ruamel.yaml fail to # load them as set data. Keep this code commented until a case is # found which proves otherwise. # # Check first for a difference in YAML Tag # lhs_tag = lhs.tag.value if hasattr(lhs, "tag") else None # rhs_tag = rhs.tag.value if hasattr(rhs, "tag") else None # if lhs_tag != rhs_tag: # self.logger.debug( # "Sets have different YAML Tags; {} != {}:".format( # lhs_tag, rhs_tag), # prefix="Differ::_diff_sets: ") # self._diffs.append( # DiffEntry( # DiffActions.DELETE, path, lhs, None, key_tag=lhs_tag)) # self._diffs.append( # DiffEntry( # DiffActions.ADD, path, None, rhs, key_tag=rhs_tag)) # return lhs_keys = set(lhs) rhs_keys = set(rhs) lhs_key_indicies = Differ._get_key_indicies(lhs) rhs_key_indicies = Differ._get_key_indicies(rhs) self.logger.debug("Got LHS key indicies:", prefix="Differ::_diff_sets: ", data=lhs_key_indicies) self.logger.debug("Got RHS key indicies:", prefix="Differ::_diff_sets: ", data=rhs_key_indicies) # Look for changes for key in [(key) for key in rhs if key in lhs and key in rhs]: next_path = (path + YAMLPath.escape_path_section(key, path.seperator)) lhs_ele = None rhs_ele = None for ele in lhs: if ele == key: lhs_ele = ele break for ele in rhs: if ele == key: rhs_ele = ele break self._diff_between(next_path, lhs_ele, rhs_ele, lhs_parent=lhs, lhs_iteration=lhs_key_indicies[key], rhs_parent=rhs, rhs_iteration=rhs_key_indicies[key], parentref=key) # Look for deleted keys for key in lhs_keys - rhs_keys: next_path = (path + YAMLPath.escape_path_section(key, path.seperator)) self._diffs.append( DiffEntry( DiffActions.DELETE, next_path, key, None, lhs_parent=lhs, lhs_iteration=lhs_key_indicies[key], rhs_parent=rhs, key_tag=key.tag.value if hasattr(key, "tag") else None)) # Look for new keys for key in rhs_keys - lhs_keys: next_path = (path + YAMLPath.escape_path_section(key, path.seperator)) self._diffs.append( DiffEntry( DiffActions.ADD, next_path, None, key, lhs_parent=lhs, rhs_parent=rhs, rhs_iteration=rhs_key_indicies[key], key_tag=key.tag.value if hasattr(key, "tag") else None))
def _diff_dicts(self, path: YAMLPath, lhs: CommentedMap, rhs: CommentedMap) -> None: """ Diff two dicts. Parameters: 1. path (YAMLPath) YAML Path to the document element under evaluation 2. lhs (Any) The left-hand-side (original) document 3. rhs (Any) The right-hand-side (altered) document """ self.logger.debug("Comparing LHS:", prefix="Differ::_diff_dicts: ", data=lhs) self.logger.debug("Against RHS:", prefix="Differ::_diff_dicts: ", data=rhs) # Check first for a difference in YAML Tag lhs_tag = lhs.tag.value if hasattr(lhs, "tag") else None rhs_tag = rhs.tag.value if hasattr(rhs, "tag") else None if lhs_tag != rhs_tag: self.logger.debug( "Dictionaries have different YAML Tags; {} != {}:".format( lhs_tag, rhs_tag), prefix="Differ::_diff_dicts: ") self._diffs.append( DiffEntry(DiffActions.DELETE, path, lhs, None, key_tag=lhs_tag)) self._diffs.append( DiffEntry(DiffActions.ADD, path, None, rhs, key_tag=rhs_tag)) return lhs_keys = set(lhs) rhs_keys = set(rhs) lhs_key_indicies = Differ._get_key_indicies(lhs) rhs_key_indicies = Differ._get_key_indicies(rhs) self.logger.debug("Got LHS key indicies:", prefix="Differ::_diff_dicts: ", data=lhs_key_indicies) self.logger.debug("Got RHS key indicies:", prefix="Differ::_diff_dicts: ", data=rhs_key_indicies) # Look for changes for key, val in [(key, val) for key, val in rhs.items() if key in lhs and key in rhs]: next_path = (path + YAMLPath.escape_path_section(key, path.seperator)) self._diff_between(next_path, lhs[key], val, lhs_parent=lhs, lhs_iteration=lhs_key_indicies[key], rhs_parent=rhs, rhs_iteration=rhs_key_indicies[key], parentref=key) # Look for deleted keys for key in lhs_keys - rhs_keys: next_path = (path + YAMLPath.escape_path_section(key, path.seperator)) self._diffs.append( DiffEntry( DiffActions.DELETE, next_path, lhs[key], None, lhs_parent=lhs, lhs_iteration=lhs_key_indicies[key], rhs_parent=rhs, key_tag=key.tag.value if hasattr(key, "tag") else None)) # Look for new keys for key in rhs_keys - lhs_keys: next_path = (path + YAMLPath.escape_path_section(key, path.seperator)) self._diffs.append( DiffEntry( DiffActions.ADD, next_path, None, rhs[key], lhs_parent=lhs, rhs_parent=rhs, rhs_iteration=rhs_key_indicies[key], key_tag=key.tag.value if hasattr(key, "tag") else None))
def escape_path_section(*args): """Relay function call to static method.""" return YAMLPath.escape_path_section(*args)