File 004-xmlapi-split-out-xmlbase.py-and-xmllibxml2.py.patch of Package virt-manager
Subject: xmlapi: split out xmlbase.py and xmllibxml2.py
From: Cole Robinson crobinso@redhat.com Wed Sep 17 10:38:12 2025 -0400
Date: Wed Oct 1 11:22:35 2025 -0400:
Git: d4988b02efb8bba91fd55614fbbff11b3a915d44
We will be adding new XMLAPI implementations shortly and separate
files helps with code org
Signed-off-by: Cole Robinson <crobinso@redhat.com>
diff --git a/virtinst/meson.build b/virtinst/meson.build
index f48daf696..d8be0e895 100644
--- a/virtinst/meson.build
+++ b/virtinst/meson.build
@@ -24,7 +24,9 @@ virtinst_sources = files(
'virtinstall.py',
'virtxml.py',
'xmlapi.py',
+ 'xmlbase.py',
'xmlbuilder.py',
+ 'xmllibxml2.py',
'xmlutil.py',
)
diff --git a/virtinst/xmlapi.py b/virtinst/xmlapi.py
index bbeba325c..c20718c08 100644
--- a/virtinst/xmlapi.py
+++ b/virtinst/xmlapi.py
@@ -4,439 +4,6 @@
# This work is licensed under the GNU GPLv2 or later.
# See the COPYING file in the top-level directory.
-import libxml2
+from .xmllibxml2 import Libxml2API
-from . import xmlutil
-from .logger import log
-
-# pylint: disable=protected-access
-
-
-class _XPathSegment:
- """
- Class representing a single 'segment' of an xpath string. For example,
- the xpath:
-
- ./qemu:foo/bar[1]/baz[@somepro='someval']/@finalprop
-
- will be split into the following segments:
-
- #1: nodename=., fullsegment=.
- #2: nodename=foo, nsname=qemu, fullsegment=qemu:foo
- #3: nodename=bar, condition_num=1, fullsegment=bar[1]
- #4: nodename=baz, condition_prop=somepro, condition_val=someval,
- fullsegment=baz[@somepro='somval']
- #5: nodename=finalprop, is_prop=True, fullsegment=@finalprop
- """
-
- def __init__(self, fullsegment):
- self.fullsegment = fullsegment
- self.nodename = fullsegment
-
- self.condition_prop = None
- self.condition_val = None
- self.condition_num = None
- if "[" in self.nodename:
- self.nodename, cond = self.nodename.strip("]").split("[")
- if "=" in cond:
- (cprop, cval) = cond.split("=")
- self.condition_prop = cprop.strip("@")
- self.condition_val = cval.strip("'")
- elif cond.isdigit():
- self.condition_num = int(cond)
-
- self.is_prop = self.nodename.startswith("@")
- if self.is_prop:
- self.nodename = self.nodename[1:]
-
- self.nsname = None
- if ":" in self.nodename:
- self.nsname, self.nodename = self.nodename.split(":")
-
-
-class _XPath:
- """
- Helper class for performing manipulations of XPath strings. Splits
- the xpath into segments.
- """
-
- def __init__(self, fullxpath):
- self.fullxpath = fullxpath
- self.segments = []
- for s in self.fullxpath.split("/"):
- if s == "..":
- # Resolve and flatten .. in xpaths
- self.segments = self.segments[:-1]
- continue
- self.segments.append(_XPathSegment(s))
-
- self.is_prop = self.segments[-1].is_prop
- self.propname = self.is_prop and self.segments[-1].nodename or None
- if self.is_prop:
- self.segments = self.segments[:-1]
- self.xpath = self.join(self.segments)
-
- @staticmethod
- def join(segments):
- return "/".join(s.fullsegment for s in segments)
-
- def parent_xpath(self):
- return self.join(self.segments[:-1])
-
-
-class _XMLBase:
- NAMESPACES = {}
-
- @classmethod
- def register_namespace(cls, nsname, uri):
- cls.NAMESPACES[nsname] = uri
-
- def copy_api(self):
- raise NotImplementedError()
-
- def count(self, xpath):
- raise NotImplementedError()
-
- def _find(self, fullxpath):
- raise NotImplementedError()
-
- def _node_tostring(self, node):
- raise NotImplementedError()
-
- def _node_get_text(self, node):
- raise NotImplementedError()
-
- def _node_set_text(self, node, setval):
- raise NotImplementedError()
-
- def _node_get_property(self, node, propname):
- raise NotImplementedError()
-
- def _node_set_property(self, node, propname, setval):
- raise NotImplementedError()
-
- def _node_new(self, xpathseg, parentnode):
- raise NotImplementedError()
-
- def _node_add_child(self, parentxpath, parentnode, newnode):
- raise NotImplementedError()
-
- def _node_remove_child(self, parentnode, childnode):
- raise NotImplementedError()
-
- def _node_replace_child(self, xpath, newnode):
- raise NotImplementedError()
-
- def _node_from_xml(self, xml):
- raise NotImplementedError()
-
- def _node_has_content(self, node):
- raise NotImplementedError()
-
- def _node_get_name(self, node):
- raise NotImplementedError()
-
- def node_clear(self, xpath):
- raise NotImplementedError()
-
- def _sanitize_xml(self, xml):
- raise NotImplementedError()
-
- def get_xml(self, xpath):
- node = self._find(xpath)
- if node is None:
- return ""
- return self._sanitize_xml(self._node_tostring(node))
-
- def get_xpath_content(self, xpath, is_bool):
- node = self._find(xpath)
- if node is None:
- return None
- if is_bool:
- return True
- xpathobj = _XPath(xpath)
- if xpathobj.is_prop:
- return self._node_get_property(node, xpathobj.propname)
- return self._node_get_text(node)
-
- def set_xpath_content(self, xpath, setval):
- node = self._find(xpath)
- if setval is False:
- # Boolean False, means remove the node entirely
- self.node_force_remove(xpath)
- elif setval is None:
- if node is not None:
- self._node_set_content(xpath, node, None)
- self._node_remove_empty(xpath)
- else:
- if node is None:
- node = self._node_make_stub(xpath)
-
- if setval is True:
- # Boolean property, creating the node is enough
- return
- self._node_set_content(xpath, node, setval)
-
- def node_add_xml(self, xml, xpath):
- newnode = self._node_from_xml(xml)
- parentnode = self._node_make_stub(xpath)
- self._node_add_child(xpath, parentnode, newnode)
-
- def node_replace_xml(self, xpath, xml):
- """
- Replace the node at xpath with the passed in xml
- """
- newnode = self._node_from_xml(xml)
- self._node_replace_child(xpath, newnode)
-
- def node_force_remove(self, fullxpath):
- """
- Remove the element referenced at the passed xpath, regardless
- of whether it has children or not, and then clean up the XML
- chain
- """
- xpathobj = _XPath(fullxpath)
- parentnode = self._find(xpathobj.parent_xpath())
- childnode = self._find(fullxpath)
- if parentnode is None or childnode is None:
- return
- self._node_remove_child(parentnode, childnode)
-
- def validate_root_name(self, expected_root_name):
- rootname = self._node_get_name(self._find("."))
- if rootname == expected_root_name:
- return
- raise RuntimeError(
- _(
- "XML did not have expected root element name "
- "'%(expectname)s', found '%(foundname)s'"
- )
- % {"expectname": expected_root_name, "foundname": rootname}
- )
-
- def _node_set_content(self, xpath, node, setval):
- xpathobj = _XPath(xpath)
- if setval is not None:
- setval = str(setval)
- if xpathobj.is_prop:
- self._node_set_property(node, xpathobj.propname, setval)
- else:
- self._node_set_text(node, setval)
-
- def _node_make_stub(self, fullxpath):
- """
- Build all nodes for the passed xpath. For example, if XML is <foo/>,
- and xpath=./bar/@baz, after this function the XML will be:
-
- <foo>
- <bar baz=''/>
- </foo>
-
- And the node pointing to @baz will be returned, for the caller to
- do with as they please.
-
- There's also special handling to ensure that setting
- xpath=./bar[@baz='foo']/frob will create
-
- <bar baz='foo'>
- <frob></frob>
- </bar>
-
- Even if <bar> didn't exist before. So we fill in the dependent property
- expression values
- """
- xpathobj = _XPath(fullxpath)
- parentxpath = "."
- parentnode = self._find(parentxpath)
- if not parentnode:
- raise xmlutil.DevError("Did not find XML root node for xpath=%s" % fullxpath)
-
- for xpathseg in xpathobj.segments[1:]:
- oldxpath = parentxpath
- parentxpath += "/%s" % xpathseg.fullsegment
- tmpnode = self._find(parentxpath)
- if tmpnode is not None:
- # xpath node already exists, nothing to create yet
- parentnode = tmpnode
- continue
-
- newnode = self._node_new(xpathseg, parentnode)
- self._node_add_child(oldxpath, parentnode, newnode)
- parentnode = newnode
-
- # For a conditional xpath like ./foo[@bar='baz'],
- # we also want to implicitly set <foo bar='baz'/>
- if xpathseg.condition_prop:
- self._node_set_property(parentnode, xpathseg.condition_prop, xpathseg.condition_val)
-
- return parentnode
-
- def _node_remove_empty(self, fullxpath):
- """
- Walk backwards up the xpath chain, and remove each element
- if it doesn't have any children or attributes, so we don't
- leave stale elements in the XML
- """
- xpathobj = _XPath(fullxpath)
- segments = xpathobj.segments[:]
- parent = None
- while segments:
- xpath = _XPath.join(segments)
- segments.pop()
- child = parent
- parent = self._find(xpath)
- if parent is None:
- break
- if child is None:
- continue
- if self._node_has_content(child):
- break
-
- self._node_remove_child(parent, child)
-
-
-def node_is_text(n):
- return bool(n and n.type == "text")
-
-
-class _Libxml2API(_XMLBase):
- def __init__(self, xml):
- _XMLBase.__init__(self)
-
- # Use of gtksourceview in virt-manager changes this libxml
- # global setting which messes up whitespace after parsing.
- # We can probably get away with calling this less but it
- # would take some investigation
- libxml2.keepBlanksDefault(1)
-
- self._doc = libxml2.parseDoc(xml)
- self._ctx = self._doc.xpathNewContext()
- self._ctx.setContextNode(self._doc.children)
- for key, val in self.NAMESPACES.items():
- self._ctx.xpathRegisterNs(key, val)
-
- def __del__(self):
- if not hasattr(self, "_doc"):
- # In case we error when parsing the doc
- return
- self._doc.freeDoc()
- self._doc = None
- self._ctx.xpathFreeContext()
- self._ctx = None
-
- def _sanitize_xml(self, xml):
- if not xml.endswith("\n") and "\n" in xml:
- xml += "\n"
- return xml
-
- def copy_api(self):
- return _Libxml2API(self._doc.children.serialize())
-
- def _find(self, fullxpath):
- xpath = _XPath(fullxpath).xpath
- try:
- node = self._ctx.xpathEval(xpath)
- except Exception as e:
- log.debug("fullxpath=%s xpath=%s eval failed", fullxpath, xpath, exc_info=True)
- raise RuntimeError("%s %s" % (fullxpath, str(e))) from None
- return node and node[0] or None
-
- def count(self, xpath):
- return len(self._ctx.xpathEval(xpath))
-
- def _node_tostring(self, node):
- return node.serialize()
-
- def _node_from_xml(self, xml):
- return libxml2.parseDoc(xml).children
-
- def _node_get_text(self, node):
- return node.content
-
- def _node_set_text(self, node, setval):
- if setval is not None:
- setval = xmlutil.xml_escape(setval)
- node.setContent(setval)
-
- def _node_get_property(self, node, propname):
- prop = node.hasProp(propname)
- if prop:
- return prop.content
-
- def _node_set_property(self, node, propname, setval):
- if setval is None:
- prop = node.hasProp(propname)
- if prop:
- prop.unlinkNode()
- prop.freeNode()
- else:
- node.setProp(propname, setval)
-
- def _node_new(self, xpathseg, parentnode):
- newnode = libxml2.newNode(xpathseg.nodename)
- if not xpathseg.nsname:
- return newnode
-
- def _find_parent_ns():
- parent = parentnode
- while parent:
- for ns in xmlutil.listify(parent.nsDefs()):
- if ns.name == xpathseg.nsname:
- return ns
- parent = parent.get_parent()
-
- ns = _find_parent_ns()
- if not ns:
- ns = newnode.newNs(self.NAMESPACES[xpathseg.nsname], xpathseg.nsname)
- newnode.setNs(ns)
- return newnode
-
- def node_clear(self, xpath):
- node = self._find(xpath)
- if node:
- propnames = [p.name for p in (node.properties or [])]
- for p in propnames:
- node.unsetProp(p)
- node.setContent(None)
-
- def _node_has_content(self, node):
- return node.type == "element" and (node.children or node.properties)
-
- def _node_get_name(self, node):
- return node.name
-
- def _node_remove_child(self, parentnode, childnode):
- node = childnode
-
- # Look for preceding whitespace and remove it
- white = node.get_prev()
- if node_is_text(white):
- white.unlinkNode()
- white.freeNode()
-
- node.unlinkNode()
- node.freeNode()
- if all([node_is_text(n) for n in parentnode.children]):
- parentnode.setContent(None)
-
- def _node_add_child(self, parentxpath, parentnode, newnode):
- ignore = parentxpath
- if not node_is_text(parentnode.get_last()):
- prevsib = parentnode.get_prev()
- if node_is_text(prevsib):
- newlast = libxml2.newText(prevsib.content)
- else:
- newlast = libxml2.newText("\n")
- parentnode.addChild(newlast)
-
- endtext = parentnode.get_last().content
- parentnode.addChild(libxml2.newText(" "))
- parentnode.addChild(newnode)
- parentnode.addChild(libxml2.newText(endtext))
-
- def _node_replace_child(self, xpath, newnode):
- oldnode = self._find(xpath)
- oldnode.replaceNode(newnode)
-
-
-XMLAPI = _Libxml2API
+XMLAPI = Libxml2API
diff --git a/virtinst/xmlbase.py b/virtinst/xmlbase.py
new file mode 100644
index 000000000..098e75f5a
--- /dev/null
+++ b/virtinst/xmlbase.py
@@ -0,0 +1,290 @@
+#
+# XML API common infrastructure
+#
+# This work is licensed under the GNU GPLv2 or later.
+# See the COPYING file in the top-level directory.
+
+from . import xmlutil
+
+
+class _XPathSegment:
+ """
+ Class representing a single 'segment' of an xpath string. For example,
+ the xpath:
+
+ ./qemu:foo/bar[1]/baz[@somepro='someval']/@finalprop
+
+ will be split into the following segments:
+
+ #1: nodename=., fullsegment=.
+ #2: nodename=foo, nsname=qemu, fullsegment=qemu:foo
+ #3: nodename=bar, condition_num=1, fullsegment=bar[1]
+ #4: nodename=baz, condition_prop=somepro, condition_val=someval,
+ fullsegment=baz[@somepro='somval']
+ #5: nodename=finalprop, is_prop=True, fullsegment=@finalprop
+ """
+
+ def __init__(self, fullsegment):
+ self.fullsegment = fullsegment
+ self.nodename = fullsegment
+
+ self.condition_prop = None
+ self.condition_val = None
+ self.condition_num = None
+ if "[" in self.nodename:
+ self.nodename, cond = self.nodename.strip("]").split("[")
+ if "=" in cond:
+ (cprop, cval) = cond.split("=")
+ self.condition_prop = cprop.strip("@")
+ self.condition_val = cval.strip("'")
+ elif cond.isdigit():
+ self.condition_num = int(cond)
+
+ self.is_prop = self.nodename.startswith("@")
+ if self.is_prop:
+ self.nodename = self.nodename[1:]
+
+ self.nsname = None
+ if ":" in self.nodename:
+ self.nsname, self.nodename = self.nodename.split(":")
+
+
+class XPath:
+ """
+ Helper class for performing manipulations of XPath strings. Splits
+ the xpath into segments.
+ """
+
+ def __init__(self, fullxpath):
+ self.fullxpath = fullxpath
+ self.segments = []
+ for s in self.fullxpath.split("/"):
+ if s == "..":
+ # Resolve and flatten .. in xpaths
+ self.segments = self.segments[:-1]
+ continue
+ self.segments.append(_XPathSegment(s))
+
+ self.is_prop = self.segments[-1].is_prop
+ self.propname = self.is_prop and self.segments[-1].nodename or None
+ if self.is_prop:
+ self.segments = self.segments[:-1]
+ self.xpath = self.join(self.segments)
+
+ @staticmethod
+ def join(segments):
+ return "/".join(s.fullsegment for s in segments)
+
+ def parent_xpath(self):
+ return self.join(self.segments[:-1])
+
+
+class XMLBase:
+ NAMESPACES = {}
+
+ @classmethod
+ def register_namespace(cls, nsname, uri):
+ cls.NAMESPACES[nsname] = uri
+
+ def copy_api(self):
+ raise NotImplementedError()
+
+ def count(self, xpath):
+ raise NotImplementedError()
+
+ def _find(self, fullxpath):
+ raise NotImplementedError()
+
+ def _node_tostring(self, node):
+ raise NotImplementedError()
+
+ def _node_get_text(self, node):
+ raise NotImplementedError()
+
+ def _node_set_text(self, node, setval):
+ raise NotImplementedError()
+
+ def _node_get_property(self, node, propname):
+ raise NotImplementedError()
+
+ def _node_set_property(self, node, propname, setval):
+ raise NotImplementedError()
+
+ def _node_new(self, xpathseg, parentnode):
+ raise NotImplementedError()
+
+ def _node_add_child(self, parentxpath, parentnode, newnode):
+ raise NotImplementedError()
+
+ def _node_remove_child(self, parentnode, childnode):
+ raise NotImplementedError()
+
+ def _node_replace_child(self, xpath, newnode):
+ raise NotImplementedError()
+
+ def _node_from_xml(self, xml):
+ raise NotImplementedError()
+
+ def _node_has_content(self, node):
+ raise NotImplementedError()
+
+ def _node_get_name(self, node):
+ raise NotImplementedError()
+
+ def node_clear(self, xpath):
+ raise NotImplementedError()
+
+ def _sanitize_xml(self, xml):
+ raise NotImplementedError()
+
+ def get_xml(self, xpath):
+ node = self._find(xpath)
+ if node is None:
+ return ""
+ return self._sanitize_xml(self._node_tostring(node))
+
+ def get_xpath_content(self, xpath, is_bool):
+ node = self._find(xpath)
+ if node is None:
+ return None
+ if is_bool:
+ return True
+ xpathobj = XPath(xpath)
+ if xpathobj.is_prop:
+ return self._node_get_property(node, xpathobj.propname)
+ return self._node_get_text(node)
+
+ def set_xpath_content(self, xpath, setval):
+ node = self._find(xpath)
+ if setval is False:
+ # Boolean False, means remove the node entirely
+ self.node_force_remove(xpath)
+ elif setval is None:
+ if node is not None:
+ self._node_set_content(xpath, node, None)
+ self._node_remove_empty(xpath)
+ else:
+ if node is None:
+ node = self._node_make_stub(xpath)
+
+ if setval is True:
+ # Boolean property, creating the node is enough
+ return
+ self._node_set_content(xpath, node, setval)
+
+ def node_add_xml(self, xml, xpath):
+ newnode = self._node_from_xml(xml)
+ parentnode = self._node_make_stub(xpath)
+ self._node_add_child(xpath, parentnode, newnode)
+
+ def node_replace_xml(self, xpath, xml):
+ """
+ Replace the node at xpath with the passed in xml
+ """
+ newnode = self._node_from_xml(xml)
+ self._node_replace_child(xpath, newnode)
+
+ def node_force_remove(self, fullxpath):
+ """
+ Remove the element referenced at the passed xpath, regardless
+ of whether it has children or not, and then clean up the XML
+ chain
+ """
+ xpathobj = XPath(fullxpath)
+ parentnode = self._find(xpathobj.parent_xpath())
+ childnode = self._find(fullxpath)
+ if parentnode is None or childnode is None:
+ return
+ self._node_remove_child(parentnode, childnode)
+
+ def validate_root_name(self, expected_root_name):
+ rootname = self._node_get_name(self._find("."))
+ if rootname == expected_root_name:
+ return
+ raise RuntimeError(
+ _(
+ "XML did not have expected root element name "
+ "'%(expectname)s', found '%(foundname)s'"
+ )
+ % {"expectname": expected_root_name, "foundname": rootname}
+ )
+
+ def _node_set_content(self, xpath, node, setval):
+ xpathobj = XPath(xpath)
+ if setval is not None:
+ setval = str(setval)
+ if xpathobj.is_prop:
+ self._node_set_property(node, xpathobj.propname, setval)
+ else:
+ self._node_set_text(node, setval)
+
+ def _node_make_stub(self, fullxpath):
+ """
+ Build all nodes for the passed xpath. For example, if XML is <foo/>,
+ and xpath=./bar/@baz, after this function the XML will be:
+
+ <foo>
+ <bar baz=''/>
+ </foo>
+
+ And the node pointing to @baz will be returned, for the caller to
+ do with as they please.
+
+ There's also special handling to ensure that setting
+ xpath=./bar[@baz='foo']/frob will create
+
+ <bar baz='foo'>
+ <frob></frob>
+ </bar>
+
+ Even if <bar> didn't exist before. So we fill in the dependent property
+ expression values
+ """
+ xpathobj = XPath(fullxpath)
+ parentxpath = "."
+ parentnode = self._find(parentxpath)
+ if not parentnode:
+ raise xmlutil.DevError("Did not find XML root node for xpath=%s" % fullxpath)
+
+ for xpathseg in xpathobj.segments[1:]:
+ oldxpath = parentxpath
+ parentxpath += "/%s" % xpathseg.fullsegment
+ tmpnode = self._find(parentxpath)
+ if tmpnode is not None:
+ # xpath node already exists, nothing to create yet
+ parentnode = tmpnode
+ continue
+
+ newnode = self._node_new(xpathseg, parentnode)
+ self._node_add_child(oldxpath, parentnode, newnode)
+ parentnode = newnode
+
+ # For a conditional xpath like ./foo[@bar='baz'],
+ # we also want to implicitly set <foo bar='baz'/>
+ if xpathseg.condition_prop:
+ self._node_set_property(parentnode, xpathseg.condition_prop, xpathseg.condition_val)
+
+ return parentnode
+
+ def _node_remove_empty(self, fullxpath):
+ """
+ Walk backwards up the xpath chain, and remove each element
+ if it doesn't have any children or attributes, so we don't
+ leave stale elements in the XML
+ """
+ xpathobj = XPath(fullxpath)
+ segments = xpathobj.segments[:]
+ parent = None
+ while segments:
+ xpath = XPath.join(segments)
+ segments.pop()
+ child = parent
+ parent = self._find(xpath)
+ if parent is None:
+ break
+ if child is None:
+ continue
+ if self._node_has_content(child):
+ break
+
+ self._node_remove_child(parent, child)
diff --git a/virtinst/xmllibxml2.py b/virtinst/xmllibxml2.py
new file mode 100644
index 000000000..e704276e9
--- /dev/null
+++ b/virtinst/xmllibxml2.py
@@ -0,0 +1,157 @@
+#
+# XML API using libxml2
+#
+# This work is licensed under the GNU GPLv2 or later.
+# See the COPYING file in the top-level directory.
+
+import libxml2
+
+from . import xmlutil
+from .logger import log
+from .xmlbase import XMLBase, XPath
+
+# pylint: disable=protected-access
+
+
+def node_is_text(n):
+ return bool(n and n.type == "text")
+
+
+class Libxml2API(XMLBase):
+ def __init__(self, xml):
+ XMLBase.__init__(self)
+
+ # Use of gtksourceview in virt-manager changes this libxml
+ # global setting which messes up whitespace after parsing.
+ # We can probably get away with calling this less but it
+ # would take some investigation
+ libxml2.keepBlanksDefault(1)
+
+ self._doc = libxml2.parseDoc(xml)
+ self._ctx = self._doc.xpathNewContext()
+ self._ctx.setContextNode(self._doc.children)
+ for key, val in self.NAMESPACES.items():
+ self._ctx.xpathRegisterNs(key, val)
+
+ def __del__(self):
+ if not hasattr(self, "_doc"):
+ # In case we error when parsing the doc
+ return
+ self._doc.freeDoc()
+ self._doc = None
+ self._ctx.xpathFreeContext()
+ self._ctx = None
+
+ def _sanitize_xml(self, xml):
+ if not xml.endswith("\n") and "\n" in xml:
+ xml += "\n"
+ return xml
+
+ def copy_api(self):
+ return Libxml2API(self._doc.children.serialize())
+
+ def _find(self, fullxpath):
+ xpath = XPath(fullxpath).xpath
+ try:
+ node = self._ctx.xpathEval(xpath)
+ except Exception as e:
+ log.debug("fullxpath=%s xpath=%s eval failed", fullxpath, xpath, exc_info=True)
+ raise RuntimeError("%s %s" % (fullxpath, str(e))) from None
+ return node and node[0] or None
+
+ def count(self, xpath):
+ return len(self._ctx.xpathEval(xpath))
+
+ def _node_tostring(self, node):
+ return node.serialize()
+
+ def _node_from_xml(self, xml):
+ return libxml2.parseDoc(xml).children
+
+ def _node_get_text(self, node):
+ return node.content
+
+ def _node_set_text(self, node, setval):
+ if setval is not None:
+ setval = xmlutil.xml_escape(setval)
+ node.setContent(setval)
+
+ def _node_get_property(self, node, propname):
+ prop = node.hasProp(propname)
+ if prop:
+ return prop.content
+
+ def _node_set_property(self, node, propname, setval):
+ if setval is None:
+ prop = node.hasProp(propname)
+ if prop:
+ prop.unlinkNode()
+ prop.freeNode()
+ else:
+ node.setProp(propname, setval)
+
+ def _node_new(self, xpathseg, parentnode):
+ newnode = libxml2.newNode(xpathseg.nodename)
+ if not xpathseg.nsname:
+ return newnode
+
+ def _find_parent_ns():
+ parent = parentnode
+ while parent:
+ for ns in xmlutil.listify(parent.nsDefs()):
+ if ns.name == xpathseg.nsname:
+ return ns
+ parent = parent.get_parent()
+
+ ns = _find_parent_ns()
+ if not ns:
+ ns = newnode.newNs(self.NAMESPACES[xpathseg.nsname], xpathseg.nsname)
+ newnode.setNs(ns)
+ return newnode
+
+ def node_clear(self, xpath):
+ node = self._find(xpath)
+ if node:
+ propnames = [p.name for p in (node.properties or [])]
+ for p in propnames:
+ node.unsetProp(p)
+ node.setContent(None)
+
+ def _node_has_content(self, node):
+ return node.type == "element" and (node.children or node.properties)
+
+ def _node_get_name(self, node):
+ return node.name
+
+ def _node_remove_child(self, parentnode, childnode):
+ node = childnode
+
+ # Look for preceding whitespace and remove it
+ white = node.get_prev()
+ if node_is_text(white):
+ white.unlinkNode()
+ white.freeNode()
+
+ node.unlinkNode()
+ node.freeNode()
+ if all([node_is_text(n) for n in parentnode.children]):
+ parentnode.setContent(None)
+
+ def _node_add_child(self, parentxpath, parentnode, newnode):
+ ignore = parentxpath
+ if not node_is_text(parentnode.get_last()):
+ prevsib = parentnode.get_prev()
+ if node_is_text(prevsib):
+ newlast = libxml2.newText(prevsib.content)
+ else:
+ newlast = libxml2.newText("\n")
+ parentnode.addChild(newlast)
+
+ endtext = parentnode.get_last().content
+ parentnode.addChild(libxml2.newText(" "))
+ parentnode.addChild(newnode)
+ parentnode.addChild(libxml2.newText(endtext))
+
+ def _node_replace_child(self, xpath, newnode):
+ oldnode = self._find(xpath)
+ oldnode.replaceNode(newnode)