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",