diff --git a/src/xul/cmd/xp.py b/src/xul/cmd/xp.py index dd8d0b3..10ee39f 100644 --- a/src/xul/cmd/xp.py +++ b/src/xul/cmd/xp.py @@ -102,7 +102,8 @@ def xpath_class(el_tree: etree._ElementTree, xpath_exp: str, ns_map: dict[str, s :param el_tree: lxml ElementTree :param xpath_exp: XPath expression - :param ns_map: XML namespaces (xmlns) 'prefix': 'URI' dict + :param ns_map: XML namespace (prefix: URI) dictionary + :return: XPath result """ if xpath_obj := build_xpath(xpath_exp, ns_map): return etree_xpath(el_tree, xpath_obj) @@ -114,16 +115,17 @@ def eltree_xpath(el_tree: etree._ElementTree, xpath_exp: str, ns_map: dict[str, :param el_tree: lxml ElementTree :param xpath_exp: XPath expression - :param ns_map: XML namespaces (xmlns) 'prefix': 'URI' dict + :param ns_map: XML namespace (prefix: URI) dictionary + :return: XPath result """ try: return el_tree.xpath(xpath_exp, namespaces=ns_map) except etree.XPathEvalError as e: sys.stderr.write(f"{e}: {xpath_exp}\n") return None - # EXSLT function call errors (re:test positional arguments). + # Incorrect EXSLT function call (e.g. number of positional arguments for re:test). except TypeError as e: - sys.stderr.write(f"Type error {e}: {xpath_exp}\n") + sys.stderr.write(f"{xpath_exp} is invalid: {e}\n") return None @@ -153,7 +155,7 @@ def xp_prepare( def print_xmlns(ns_map: dict[str, str], root: etree._Element) -> None: """Print XML source namespaces (prefix: namespace URI). - :param ns_map: XML namespaces (xmlns) 'prefix': 'URI' dict + :param ns_map: XML namespace (prefix: URI) dictionary :param root: root (document) element """ if ns_map: @@ -322,12 +324,6 @@ def print_result_list(result_list, el_tree: etree._ElementTree, args: argparse.N else: print(f"prefix: {prefix:<8} URI: {uri}") - # ? - else: - print("**DEBUG fallback**") - print(type(node)) - print(node) - def print_result_header(source_name: str, xp_result) -> None: """Print header with XPath result summary. diff --git a/src/xul/xpath.py b/src/xul/xpath.py index 6b430f2..e2d9bb3 100644 --- a/src/xul/xpath.py +++ b/src/xul/xpath.py @@ -1,9 +1,13 @@ """XPath. -XPath with lxml +XPath: https://lxml.de/xpathxslt.html#xpath -The XPath result depends on the XPath expression used. +The XPath class: + https://lxml.de/xpathxslt.html#the-xpath-class + https://lxml.de/apidoc/lxml.etree.html#lxml.etree.XPath + +The return value types of XPath evaluations vary, depending on the XPath expression used: https://lxml.de/xpathxslt.html#xpath-return-values - attributes: "location/@attribute" - text nodes: "location/text()" @@ -16,62 +20,58 @@ """ from logging import getLogger +from typing import Optional, TextIO, Union -# pylint: disable=no-name-in-module -from lxml.etree import LIBXSLT_COMPILED_VERSION, XPath, XPathEvalError, XPathSyntaxError +from lxml import etree -# Import my own modules. from .etree import build_etree -# Module logging initialisation. logger = getLogger(__name__) -def build_xpath(xpath_exp, ns_map=None): +def build_xpath(xpath_exp: str, ns_map: Optional[dict[str, str]] = None) -> Optional[etree.XPath]: """Build an lxml.etree.XPath instance from an XPath expression. - xpath_exp -- XPath expression - ns_map -- XML namespace (prefix: URI) dictionary - - Uses the lxml XPath class: - https://lxml.de/xpathxslt.html#the-xpath-class - https://lxml.de/apidoc/lxml.etree.html#lxml.etree.XPath + :param xpath_exp: XPath expression + :param ns_map: XML namespace (prefix: URI) dictionary """ if not ns_map: ns_map = {} try: - return XPath(xpath_exp, namespaces=ns_map) + return etree.XPath(xpath_exp, namespaces=ns_map) # Handle (parsing) errors in XPath expression. # https://lxml.de/xpathxslt.html#error-handling - except XPathSyntaxError as e: + except etree.XPathSyntaxError as e: logger.error("%s: %s", e, xpath_exp) return None -def etree_xpath(el_tree, xpath_obj): +def etree_xpath(el_tree: etree._ElementTree, xpath_obj: etree.XPath): """Apply XPath instance to an ElementTree. - el_tree -- ElementTree (lxml.etree._ElementTree) - xpath_obj -- lxml.etree.XPath instance; see build_xpath() + :param el_tree: lxml ElementTree + :param xpath_obj: lxml.etree.XPath instance; see build_xpath() + :return: XPath result """ try: return xpath_obj(el_tree) # Handle errors in evaluating an XPath expression. # https://lxml.de/xpathxslt.html#error-handling - except XPathEvalError as e: + except etree.XPathEvalError as e: logger.error("%s: %s", e, xpath_obj) return None - # Incorrect EXSLT function call (e.g. number of arguments for re:match). + # Incorrect EXSLT function call (e.g. number of positional arguments for re:match). except TypeError as e: - logger.error("Type error %s: %s", e, xpath_obj) + logger.error("%s is invalid: %s", xpath_obj, e) return None -def call_xpath(xml_source, xpath_obj): - """Apply lxml.etree.XPath on an XML source. +def call_xpath(xml_source: Union[TextIO, str], xpath_obj: etree.XPath): + """Apply lxml.etree.XPath to an XML source. - xml_source -- XML file, file-like object or URL - xpath_obj -- lxml.etree.XPath instance; see build_xpath() + :param xml_source: XML file, file-like object or URL + :param xpath_obj: lxml.etree.XPath instance; see build_xpath() + :return: XPath result """ # Parse an XML source into an XML Document Object Model. el_tree = build_etree(xml_source, lenient=False) @@ -85,27 +85,27 @@ def call_xpath(xml_source, xpath_obj): return xpath_result -def xml_xpath(xml_source, xpath_exp): - """Apply XPath expression to an XML source. +def xml_xpath(xml_source: Union[TextIO, str], xpath_exp: str): + """Apply XPath expression to an XML source with call_xpath(). - xml_source -- XML file, file-like object or URL - xpath_exp -- XPath expression - - Uses call_xpath() + :param xml_source: XML file, file-like object or URL + :param xpath_exp: XPath expression + :return: XPath result """ - xpath_obj = build_xpath(xpath_exp) - if not xpath_obj: - return None + if xpath_obj := build_xpath(xpath_exp): + return call_xpath(xml_source, xpath_obj) - return call_xpath(xml_source, xpath_obj) + return None -def update_ns_map(ns_map, elm, none_prefix="default"): +def update_ns_map( + ns_map: dict[str, str], elm: etree._Element, none_prefix: str = "default" +) -> None: """Update XPath namespace prefix mapping with element namespaces. - ns_map -- an XML namespace prefix mapping - elm -- element with namespaces - none_prefix -- prefix for the default namespace in XPath + :param ns_map: XML namespace (prefix: URI) dictionary + :param elm: element with namespaces + :param none_prefix: prefix for the default namespace in XPath Element namespaces: - xmlns, default namespace (None prefix) URI: elm.nsmap[None] @@ -121,35 +121,36 @@ def update_ns_map(ns_map, elm, none_prefix="default"): https://lxml.de/xpathxslt.html#namespaces-and-prefixes """ for key in elm.nsmap: - if not key: + if key is None: # XPath prefix for element default namespace. if none_prefix not in ns_map: ns_map[none_prefix] = elm.nsmap[key] - elif key not in ns_map: - # Protect the XPath default namespace prefix. - if not key == none_prefix: - ns_map[key] = elm.nsmap[key] + # Protect the XPath default namespace prefix. + elif not (key in ns_map or key == none_prefix): + ns_map[key] = elm.nsmap[key] -def namespaces(el_tree, exslt=False, none_prefix="default"): +def namespaces( + el_tree: etree._ElementTree, exslt: bool = False, none_prefix: str = "default" +) -> dict[str, str]: """Collect all XML namespaces (xmlns) in ElementTree. - el_tree -- ElementTree (lxml.etree._ElementTree) - exslt -- add EXSLT XML namespace prefixes (libxslt 1.1.25 and newer) - none_prefix -- prefix for the default namespace in XPath + :param el_tree: lxml ElementTree + :param exslt: add EXSLT XML namespace prefixes (libxslt 1.1.25 and newer) + :param none_prefix: prefix for the default namespace in XPath - Return XML namespaces (xmlns) 'prefix: URI' dict. + Return XML namespaces (xmlns) 'prefix: URI' mapping. Namespaces. https://lxml.de/tutorial.html#namespaces """ if exslt: - if LIBXSLT_COMPILED_VERSION < (1, 1, 25): + if etree.LIBXSLT_COMPILED_VERSION < (1, 1, 25): logger.warning( - "EXSLT requires libxslt 1.1.25 or higher. " + "lxml is compiled against libxslt %s", - ".".join(str(n) for n in LIBXSLT_COMPILED_VERSION), + "EXSLT requires libxslt 1.1.25 or higher. lxml is compiled against libxslt %s", + ".".join(str(n) for n in etree.LIBXSLT_COMPILED_VERSION), ) - # EXSLT + # EXSLT ns_map = { "date": "http://exslt.org/dates-and-times", "dyn": "http://exslt.org/dynamic",