Skip to content

Commit

Permalink
xul.xpath typing.
Browse files Browse the repository at this point in the history
  • Loading branch information
peteradrichem committed Jan 12, 2025
1 parent 228acc3 commit b626b0a
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 64 deletions.
18 changes: 7 additions & 11 deletions src/xul/cmd/xp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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


Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
107 changes: 54 additions & 53 deletions src/xul/xpath.py
Original file line number Diff line number Diff line change
@@ -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()"
Expand All @@ -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)
Expand All @@ -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]
Expand All @@ -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 <http://exslt.org/>
# EXSLT <https://exslt.github.io/>
ns_map = {
"date": "http://exslt.org/dates-and-times",
"dyn": "http://exslt.org/dynamic",
Expand Down

0 comments on commit b626b0a

Please sign in to comment.