text: """
  Implement KeywordSearches.
  
  This is a static library of generally-useful code for searching data based on
  pre-defined keywords (in the programming language sense).
  
  Copyright 2020 William W. Kimball, Jr. MBA MSIS
  """
  from typing import Any, Dict, Generator, List
  
  from ruamel.yaml.comments import CommentedMap
  
  from yamlpath.types import AncestryEntry, PathSegment
  from yamlpath.enums import PathSearchKeywords, PathSearchMethods
  from yamlpath.common import Anchors, Nodes, Searches
  from yamlpath.path import SearchKeywordTerms
  from yamlpath.exceptions import YAMLPathException
  from yamlpath.wrappers import NodeCoords
  from yamlpath import YAMLPath
  
  class KeywordSearches:
      """Helper methods for common data searching operations."""
  
      @staticmethod
      def search_matches(
          terms: SearchKeywordTerms, haystack: Any, yaml_path: YAMLPath,
          **kwargs: Any
      ) -> Generator[NodeCoords, None, None]:
          """
          Perform a keyword search.
  
          Parameters:
          1. terms (SearchKeywordTerms) The search operation to perform
          2. haystack (Any) The data to evaluate
          3. yaml_path (YAMLPath) YAML Path containing this search keyword
  
          Keyword Arguments:  See each of the called KeywordSearches methods
  
          Returns:  (Generator[NodeCoords, None, None]) Matching data as it is
              generated
          """
          invert: bool = terms.inverted
          keyword: PathSearchKeywords = terms.keyword
          parameters: List[str] = terms.parameters
          nc_matches: Generator[NodeCoords, None, None]
  
          if keyword is PathSearchKeywords.HAS_CHILD:
              nc_matches = KeywordSearches.has_child(
                  haystack, invert, parameters, yaml_path, **kwargs)
          elif keyword is PathSearchKeywords.NAME:
              nc_matches = KeywordSearches.name(
                  invert, parameters, yaml_path, **kwargs)
          elif keyword is PathSearchKeywords.MAX:
              nc_matches = KeywordSearches.max(
                  haystack, invert, parameters, yaml_path, **kwargs)
          elif keyword is PathSearchKeywords.MIN:
              nc_matches = KeywordSearches.min(
                  haystack, invert, parameters, yaml_path, **kwargs)
          elif keyword is PathSearchKeywords.PARENT:
              nc_matches = KeywordSearches.parent(
                  haystack, invert, parameters, yaml_path, **kwargs)
          else:
              raise YAMLPathException(
                  "Unsupported search keyword {} in".format(keyword),
                  str(yaml_path))
  
          for nc_match in nc_matches:
              yield nc_match
  
      @staticmethod
      # pylint: disable=locally-disabled,too-many-locals
      def has_child(
          data: Any, invert: bool, parameters: List[str], yaml_path: YAMLPath,
          **kwargs: Any
      ) -> Generator[NodeCoords, None, None]:
          """
          Indicate whether data has a named or anchored child.
  
          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
          """
          # There must be exactly one parameter
          param_count = len(parameters)
          if param_count != 1:
              raise YAMLPathException(
                  ("Invalid parameter count to {}; {} required, got {} in"
                   " YAML Path").format(
                       PathSearchKeywords.HAS_CHILD, 1, param_count),
                  str(yaml_path))
          match_key = parameters[0]
  
          if match_key[0] == "&":
              matches = KeywordSearches._has_anchored_child(
                  data, invert, parameters, yaml_path, **kwargs)
          else:
              matches = KeywordSearches._has_concrete_child(
                  data, invert, parameters, yaml_path, **kwargs)
  
          for match in matches:
              yield match
  
      @staticmethod
      # pylint: disable=locally-disabled,too-many-locals
      def _has_concrete_child(
          data: Any, invert: bool, parameters: List[str], yaml_path: YAMLPath,
          **kwargs: Any
      ) -> Generator[NodeCoords, None, None]:
          """
          Indicate whether data has a named child.
  
          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)
  
          match_key = parameters[0]
  
          # Against a map, this will return nodes which have an immediate
          # child key exactly named as per parameters.  When inverted, only
          # parents with no such key are yielded.
          if isinstance(data, dict):
              child_present = data is not None and match_key in data
              if (
                  (invert and not child_present) or
                  (child_present and not invert)
              ):
                  yield NodeCoords(
                      data, parent, parentref, translated_path, ancestry,
                      relay_segment)
  
          # Against a list, this will merely require an exact match between
          # parameters and any list elements.  When inverted, every
          # non-matching element is yielded.
          elif isinstance(data, list):
              # Against an AoH, this will scan each element's immediate children,
              # treating and yielding as if this search were performed directly
              # against each map in the list.
              if Nodes.node_is_aoh(data):
                  for idx, ele in enumerate(data):
                      next_path = translated_path.append("[{}]".format(str(idx)))
                      for aoh_match in KeywordSearches._has_concrete_child(
                          ele, invert, parameters, yaml_path,
                          parent=data, parentref=idx, translated_path=next_path
                      ):
                          yield aoh_match
                  return
  
              child_present = match_key in data
              if (
                  (invert and not child_present) or
                  (child_present and not invert)
              ):
                  yield NodeCoords(
                      data, parent, parentref, translated_path, ancestry,
                      relay_segment)
  
          elif data is None:
              if invert:
                  yield NodeCoords(
                      data, parent, parentref, translated_path, ancestry,
                      relay_segment)
  
          else:
              raise YAMLPathException(
                  ("{} data has no child nodes in YAML Path").format(type(data)),
                  str(yaml_path))
  
      @staticmethod
      # pylint: disable=locally-disabled,too-many-locals,too-many-branches,too-many-statements
      def _has_anchored_child(
          data: Any, invert: bool, parameters: List[str], yaml_path: YAMLPath,
          **kwargs: Any
      ) -> Generator[NodeCoords, None, None]:
          """
          Indicate whether data has an anchored child.
  
          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)
  
          match_key = parameters[0]
          anchor_name = match_key[1:] if match_key[0] == "&" else match_key
  
          if isinstance(data, CommentedMap):
              # Look for YAML Merge Keys by the Anchor name
              all_data = ancestry[0][0] if len(ancestry) > 0 else data
              all_anchors: Dict[str, Any] = {}
              Anchors.scan_for_anchors(all_data, all_anchors)
              compare_node = (all_anchors[anchor_name]
                              if anchor_name in all_anchors
                              else None)
              is_ymk_anchor = (
                  compare_node is not None and isinstance(compare_node, dict))
  
              if is_ymk_anchor:
                  child_present = False
                  if hasattr(data, "merge") and len(data.merge) > 0:
                      # Ignore comparision if there is no source
                      for (idx, merge_node) in data.merge:
                          if merge_node == compare_node:
                              child_present = True
                              break
  
                  if (
                      (invert and not child_present) or
                      (child_present and not invert)
                  ):
                      yield NodeCoords(
                          data, parent, parentref, translated_path,
                          ancestry, relay_segment)
                      return
  
              # Look for Anchored keys; include merged nodes
              else:
                  child_present = False
                  for (key, val) in data.items():
                      key_anchor = Anchors.get_node_anchor(key)
                      val_anchor = Anchors.get_node_anchor(val)
                      if key_anchor and key_anchor == anchor_name:
                          child_present = True
                          break
                      if val_anchor and val_anchor == anchor_name:
                          child_present = True
                          break
  
                  if (
                      (invert and not child_present) or
                      (child_present and not invert)
                  ):
                      yield NodeCoords(
                          data, parent, parentref, translated_path,
                          ancestry, relay_segment)
  
          elif Nodes.node_is_aoh(data, accept_nulls=True):
              for idx, ele in enumerate(data):
                  if ele is None:
                      continue
  
                  next_path = translated_path.append("[{}]".format(str(idx)))
                  next_ancestry = ancestry + [(data, idx)]
                  for aoh_match in KeywordSearches._has_anchored_child(
                      ele, invert, parameters, yaml_path,
                      parent=data, parentref=idx, translated_path=next_path,
                      ancestry=next_ancestry
                  ):
                      yield aoh_match
  
          elif isinstance(data, list):
              child_present = False
              for ele in data:
                  ele_anchor = Anchors.get_node_anchor(ele)
                  if ele_anchor and ele_anchor == anchor_name:
                      child_present = True
                      break
  
              if (
                  (invert and not child_present) or
                  (child_present and not invert)
              ):
                  yield NodeCoords(
                      data, parent, parentref, translated_path,
                      ancestry, relay_segment)
  
      @staticmethod
      # pylint: disable=locally-disabled,too-many-locals
      def name(
          invert: bool, parameters: List[str], yaml_path: YAMLPath,
          **kwargs: Any
      ) -> Generator[NodeCoords, None, None]:
          """
          Match only the key-name of the present node.
  
          Parameters:
          1. invert (bool) Invert the evaluation
          2. parameters (List[str]) Parsed parameters
          3. 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 are no parameters
          param_count = len(parameters)
          if param_count > 1:
              raise YAMLPathException((
                  "Invalid parameter count to {}(); {} are permitted, "
                  " got {} in YAML Path"
                  ).format(PathSearchKeywords.NAME, 0, param_count),
                  str(yaml_path))
  
          if invert:
              raise YAMLPathException((
                  "Inversion is meaningless to {}()"
                  ).format(PathSearchKeywords.NAME),
                  str(yaml_path))
  
          yield NodeCoords(
              parentref, parent, parentref, translated_path, ancestry,
              relay_segment)
  
      @staticmethod
      # pylint: disable=locally-disabled,too-many-locals,too-many-branches,too-many-statements
      def max(
          data: Any, invert: bool, parameters: List[str], yaml_path: YAMLPath,
          **kwargs: Any
      ) -> Generator[NodeCoords, None, None]:
          """
          Find whichever nodes/elements have a maximum 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.MAX, 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.MAX),
                      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.GREATER_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.MAX),
                      str(yaml_path))
  
              for key, val in data.items():
                  next_path = (
                      translated_path + YAMLPath.escape_path_section(
                          key, translated_path.seperator))
                  next_ancestry = ancestry + [(data, key)]
                  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.GREATER_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.MAX),
                          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.MAX),
                      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.GREATER_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
  
  
      @staticmethod
      # pylint: disable=locally-disabled,too-many-locals,too-many-branches,too-many-statements
      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
  
      @staticmethod
      # pylint: disable=locally-disabled,too-many-locals
      def parent(
          data: Any, invert: bool, parameters: List[str], yaml_path: YAMLPath,
          **kwargs: Any
      ) -> Generator[NodeCoords, None, None]:
          """
          Climb back up N parent levels in the data hierarchy.
  
          Parameters:
          1. data (Any) The data to evaluate
          2. invert (bool) Invert the evaluation; not possible for parent()
          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 {}([STEPS]); up to {} permitted, "
                  " got {} in YAML Path"
                  ).format(PathSearchKeywords.PARENT, 1, param_count),
                  str(yaml_path))
  
          if invert:
              raise YAMLPathException((
                  "Inversion is meaningless to {}([STEPS])"
                  ).format(PathSearchKeywords.PARENT),
                  str(yaml_path))
  
          parent_levels: int = 1
          ancestry_len: int = len(ancestry)
          steps_max = ancestry_len
          if param_count > 0:
              try:
                  parent_levels = int(parameters[0])
              except ValueError as ex:
                  raise YAMLPathException((
                      "Invalid parameter passed to {}([STEPS]), {}; must be"
                      " unset or an integer number indicating how may parent"
                      " STEPS to climb in YAML Path"
                      ).format(PathSearchKeywords.PARENT, parameters[0]),
                      str(yaml_path)) from ex
  
          if parent_levels > steps_max:
              raise YAMLPathException((
                  "Cannot {}([STEPS]) higher than the document root.  {} steps"
                  " requested when {} available in YAML Path"
                  ).format(PathSearchKeywords.PARENT, parent_levels, steps_max),
                  str(yaml_path))
  
          if parent_levels < 1:
              # parent(0) is the present node
              yield NodeCoords(
                  data, parent, parentref, translated_path, ancestry,
                  relay_segment)
          else:
              for _ in range(parent_levels):
                  translated_path.pop()
                  (data, _) = ancestry.pop()
                  ancestry_len -= 1
  
              parentref = ancestry[-1][1] if ancestry_len > 0 else None
              parent = ancestry[-1][0] if ancestry_len > 0 else None
              yield NodeCoords(
                  data, parent, parentref, translated_path, ancestry,
                  relay_segment)
  