def within_distance(node, context): confirm_args_count(node, 4) property_name, coords, distance, unit = node.args property_name = _property_name_to_string(property_name, node) try: geo_point = GeoPoint.from_string(coords, flexible=True) except BadValueError as e: raise XPathFunctionException( _(f"The second argument to '{node.name}' must be valid coordinates" ), serialize(node)) from e try: distance = float(distance) except ValueError as e: raise XPathFunctionException( _(f"The third argument to '{node.name}' must be a number, got '{distance}'" ), serialize(node)) from e if unit not in DISTANCE_UNITS: raise XPathFunctionException( _(f"'{unit}' is not a valid distance unit. Expected one of {', '.join(DISTANCE_UNITS)}" ), serialize(node)) return case_property_geo_distance(property_name, geo_point, **{unit: distance})
def _property_name_to_string(value, node): if isinstance(value, Step): return serialize(value) if isinstance(value, str): return value raise XPathFunctionException( _(f"The first argument to '{node.name}' must be a valid case property name" ), serialize(node))
def _create_xml_node(xast, node, context, insert_index=None): if isinstance(xast, ast.Step): if isinstance(xast.node_test, ast.NameTest): # check the predicates (if any) to verify they're constructable for pred in xast.predicates: if not _predicate_is_constructible(pred): msg = ( "Missing element for '%s', and node creation is " + "supported only for simple child and attribute " + "nodes with simple predicates." ) % (serialize(xast),) raise Exception(msg) # create the node itself if xast.axis in (None, "child"): new_node = _create_child_node(node, context, xast, insert_index) elif xast.axis in ("@", "attribute"): new_node = _create_attribute_node(node, context, xast) # and create any nodes necessary for the predicates for pred in xast.predicates: _construct_predicate(pred, new_node, context) return new_node # if this is a text() node, we don't need to create anything further # return the node that will be parent to text() elif _is_text_nodetest(xast): return node elif isinstance(xast, ast.BinaryExpression): if xast.op == "/": left_xpath = serialize(xast.left) left_node = _find_xml_node(left_xpath, node, context) if left_node is None: left_node = _create_xml_node(xast.left, node, context) return _create_xml_node(xast.right, left_node, context) # anything else, throw an exception: msg = ( "Missing element for '%s', and node creation is supported " + "only for simple child and attribute nodes." ) % (serialize(xast),) raise Exception(msg)
def _remove_xml(xast, node, context, if_empty=False): '''Remove a node or attribute. For multipart XPaths that are constructible by :mod:`eulxml.xmlmap`, the corresponding nodes will be removed if they are empty (other than predicates specified in the XPath). :param xast: parsed xpath (xpath abstract syntax tree) from :mod:`eulxml.xpath` :param node: lxml node relative to which xast should be removed :param context: any context required for the xpath (e.g., namespace definitions) :param if_empty: optional boolean; only remove a node if it is empty (no attributes and no child nodes); defaults to False :returns: True if something was deleted ''' if isinstance(xast, ast.Step): if isinstance(xast.node_test, ast.NameTest): if xast.axis in (None, 'child'): return _remove_child_node(node, context, xast, if_empty=if_empty) elif xast.axis in ('@', 'attribute'): return _remove_attribute_node(node, context, xast) # special case for text() # since it can't be removed, at least clear out any value in the text node elif _is_text_nodetest(xast): node.text = '' return True # If the xpath is a multi-step path (e.g., foo[@id="a"]/bar[@id="b"]/baz), # remove the leaf node. If the remaining portions of that path # could have been constructed when setting the node and are empty # (other than any predicates defined in the xpath), remove them as well. elif isinstance(xast, ast.BinaryExpression): if xast.op == '/': left_xpath = serialize(xast.left) left_node = _find_xml_node(left_xpath, node, context) if left_node is not None: # remove the last element in the xpath removed = _remove_xml( xast.right, left_node, context, if_empty=if_empty) # honor current if_empty flag # If the left portion of the xpath is something we # could have constructed, remove it if it is empty. if removed and _predicate_is_constructible(left_xpath): _remove_xml(xast.left, node, context, if_empty=True) # report on whether the leaf node was removed or not, # regardless of what was done with left portion of the path return removed return False
def _create_xml_node(xast, node, context, insert_index=None): if isinstance(xast, ast.Step): if isinstance(xast.node_test, ast.NameTest): # check the predicates (if any) to verify they're constructable for pred in xast.predicates: if not _predicate_is_constructible(pred): msg = ("Missing element for '%s', and node creation is " + "supported only for simple child and attribute " + "nodes with simple predicates.") % ( serialize(xast), ) raise Exception(msg) # create the node itself if xast.axis in (None, 'child'): new_node = _create_child_node(node, context, xast, insert_index) elif xast.axis in ('@', 'attribute'): new_node = _create_attribute_node(node, context, xast) # and create any nodes necessary for the predicates for pred in xast.predicates: _construct_predicate(pred, new_node, context) return new_node # if this is a text() node, we don't need to create anything further # return the node that will be parent to text() elif _is_text_nodetest(xast): return node elif isinstance(xast, ast.BinaryExpression): if xast.op == '/': left_xpath = serialize(xast.left) left_node = _find_xml_node(left_xpath, node, context) if left_node is None: left_node = _create_xml_node(xast.left, node, context) return _create_xml_node(xast.right, left_node, context) # anything else, throw an exception: msg = ("Missing element for '%s', and node creation is supported " + \ "only for simple child and attribute nodes.") % (serialize(xast),) raise Exception(msg)
def _remove_xml(xast, node, context, if_empty=False): '''Remove a node or attribute. For multipart XPaths that are constructible by :mod:`eulxml.xmlmap`, the corresponding nodes will be removed if they are empty (other than predicates specified in the XPath). :param xast: parsed xpath (xpath abstract syntax tree) from :mod:`eulxml.xpath` :param node: lxml node relative to which xast should be removed :param context: any context required for the xpath (e.g., namespace definitions) :param if_empty: optional boolean; only remove a node if it is empty (no attributes and no child nodes); defaults to False :returns: True if something was deleted ''' if isinstance(xast, ast.Step): if isinstance(xast.node_test, ast.NameTest): if xast.axis in (None, 'child'): return _remove_child_node(node, context, xast, if_empty=if_empty) elif xast.axis in ('@', 'attribute'): return _remove_attribute_node(node, context, xast) # special case for text() # since it can't be removed, at least clear out any value in the text node elif _is_text_nodetest(xast): node.text = '' return True # If the xpath is a multi-step path (e.g., foo[@id="a"]/bar[@id="b"]/baz), # remove the leaf node. If the remaining portions of that path # could have been constructed when setting the node and are empty # (other than any predicates defined in the xpath), remove them as well. elif isinstance(xast, ast.BinaryExpression): if xast.op == '/': left_xpath = serialize(xast.left) left_node = _find_xml_node(left_xpath, node, context) if left_node is not None: # remove the last element in the xpath removed = _remove_xml(xast.right, left_node, context, if_empty=if_empty) # honor current if_empty flag # If the left portion of the xpath is something we # could have constructed, remove it if it is empty. if removed and _predicate_is_constructible(left_xpath): _remove_xml(xast.left, node, context, if_empty=True) # report on whether the leaf node was removed or not, # regardless of what was done with left portion of the path return removed return False
def _remove_predicates(xast, node, context): '''Remove any constructible predicates specified in the xpath relative to the specified node. :param xast: parsed xpath (xpath abstract syntax tree) from :mod:`eulxml.xpath` :param node: lxml element which predicates will be removed from :param context: any context required for the xpath (e.g., namespace definitions) :returns: updated a copy of the xast without the predicates that were successfully removed ''' # work from a copy since it may be modified xast_c = deepcopy(xast) # check if predicates are constructable for pred in list(xast_c.predicates): # ignore predicates that we can't construct if not _predicate_is_constructible(pred): continue if isinstance(pred, ast.BinaryExpression): # TODO: support any other predicate operators? # predicate construction supports op / # If the xml still matches the constructed value, remove it. # e.g., @type='text' or level='leaf' if pred.op == '=' and \ node.xpath(serialize(pred), **context) is True: # predicate xpath returns True if node=value if isinstance(pred.left, ast.Step): if pred.left.axis in ('@', 'attribute'): if _remove_attribute_node(node, context, pred.left): # remove from the xast xast_c.predicates.remove(pred) elif pred.left.axis in (None, 'child'): if _remove_child_node(node, context, pred.left, if_empty=True): xast_c.predicates.remove(pred) elif isinstance(pred.left, ast.BinaryExpression): # e.g., level/@id='b' or level/deep='deeper' # - value has already been checked by xpath above, # so just remove the multipart path _remove_xml(pred.left, node, context, if_empty=True) return xast_c
def set(self, xpath, xast, node, context, mapper, value): xvalue = mapper.to_xml(value) match = _find_xml_node(xpath, node, context) if xvalue is None: # match must be None. if it exists, delete it. if match is not None: removed = _remove_xml(xast, node, context) # if a node can't be removed, warn since it could have unexpected results if not removed: logger.warn("""Could not remove xml for '%s' from %r""" % (serialize(xast), node)) else: if match is None: match = _create_xml_node(xast, node, context) # terminal (rightmost) step informs how we update the xml step = _find_terminal_step(xast) _set_in_xml(match, xvalue, context, step)
def set(self, xpath, xast, node, context, mapper, value): xvalue = mapper.to_xml(value) match = _find_xml_node(xpath, node, context) if xvalue is None: # match must be None. if it exists, delete it. if match is not None: removed = _remove_xml(xast, node, context) # if a node can't be removed, warn since it could have unexpected results if not removed: logger.warn('''Could not remove xml for '%s' from %r''' % \ (serialize(xast), node)) else: if match is None: match = _create_xml_node(xast, node, context) # terminal (rightmost) step informs how we update the xml step = _find_terminal_step(xast) _set_in_xml(match, xvalue, context, step)
def _remove_xml(xast, node, context): 'Remove a node or attribute; returns True when something is deleted' if isinstance(xast, ast.Step): if isinstance(xast.node_test, ast.NameTest): if xast.axis in (None, 'child'): return _remove_child_node(node, context, xast) elif xast.axis in ('@', 'attribute'): return _remove_attribute_node(node, context, xast) # special case for text() # since it can't be removed, at least clear out any value in the text node elif _is_text_nodetest(xast): node.text = '' return True elif isinstance(xast, ast.BinaryExpression): if xast.op == '/': left_xpath = serialize(xast.left) left_node = _find_xml_node(left_xpath, node, context) if left_node is not None: return _remove_xml(xast.right, left_node, context) return False
def _remove_child_node(node, context, xast, if_empty=False): """Remove a child node based on the specified xpath. :param node: lxml element relative to which the xpath will be interpreted :param context: any context required for the xpath (e.g., namespace definitions) :param xast: parsed xpath (xpath abstract syntax tree) from :mod:`eulxml.xpath` :param if_empty: optional boolean; only remove a node if it is empty (no attributes and no child nodes); defaults to False :returns: True if a node was deleted """ xpath = serialize(xast) child = _find_xml_node(xpath, node, context) if child is not None: # if if_empty was specified and node has children or attributes # other than any predicates defined in the xpath, don't remove if if_empty is True and not _empty_except_predicates(xast, child, context): return False node.remove(child) return True
def _remove_child_node(node, context, xast, if_empty=False): '''Remove a child node based on the specified xpath. :param node: lxml element relative to which the xpath will be interpreted :param context: any context required for the xpath (e.g., namespace definitions) :param xast: parsed xpath (xpath abstract syntax tree) from :mod:`eulxml.xpath` :param if_empty: optional boolean; only remove a node if it is empty (no attributes and no child nodes); defaults to False :returns: True if a node was deleted ''' xpath = serialize(xast) child = _find_xml_node(xpath, node, context) if child is not None: # if if_empty was specified and node has children or attributes # other than any predicates defined in the xpath, don't remove if if_empty is True and \ not _empty_except_predicates(xast, child, context): return False node.remove(child) return True
def confirm_args_count(node, expected): actual = len(node.args) if actual != expected: raise XPathFunctionException( _(f"The '{node.name}' function accepts exactly {expected} arguments, got {actual}" ), serialize(node))
def round_trip(self, xpath_str): xp = xpath.parse(xpath_str) self.assertEqual(xpath_str, serialize(xp))
def _remove_child_node(node, context, xast): xpath = serialize(xast) child = _find_xml_node(xpath, node, context) if child is not None: node.remove(child) return True