def test_yield_raw_children_direct(self, tmp_path_factory, quiet_logger): from yamlpath.enums import PathSeperators, PathSearchMethods from yamlpath.path import SearchTerms from yamlpath.func import get_yaml_data, get_yaml_editor from yamlpath.commands.yaml_paths import yield_children from itertools import zip_longest content = """some raw text value """ processor = get_yaml_editor() yaml_file = create_temp_yaml_file(tmp_path_factory, content) yaml_data = get_yaml_data(processor, quiet_logger, yaml_file) seen_anchors = [] assertions = ["/"] results = [] for assertion, path in zip_longest( assertions, yield_children(quiet_logger, yaml_data, SearchTerms(False, PathSearchMethods.STARTS_WITH, "*", "some"), PathSeperators.FSLASH, "", seen_anchors, search_anchors=False, include_key_aliases=False, include_value_aliases=False)): assert assertion == str(path)
def test_yield_seq_children_direct(self, tmp_path_factory, quiet_logger): from yamlpath.enums import PathSeperators, PathSearchMethods from yamlpath.path import SearchTerms from yamlpath.func import get_yaml_data, get_yaml_editor from yamlpath.commands.yaml_paths import yield_children from itertools import zip_longest content = """--- - &value Test value - value - *value """ processor = get_yaml_editor() yaml_file = create_temp_yaml_file(tmp_path_factory, content) (yaml_data, doc_loaded) = get_yaml_data(processor, quiet_logger, yaml_file) seen_anchors = [] assertions = ["/&value", "/[1]"] results = [] for assertion, path in zip_longest( assertions, yield_children(quiet_logger, yaml_data, SearchTerms(False, PathSearchMethods.EQUALS, "*", "value"), PathSeperators.FSLASH, "", seen_anchors, search_anchors=True, include_aliases=False)): assert assertion == str(path)
def test_yield_map_children_direct(self, tmp_path_factory, quiet_logger, include_aliases, assertions): from yamlpath.enums import PathSeperators, PathSearchMethods from yamlpath.path import SearchTerms from yamlpath.func import get_yaml_data, get_yaml_editor from yamlpath.commands.yaml_paths import yield_children from itertools import zip_longest content = """--- aliases: - &aValue val2 hash: key1: val1 key2: *aValue key3: val3 """ processor = get_yaml_editor() yaml_file = create_temp_yaml_file(tmp_path_factory, content) (yaml_data, doc_loaded) = get_yaml_data(processor, quiet_logger, yaml_file) seen_anchors = [] results = [] for assertion, path in zip_longest( assertions, yield_children(quiet_logger, yaml_data, SearchTerms(False, PathSearchMethods.EQUALS, "*", "anchor"), PathSeperators.FSLASH, "", seen_anchors, search_anchors=True, include_value_aliases=include_aliases)): assert assertion == str(path)
def create_searchterms_from_pathattributes( rhs: PathAttributes) -> SearchTerms: """Convert a PathAttributes instance to a SearchTerms instance.""" if isinstance(rhs, SearchTerms): newinst: SearchTerms = SearchTerms(rhs.inverted, rhs.method, rhs.attribute, rhs.term) return newinst raise AttributeError
def create_searchterms_from_pathattributes(rhs: PathAttributes) -> SearchTerms: """ Generates a new SearchTerms instance by copying SearchTerms attributes from a YAML Path segment's attributes. """ if isinstance(rhs, SearchTerms): newinst: SearchTerms = SearchTerms(rhs.inverted, rhs.method, rhs.attribute, rhs.term) return newinst raise AttributeError
def test_search_anchor(self): anchor_value = "anchor_name" node = PlainScalarString("anchored value", anchor=anchor_value) terms = SearchTerms(False, PathSearchMethods.CONTAINS, ".", "name") seen_anchors = [] search_anchors = True include_aliases = True assert search_anchor( node, terms, seen_anchors, search_anchors=search_anchors, include_aliases=include_aliases) == AnchorMatches.MATCH
def create_searchterms_from_pathattributes( rhs: PathAttributes) -> SearchTerms: """ Convert a PathAttributes instance to a SearchTerms instance. Parameters: 1. rhs (PathAttributes) PathAttributes instance to convert Returns: (SearchTerms) SearchTerms extracted from `rhs` """ if isinstance(rhs, SearchTerms): newinst: SearchTerms = SearchTerms(rhs.inverted, rhs.method, rhs.attribute, rhs.term) return newinst raise AttributeError
def test_nonexistant_path_search_method_error(self, quiet_logger): from enum import Enum from yamlpath.enums import PathSearchMethods names = [m.name for m in PathSearchMethods] + ['DNF'] PathSearchMethods = Enum('PathSearchMethods', names) yamldata = """--- top_scalar: value """ yaml = YAML() data = yaml.load(yamldata) processor = Processor(quiet_logger, data) with pytest.raises(NotImplementedError): nodes = list(processor._get_nodes_by_search( data, SearchTerms(True, PathSearchMethods.DNF, ".", "top_scalar") ))
def _expand_splats( yaml_path: str, segment_id: str, segment_type: Optional[PathSegmentTypes] = None ) -> tuple: """ Replace segment IDs with search operators when * is present. Parameters: 1. yaml_path (str) The full YAML Path being processed. 2. segment_id (str) The segment identifier to parse. 3. segment_type (Optional[PathSegmentTypes]) Pending predetermined type of the segment under evaluation. Returns: (tuple) Coallesced YAML Path segment. """ coal_type = segment_type coal_value: Union[str, SearchTerms, None] = segment_id if '*' in segment_id: splat_count = segment_id.count("*") splat_pos = segment_id.index("*") segment_len = len(segment_id) if splat_count == 1: if segment_len == 1: # /*/ -> [.=~/.*/] coal_type = PathSegmentTypes.SEARCH coal_value = SearchTerms( False, PathSearchMethods.REGEX, ".", ".*") elif splat_pos == 0: # /*text/ -> [.$text] coal_type = PathSegmentTypes.SEARCH coal_value = SearchTerms( False, PathSearchMethods.ENDS_WITH, ".", segment_id[1:]) elif splat_pos == segment_len - 1: # /text*/ -> [.^text] coal_type = PathSegmentTypes.SEARCH coal_value = SearchTerms( False, PathSearchMethods.STARTS_WITH, ".", segment_id[0:splat_pos]) else: # /te*xt/ -> [.=~/^te.*xt$/] coal_type = PathSegmentTypes.SEARCH coal_value = SearchTerms( False, PathSearchMethods.REGEX, ".", "^{}.*{}$".format( segment_id[0:splat_pos], segment_id[splat_pos + 1:])) elif splat_count == 2 and segment_len == 2: # Traversal operator coal_type = PathSegmentTypes.TRAVERSE coal_value = None elif splat_count > 1: # Multi-wildcard search search_term = "^" was_splat = False for char in segment_id: if char == "*": if was_splat: raise YAMLPathException( "The ** traversal operator has no meaning when" " combined with other characters", yaml_path, segment_id) was_splat = True search_term += ".*" else: was_splat = False search_term += char search_term += "$" coal_type = PathSegmentTypes.SEARCH coal_value = SearchTerms( False, PathSearchMethods.REGEX, ".", search_term) return (coal_type, coal_value)
def _parse_path(self, strip_escapes: bool = True ) -> Deque[PathSegment]: r""" Parse the YAML Path into its component segments. Breaks apart a stringified YAML Path into component segments, each identified by its type. See README.md for sample YAML Paths. Parameters: 1. strip_escapes (bool) True = Remove leading \ symbols, leaving only the "escaped" symbol. False = Leave all leading \ symbols intact. Returns: (deque) an empty queue or a queue of tuples, each identifying (PathSegmentTypes, segment_attributes). Raises: - `YAMLPathException` when the YAML Path is invalid """ yaml_path: str = self.original path_segments: deque = deque() segment_id: str = "" segment_type: Optional[PathSegmentTypes] = None demarc_stack: List[str] = [] escape_next: bool = False search_inverted: bool = False search_method: Optional[PathSearchMethods] = None search_attr: str = "" seeking_regex_delim: bool = False capturing_regex: bool = False pathsep: str = str(self.seperator) collector_level: int = 0 collector_operator: CollectorOperators = CollectorOperators.NONE seeking_collector_operator: bool = False # Empty paths yield empty queues if not yaml_path: return path_segments # Infer the first possible position for a top-level Anchor mark first_anchor_pos = 0 if self.seperator is PathSeperators.FSLASH and len(yaml_path) > 1: first_anchor_pos = 1 seeking_anchor_mark = yaml_path[first_anchor_pos] == "&" # Parse the YAML Path # pylint: disable=locally-disabled,too-many-nested-blocks for char in yaml_path: demarc_count = len(demarc_stack) if escape_next: # Pass-through; capture this escaped character escape_next = False elif capturing_regex: if char == demarc_stack[-1]: # Stop the RegEx capture capturing_regex = False demarc_stack.pop() continue # Pass-through; capture everything that isn't the present # RegEx delimiter. This deliberately means users cannot # escape the RegEx delimiter itself should it occur within # the RegEx; thus, users must select a delimiter that won't # appear within the RegEx (which is exactly why the user # gets to choose the delimiter). # pylint: disable=unnecessary-pass pass # pragma: no cover # The escape test MUST come AFTER the RegEx capture test so users # won't be forced into "The Backslash Plague". # (https://docs.python.org/3/howto/regex.html#the-backslash-plague) elif char == "\\": # Escape the next character escape_next = True if strip_escapes: continue elif ( char == " " and (demarc_count < 1 or demarc_stack[-1] not in ["'", '"']) ): # Ignore unescaped, non-demarcated whitespace continue elif seeking_regex_delim: # This first non-space symbol is now the RegEx delimiter seeking_regex_delim = False capturing_regex = True demarc_stack.append(char) demarc_count += 1 continue elif seeking_anchor_mark and char == "&": # Found an expected (permissible) ANCHOR mark seeking_anchor_mark = False segment_type = PathSegmentTypes.ANCHOR continue elif seeking_collector_operator and char in ['+', '-']: seeking_collector_operator = False if char == '+': collector_operator = CollectorOperators.ADDITION elif char == '-': collector_operator = CollectorOperators.SUBTRACTION continue elif char in ['"', "'"]: # Found a string demarcation mark if demarc_count > 0: # Already appending to an ongoing demarcated value if char == demarc_stack[-1]: # Close a matching pair demarc_stack.pop() demarc_count -= 1 # Record the element_id when all pairs have closed # unless there is no element_id. if demarc_count < 1: if segment_id: # Unless the element has already been # identified as a special type, assume it is a # KEY. if segment_type is None: segment_type = PathSegmentTypes.KEY path_segments.append( (segment_type, segment_id)) segment_id = "" segment_type = None continue else: # Embed a nested, demarcated component demarc_stack.append(char) demarc_count += 1 else: # Fresh demarcated value demarc_stack.append(char) demarc_count += 1 continue elif char == "(": seeking_collector_operator = False collector_level += 1 demarc_stack.append(char) demarc_count += 1 segment_type = PathSegmentTypes.COLLECTOR # Preserve nested collectors if collector_level == 1: continue elif collector_level > 0: if ( demarc_count > 0 and char == ")" and demarc_stack[-1] == "(" ): collector_level -= 1 demarc_count -= 1 demarc_stack.pop() if collector_level < 1: path_segments.append( (segment_type, CollectorTerms(segment_id, collector_operator))) segment_id = "" collector_operator = CollectorOperators.NONE seeking_collector_operator = True continue elif demarc_count == 0 and char == "[": # Array INDEX/SLICE or SEARCH if segment_id: # Record its predecessor element; unless it has already # been identified as a special type, assume it is a KEY. if segment_type is None: segment_type = PathSegmentTypes.KEY path_segments.append(self._expand_splats( yaml_path, segment_id, segment_type)) segment_id = "" demarc_stack.append(char) demarc_count += 1 segment_type = PathSegmentTypes.INDEX seeking_anchor_mark = True search_inverted = False search_method = None search_attr = "" continue elif ( demarc_count > 0 and demarc_stack[-1] == "[" and char in ["=", "^", "$", "%", "!", ">", "<", "~"] ): # Hash attribute search # pylint: disable=no-else-continue if char == "!": if search_inverted: raise YAMLPathException( "Double search inversion is meaningless at {}" .format(char) , yaml_path ) # Invert the search search_inverted = True continue elif char == "=": # Exact value match OR >=|<= segment_type = PathSegmentTypes.SEARCH if search_method is PathSearchMethods.LESS_THAN: search_method = PathSearchMethods.LESS_THAN_OR_EQUAL elif search_method is PathSearchMethods.GREATER_THAN: search_method = PathSearchMethods.GREATER_THAN_OR_EQUAL elif search_method is PathSearchMethods.EQUALS: # Allow == continue elif search_method is None: search_method = PathSearchMethods.EQUALS if segment_id: search_attr = segment_id segment_id = "" else: raise YAMLPathException( "Missing search operand before operator, {}" .format(char) , yaml_path ) else: raise YAMLPathException( "Unsupported search operator combination at {}" .format(char) , yaml_path ) continue # pragma: no cover elif char == "~": if search_method == PathSearchMethods.EQUALS: search_method = PathSearchMethods.REGEX seeking_regex_delim = True else: raise YAMLPathException( ("Unexpected use of {} operator. Please try =~ if" + " you mean to search with a Regular" + " Expression." ).format(char) , yaml_path ) continue # pragma: no cover elif not segment_id: # All tests beyond this point require an operand raise YAMLPathException( "Missing search operand before operator, {}" .format(char) , yaml_path ) elif char == "^": # Value starts with segment_type = PathSegmentTypes.SEARCH search_method = PathSearchMethods.STARTS_WITH if segment_id: search_attr = segment_id segment_id = "" continue elif char == "$": # Value ends with segment_type = PathSegmentTypes.SEARCH search_method = PathSearchMethods.ENDS_WITH if segment_id: search_attr = segment_id segment_id = "" continue elif char == "%": # Value contains segment_type = PathSegmentTypes.SEARCH search_method = PathSearchMethods.CONTAINS if segment_id: search_attr = segment_id segment_id = "" continue elif char == ">": # Value greater than segment_type = PathSegmentTypes.SEARCH search_method = PathSearchMethods.GREATER_THAN if segment_id: search_attr = segment_id segment_id = "" continue elif char == "<": # Value less than segment_type = PathSegmentTypes.SEARCH search_method = PathSearchMethods.LESS_THAN if segment_id: search_attr = segment_id segment_id = "" continue elif ( demarc_count > 0 and char == "]" and demarc_stack[-1] == "[" ): # Store the INDEX, SLICE, or SEARCH parameters if ( segment_type is PathSegmentTypes.INDEX and ':' not in segment_id ): try: idx = int(segment_id) except ValueError as wrap_ex: raise YAMLPathException( "Not an integer index: {}".format(segment_id) , yaml_path , segment_id ) from wrap_ex path_segments.append((segment_type, idx)) elif ( segment_type is PathSegmentTypes.SEARCH and search_method is not None ): # Undemarcate the search term, if it is so if segment_id and segment_id[0] in ["'", '"']: leading_mark = segment_id[0] if segment_id[-1] == leading_mark: segment_id = segment_id[1:-1] path_segments.append(( segment_type, SearchTerms(search_inverted, search_method, search_attr, segment_id) )) else: path_segments.append((segment_type, segment_id)) segment_id = "" segment_type = None demarc_stack.pop() demarc_count -= 1 search_method = None continue elif demarc_count < 1 and char == pathsep: # Do not store empty elements if segment_id: # Unless its type has already been identified as a special # type, assume it is a KEY. if segment_type is None: segment_type = PathSegmentTypes.KEY path_segments.append(self._expand_splats( yaml_path, segment_id, segment_type)) segment_id = "" segment_type = None continue segment_id += char seeking_anchor_mark = False seeking_collector_operator = False # Check for unmatched subpath demarcations if collector_level > 0: raise YAMLPathException( "YAML Path contains an unmatched () collector pair", yaml_path ) # Check for unterminated RegExes if capturing_regex: raise YAMLPathException( "YAML Path contains an unterminated Regular Expression", yaml_path ) # Check for mismatched demarcations if demarc_count > 0: raise YAMLPathException( "YAML Path contains at least one unmatched demarcation mark", yaml_path ) # Store the final element_id, which must have been a KEY if segment_id: # Unless its type has already been identified as a special # type, assume it is a KEY. if segment_type is None: segment_type = PathSegmentTypes.KEY path_segments.append(self._expand_splats( yaml_path, segment_id, segment_type)) return path_segments
def test_create_searchterms_from_pathattributes(self): st = SearchTerms(False, PathSearchMethods.EQUALS, ".", "key") assert str(st) == str(create_searchterms_from_pathattributes(st)) with pytest.raises(AttributeError): _ = create_searchterms_from_pathattributes("nothing-to-see-here")
def test_str(self, invert, method, attr, term, output): assert output == str(SearchTerms(invert, method, attr, term))