Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package virt-manager for openSUSE:Factory 
checked in at 2026-03-05 17:13:20
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/virt-manager (Old)
 and      /work/SRC/openSUSE:Factory/.virt-manager.new.561 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "virt-manager"

Thu Mar  5 17:13:20 2026 rev:287 rq:1335915 version:5.1.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/virt-manager/virt-manager.changes        
2026-02-19 14:19:34.589284928 +0100
+++ /work/SRC/openSUSE:Factory/.virt-manager.new.561/virt-manager.changes       
2026-03-05 17:14:35.225250427 +0100
@@ -1,0 +2,10 @@
+Mon Mar  2 11:11:53 MST 2026 - [email protected]
+
+- bsc#1259049 - virt-manager: virt-install deprecate libxml2 python
+  bindings
+  004-xmlapi-split-out-xmlbase.py-and-xmllibxml2.py.patch
+  005-xmlbase-fix-parentnode-None-check.patch
+  006-xmllibxml2-lazily-import-libxml2.patch
+  007-xmlapi-add-xmletree.py-backend.patch
+
+-------------------------------------------------------------------

New:
----
  004-xmlapi-split-out-xmlbase.py-and-xmllibxml2.py.patch
  005-xmlbase-fix-parentnode-None-check.patch
  006-xmllibxml2-lazily-import-libxml2.patch
  007-xmlapi-add-xmletree.py-backend.patch

----------(New B)----------
  New:  bindings
  004-xmlapi-split-out-xmlbase.py-and-xmllibxml2.py.patch
  005-xmlbase-fix-parentnode-None-check.patch
  New:  004-xmlapi-split-out-xmlbase.py-and-xmllibxml2.py.patch
  005-xmlbase-fix-parentnode-None-check.patch
  006-xmllibxml2-lazily-import-libxml2.patch
  New:  005-xmlbase-fix-parentnode-None-check.patch
  006-xmllibxml2-lazily-import-libxml2.patch
  007-xmlapi-add-xmletree.py-backend.patch
  New:  006-xmllibxml2-lazily-import-libxml2.patch
  007-xmlapi-add-xmletree.py-backend.patch
----------(New E)----------

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ virt-manager.spec ++++++
--- /var/tmp/diff_new_pack.fhijNt/_old  2026-03-05 17:14:36.989323751 +0100
+++ /var/tmp/diff_new_pack.fhijNt/_new  2026-03-05 17:14:36.989323751 +0100
@@ -49,26 +49,30 @@
 Source2:        virt-install.desktop
 Source3:        virt-manager-supportconfig
 # Upstream Patches
-Patch1:         003-virtinst-cloudinit-include-empty-meta-data-file.patch
-Patch2:         009-avoid-NoneType-pixbuf.patch
-Patch3:         
012-virtManager-wrapped-details-hw-panel-with-GtkScrolledWindow.patch
-Patch4:         
013-virtinst-interface-add-support-for-backend.hostname-and-backend.fqdn.patch
-Patch5:         014-virtinst-add-support-for-acpi-generic-initiator.patch
-Patch6:         015-virtinst-add-support-for-pcihole64.patch
-Patch7:         
017-maint-use-constants-instead-of-strings-for-boot-devices.patch
-Patch8:         018-virtinst-rework-get_boot_order.patch
-Patch9:         019-virtinst-guest-introduce-can_use_device_boot_order.patch
-Patch10:        
020-virtinst-remove-legacy-attribute-from-set_boot_order-get_boot_order.patch
-Patch11:        021-installer-add-support-to-use-device-boot-order.patch
-Patch12:        024-virtinst-Fix-XDG_DATA_HOME-handling.patch
-Patch13:        051-addhardware-Add-usb-as-a-recommended-sound-device.patch
-Patch14:        055-virtinst-Add-serial-controller-option-to-cli.patch
-Patch15:        056-virtinst-Add-NVMe-Controller.patch
-Patch16:        057-virtinst-implement-NVMe-disk-target-generation.patch
-Patch17:        058-virtManager-Add-NVMe-disk-type.patch
-Patch18:        059-ui-Show-NVMe-Controller-details.patch
-Patch19:        060-virtinst-fix-locale-when-running-in-flatpak.patch
-Patch20:        061-virtinst-add-support-for-iommufd.patch
+Patch3:         003-virtinst-cloudinit-include-empty-meta-data-file.patch
+Patch4:         004-xmlapi-split-out-xmlbase.py-and-xmllibxml2.py.patch
+Patch5:         005-xmlbase-fix-parentnode-None-check.patch
+Patch6:         006-xmllibxml2-lazily-import-libxml2.patch
+Patch7:         007-xmlapi-add-xmletree.py-backend.patch
+Patch9:         009-avoid-NoneType-pixbuf.patch
+Patch12:        
012-virtManager-wrapped-details-hw-panel-with-GtkScrolledWindow.patch
+Patch13:        
013-virtinst-interface-add-support-for-backend.hostname-and-backend.fqdn.patch
+Patch14:        014-virtinst-add-support-for-acpi-generic-initiator.patch
+Patch15:        015-virtinst-add-support-for-pcihole64.patch
+Patch17:        
017-maint-use-constants-instead-of-strings-for-boot-devices.patch
+Patch18:        018-virtinst-rework-get_boot_order.patch
+Patch19:        019-virtinst-guest-introduce-can_use_device_boot_order.patch
+Patch20:        
020-virtinst-remove-legacy-attribute-from-set_boot_order-get_boot_order.patch
+Patch21:        021-installer-add-support-to-use-device-boot-order.patch
+Patch24:        024-virtinst-Fix-XDG_DATA_HOME-handling.patch
+Patch51:        051-addhardware-Add-usb-as-a-recommended-sound-device.patch
+Patch55:        055-virtinst-Add-serial-controller-option-to-cli.patch
+Patch56:        056-virtinst-Add-NVMe-Controller.patch
+Patch57:        057-virtinst-implement-NVMe-disk-target-generation.patch
+Patch58:        058-virtManager-Add-NVMe-disk-type.patch
+Patch59:        059-ui-Show-NVMe-Controller-details.patch
+Patch60:        060-virtinst-fix-locale-when-running-in-flatpak.patch
+Patch61:        061-virtinst-add-support-for-iommufd.patch
 # SUSE Only
 Patch150:       virtman-desktop.patch
 Patch151:       virtman-kvm.patch
@@ -172,7 +176,6 @@
 Requires:       python3-argcomplete
 Requires:       python3-gobject
 Requires:       python3-libvirt-python >= 0.7.0
-Requires:       python3-libxml2-python
 Requires:       python3-requests
 Requires:       xorriso
 

++++++ 004-xmlapi-split-out-xmlbase.py-and-xmllibxml2.py.patch ++++++
++++ 930 lines (skipped)

++++++ 005-xmlbase-fix-parentnode-None-check.patch ++++++
Subject: xmlbase: fix parentnode None check
From: Cole Robinson [email protected] Tue Sep 23 09:01:47 2025 -0400
Date: Wed Oct 1 11:22:35 2025 -0400:
Git: ff9fa95e52f890ccd8dce18567aa7cc30582ca4f

Future XMLAPI implementation need this.

Signed-off-by: Cole Robinson <[email protected]>

diff --git a/virtinst/xmlbase.py b/virtinst/xmlbase.py
index 098e75f5a..8cff450bd 100644
--- a/virtinst/xmlbase.py
+++ b/virtinst/xmlbase.py
@@ -243,7 +243,7 @@ class XMLBase:
         xpathobj = XPath(fullxpath)
         parentxpath = "."
         parentnode = self._find(parentxpath)
-        if not parentnode:
+        if parentnode is None:
             raise xmlutil.DevError("Did not find XML root node for xpath=%s" % 
fullxpath)
 
         for xpathseg in xpathobj.segments[1:]:

++++++ 006-xmllibxml2-lazily-import-libxml2.patch ++++++
Subject: xmllibxml2: lazily import libxml2
From: Cole Robinson [email protected] Tue Sep 23 10:46:19 2025 -0400
Date: Wed Oct 1 11:22:35 2025 -0400:
Git: d0372e82c8b6fe6b5517d850a81847422c861446

If we switch XML backends in the future, this will save us from
having a hard dep on libxml2

Signed-off-by: Cole Robinson <[email protected]>

diff --git a/virtinst/xmllibxml2.py b/virtinst/xmllibxml2.py
index e704276e9..947ae1c0a 100644
--- a/virtinst/xmllibxml2.py
+++ b/virtinst/xmllibxml2.py
@@ -4,8 +4,6 @@
 # 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
@@ -21,13 +19,17 @@ class Libxml2API(XMLBase):
     def __init__(self, xml):
         XMLBase.__init__(self)
 
+        import libxml2
+
+        self._libxml2 = libxml2
+
         # 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._libxml2.keepBlanksDefault(1)
 
-        self._doc = libxml2.parseDoc(xml)
+        self._doc = self._libxml2.parseDoc(xml)
         self._ctx = self._doc.xpathNewContext()
         self._ctx.setContextNode(self._doc.children)
         for key, val in self.NAMESPACES.items():
@@ -66,7 +68,7 @@ class Libxml2API(XMLBase):
         return node.serialize()
 
     def _node_from_xml(self, xml):
-        return libxml2.parseDoc(xml).children
+        return self._libxml2.parseDoc(xml).children
 
     def _node_get_text(self, node):
         return node.content
@@ -91,7 +93,7 @@ class Libxml2API(XMLBase):
             node.setProp(propname, setval)
 
     def _node_new(self, xpathseg, parentnode):
-        newnode = libxml2.newNode(xpathseg.nodename)
+        newnode = self._libxml2.newNode(xpathseg.nodename)
         if not xpathseg.nsname:
             return newnode
 
@@ -142,15 +144,15 @@ class Libxml2API(XMLBase):
         if not node_is_text(parentnode.get_last()):
             prevsib = parentnode.get_prev()
             if node_is_text(prevsib):
-                newlast = libxml2.newText(prevsib.content)
+                newlast = self._libxml2.newText(prevsib.content)
             else:
-                newlast = libxml2.newText("\n")
+                newlast = self._libxml2.newText("\n")
             parentnode.addChild(newlast)
 
         endtext = parentnode.get_last().content
-        parentnode.addChild(libxml2.newText("  "))
+        parentnode.addChild(self._libxml2.newText("  "))
         parentnode.addChild(newnode)
-        parentnode.addChild(libxml2.newText(endtext))
+        parentnode.addChild(self._libxml2.newText(endtext))
 
     def _node_replace_child(self, xpath, newnode):
         oldnode = self._find(xpath)

++++++ 007-xmlapi-add-xmletree.py-backend.patch ++++++
Subject: xmlapi: add xmletree.py backend
From: Cole Robinson [email protected] Wed Sep 17 10:42:15 2025 -0400
Date: Wed Oct 1 11:22:35 2025 -0400:
Git: 766bf2ecdc5ac6853b41a36412d09c1950c700bf

This is an XMLAPI backend using stock python ElementTree.
We need to extend and re-implement some of ElementTree internals
to make its output match what libvirt generates, so virt-xml edits
don't generate extraneous diffs.

This is disabled by default but will be used if libxml2 is not
installed. You can explicitly opt in to using it by setting
env var VIRTINST_XML_BACKEND=etree before virtinst is imported

Signed-off-by: Cole Robinson <[email protected]>

diff --git a/.coveragerc b/.coveragerc
index c404df727..12e01eb0f 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -3,7 +3,7 @@ source=virtinst/
 
 [report]
 skip_covered = yes
-#omit=virtinst/_progresspriv.py
+omit=virtinst/xmletree.py
 
 exclude_lines =
     # Have to re-enable the standard pragma
diff --git a/tests/test_cli.py b/tests/test_cli.py
index daf6e2a80..b371e604b 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -1148,7 +1148,9 @@ c.add_invalid(
 )  # URI doesn't support UEFI bits
 c.add_invalid("--graphics type=vnc,keymap", grep="Option 'keymap' had no value 
set.")
 c.add_invalid("--xml FOOXPATH", grep="form of XPATH=VALUE")  # failure parsing 
xpath value
-c.add_invalid("--xml /@foo=bar", grep="/@foo xmlXPathEval")  # failure 
processing xpath
+c.add_invalid(
+    "--xml /@foo=bar", grep="(/@foo xmlXPathEval|not an iterator)"
+)  # failure processing xpath
 
 
 ########################
diff --git a/tests/test_xmlparse.py b/tests/test_xmlparse.py
index 7f150f918..052850437 100644
--- a/tests/test_xmlparse.py
+++ b/tests/test_xmlparse.py
@@ -1008,7 +1008,7 @@ def testXMLBuilderCoverage():
         # Ensure we validate root element
         virtinst.DeviceDisk(conn, parsexml="<foo/>")
 
-    with pytest.raises(Exception, match=".*xmlParseDoc.*"):
+    with pytest.raises(Exception, match=".*(xmlParseDoc|not 'int').*"):
         # Ensure we validate root element
         virtinst.DeviceDisk(conn, parsexml=-1)
 
diff --git a/virtinst/meson.build b/virtinst/meson.build
index d8be0e895..f0ba05439 100644
--- a/virtinst/meson.build
+++ b/virtinst/meson.build
@@ -26,6 +26,7 @@ virtinst_sources = files(
   'xmlapi.py',
   'xmlbase.py',
   'xmlbuilder.py',
+  'xmletree.py',
   'xmllibxml2.py',
   'xmlutil.py',
 )
diff --git a/virtinst/xmlapi.py b/virtinst/xmlapi.py
index c20718c08..38bca65fa 100644
--- a/virtinst/xmlapi.py
+++ b/virtinst/xmlapi.py
@@ -4,6 +4,31 @@
 # This work is licensed under the GNU GPLv2 or later.
 # See the COPYING file in the top-level directory.
 
+import os
+
+from .logger import log
+from .xmletree import ETreeAPI
 from .xmllibxml2 import Libxml2API
 
-XMLAPI = Libxml2API
+_backend = os.environ.get("VIRTINST_XML_BACKEND")
+log.debug("VIRTINST_XML_BACKEND=%s", _backend)
+
+
+def _get_default():  # pragma: no cover
+    if _backend == "libxml2":
+        return Libxml2API
+    elif _backend == "etree":
+        return ETreeAPI
+
+    try:
+        import libxml2
+
+        _ignore = libxml2
+        return Libxml2API
+    except ImportError as e:
+        log.debug("libxml2 import error: %s", e)
+        return ETreeAPI
+
+
+XMLAPI = _get_default()
+log.debug("Using XMLAPI=%s", XMLAPI)
diff --git a/virtinst/xmletree.py b/virtinst/xmletree.py
new file mode 100644
index 000000000..f1bdcf6b1
--- /dev/null
+++ b/virtinst/xmletree.py
@@ -0,0 +1,294 @@
+#
+# XML API using stock python ElementTree
+#
+# This work is licensed under the GNU GPLv2 or later.
+# See the COPYING file in the top-level directory.
+
+import io
+import re
+import xml.etree.ElementTree as ET
+
+from . import xmlutil
+from .xmlbase import XMLBase, XPath
+
+# We need to extend ElementTree to parse + rebuild XML with no diff
+# from default libvirt output. Otherwise `virt-xml --edit` diffs
+# are needlessly noisy.
+#
+# The main problematic area is xmlns namespace handling.
+#
+# 1) libvirt xml will preserve arbitrary xml definitions.
+#    ElementTree will _rename_ xmlns definition to ns0, ns1, etc
+#    unless `register_namespace` was called ahead of time.
+#
+# 2) ElementTree formats every xmlns attribute into the top
+#    element of the document, but libvirt may keep them inline,
+#    like for <domain> <metadata>.
+
+
+class _VirtinstElement(ET.Element):
+    """
+    Wrap Element to track specifically where an xmlns
+    was defined. Default ElementTree throws this away
+    """
+
+    def __init__(self, *args, **kwargs):
+        self.virtinst_namespaces = {}
+        ET.Element.__init__(self, *args, **kwargs)
+
+    def virtinst_add_namespace(self, prefix, uri):
+        self.virtinst_namespaces[prefix] = uri
+
+
+def _fromstring(xml):
+    namespaces = {}
+
+    class _VirtinstTreeBuilder(ET.TreeBuilder):
+        """
+        Custom tree builder to do two things:
+
+        1) track element where xmlns attribute was defined
+        2) build a mapping of xmlns prefix:uri for every xmlns we see
+        """
+
+        _ns_stack = []
+        _last_element = None
+
+        def end(self, tag):
+            self._last_element = ET.TreeBuilder.end(self, tag)
+            return self._last_element
+
+        def start_ns(self, prefix, uri):
+            self._ns_stack.append((prefix, uri))
+            return (prefix, uri)
+
+        def end_ns(self, _prefix):
+            prefix, uri = self._ns_stack.pop()
+            self._last_element.virtinst_add_namespace(prefix, uri)
+            namespaces[prefix] = uri
+            return prefix
+
+    builder = _VirtinstTreeBuilder(element_factory=_VirtinstElement, 
insert_comments=True)
+    parser = ET.XMLParser(target=builder)
+    parser.feed(xml)
+    node = parser.close()
+    return node, namespaces
+
+
+def _escape_cdata(xml):
+    if xml:
+        xml = xml.replace("&", "&amp;")
+        xml = xml.replace("<", "&lt;")
+        xml = xml.replace(">", "&gt;")
+    return xml
+
+
+def _convert_qname(tag, namespaces):
+    """
+    Convert ElementTree style namespace names to final
+    XML format. For example, given this XML:
+
+    <MYNS:FOO xmlns:MYNS="http://example.com"/>
+
+    ElementTree node.tag will be "{http://example.com}FOO";,
+    and we turn it back into "MYNS:FOO"
+    """
+    if tag and tag.startswith("{"):
+        uri, tag = tag[1:].rsplit("}", 1)
+        for key, val in namespaces.items():
+            if uri == val:
+                tag = key + ":" + tag
+                break
+    return tag
+
+
+def _serialize_node(write, elem, namespaces):
+    # derived from ElementTree._serialize_xml
+    tag = elem.tag
+    text = elem.text
+    if tag is ET.Comment:
+        write("<!--%s-->" % text)
+    else:
+        use_ns = elem.virtinst_namespaces.copy()
+        use_ns.update(namespaces)
+
+        tag = _convert_qname(tag, use_ns)
+
+        if tag is None:  # pragma: no cover
+            # This is for CDATA, which libvirt will throw out anyways.
+            pass
+        else:
+            write("<" + tag)
+            for nsprefix, nsuri in elem.virtinst_namespaces.items():
+                write(' xmlns:%s="%s"' % (nsprefix, nsuri))
+            for k, v in list(elem.items()):
+                k = _convert_qname(k, use_ns)
+                v = xmlutil.xml_escape(v)
+                write(' %s="%s"' % (k, v))
+
+            if text or len(elem):
+                write(">")
+                if text:
+                    write(_escape_cdata(text))
+                for e in elem:
+                    _serialize_node(write, e, namespaces)
+                write("</" + tag + ">")
+            else:
+                write("/>")
+
+    if elem.tail:
+        write(_escape_cdata(elem.tail))
+
+
+def _tostring(node, namespaces):
+    stream = io.StringIO()
+
+    _serialize_node(stream.write, node, namespaces)
+    ret = stream.getvalue()
+    return ret.rstrip()
+
+
+class ETreeAPI(XMLBase):
+    def __init__(self, parsexml):
+        XMLBase.__init__(self)
+        node, namespaces = _fromstring(parsexml)
+        self._et = ET.ElementTree(node)
+        self._namespaces = namespaces
+
+    #######################
+    # Private helper APIs #
+    #######################
+
+    def _sanitize_xml(self, xml):
+        return xml
+
+    def _node_tostring(self, node):
+        return _tostring(node, self._namespaces)
+
+    def _node_from_xml(self, xml):
+        return _fromstring(xml)[0]
+
+    def _node_get_name(self, node):
+        name = _convert_qname(node.tag, self._namespaces)
+        if ":" in name:
+            name = name.split(":", 1)[1]
+        return name
+
+    def _node_get_text(self, node):
+        return node.text
+
+    def _node_set_text(self, node, setval):
+        node.text = setval
+
+    def _node_get_property(self, node, propname):
+        return node.attrib.get(propname)
+
+    def _node_set_property(self, node, propname, setval):
+        if setval is None:
+            node.attrib.pop(propname, None)
+        else:
+            node.attrib[propname] = setval
+
+    def _find(self, fullxpath):
+        xpath = XPath(fullxpath).xpath
+
+        root = "/" + self._node_get_name(self._et.getroot())
+        if xpath.startswith(root):
+            # ElementTree explicitly warns that absolute xpaths don't
+            # work as expected, and need a prepended .
+            xpath = "." + xpath[len(root) :]
+
+        node = self._et.find(xpath, self.NAMESPACES)
+        if node is None:
+            return None
+        return node
+
+    ###############
+    # Simple APIs #
+    ###############
+
+    def copy_api(self):
+        return ETreeAPI(self._node_tostring(self._et.getroot()))
+
+    def count(self, xpath):
+        return len(self._et.findall(xpath, self.NAMESPACES) or [])
+
+    ####################
+    # Private XML APIs #
+    ####################
+
+    def _node_add_child(self, parentxpath, parentnode, newnode):
+        """
+        Add 'newnode' as a child of 'parentnode', but try to preserve
+        whitespace and nicely format the result.
+        """
+        xpathobj = XPath(parentxpath)
+
+        if bool(len(parentnode)):
+            lastelem = list(parentnode)[-1]
+            newnode.tail = lastelem.tail
+            lastelem.tail = parentnode.text
+        elif xpathobj.parent_xpath():
+            grandparent = self._find(xpathobj.parent_xpath())
+            idx = list(grandparent).index(parentnode)
+            if idx == (len(list(grandparent)) - 1):
+                parentnode.text = (grandparent.text or "\n") + "  "
+                newnode.tail = (parentnode.tail or "\n") + "  "
+            else:
+                parentnode.text = list(grandparent)[0].tail + "  "
+                newnode.tail = list(grandparent)[0].tail
+        else:
+            parentnode.text = "\n  "
+            newnode.tail = "\n"
+
+        parentnode.append(newnode)
+
+    def _node_has_content(self, node):
+        return len(node) or node.attrib or re.search(r"\w+", (node.text or ""))
+
+    def _node_remove_child(self, parentnode, childnode):
+        idx = list(parentnode).index(childnode)
+
+        if idx != 0 and idx == (len(list(parentnode)) - 1):
+            prevsibling = list(parentnode)[idx - 1]
+            prevsibling.tail = prevsibling.tail[:-2]
+        elif idx == 0 and len(list(parentnode)) == 1:
+            parentnode.text = None
+
+        parentnode.remove(childnode)
+
+    def _node_new(self, xpathseg, _parentnode):
+        newname = xpathseg.nodename
+        nsname = xpathseg.nsname
+        nsuri = self.NAMESPACES.get(nsname, None)
+
+        if nsname:
+            newname = "{%s}%s" % (nsuri, newname)
+        element = _VirtinstElement(newname)
+        if nsname and nsname not in self._namespaces:
+            self._namespaces[nsname] = nsuri
+            element.virtinst_add_namespace(nsname, nsuri)
+        return element
+
+    def _node_replace_child(self, xpath, newnode):
+        oldnode = self._find(xpath)
+        parentnode = self._find(xpath + "...")
+        for idx, elem in list(enumerate(parentnode)):
+            if elem != oldnode:
+                continue
+            newnode.tail = oldnode.tail
+            parentnode.remove(oldnode)
+            parentnode.insert(idx, newnode)
+            break
+
+    ####################
+    # XML editing APIs #
+    ####################
+
+    def node_clear(self, xpath):
+        node = self._find(xpath)
+        if node is not None:
+            for c in list(node):
+                node.remove(c)
+            node.attrib.clear()
+            node.text = None

Reply via email to