diff --git a/Doc/library/xml.etree.elementtree.rst b/Doc/library/xml.etree.elementtree.rst index 30a7b653f940e97..e7287ba1280088a 100644 --- a/Doc/library/xml.etree.elementtree.rst +++ b/Doc/library/xml.etree.elementtree.rst @@ -1179,7 +1179,9 @@ ElementTree Objects *xml_declaration* controls if an XML declaration should be added to the file. Use ``False`` for never, ``True`` for always, ``None`` for only if not US-ASCII or UTF-8 or Unicode (default is ``None``). - *default_namespace* sets the default XML namespace (for "xmlns"). + *default_namespace* sets the default XML namespace (for "xmlns"). This + option takes precedence over a default namespace registered using + :func:`register_namespace`. *method* is either ``"xml"``, ``"html"`` or ``"text"`` (default is ``"xml"``). The keyword-only *short_empty_elements* parameter controls the formatting diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py index bae61f754e75f59..6130b87525d455d 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -2074,6 +2074,14 @@ def test_xinclude_failures(self): class BugsTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls._orig_namespace_map = ET.register_namespace._namespace_map.copy() + + def tearDown(self): + ET.register_namespace._namespace_map.clear() + ET.register_namespace._namespace_map.update(self._orig_namespace_map) + def test_bug_xmltoolkit21(self): # marshaller gives obscure errors for non-string values @@ -2272,6 +2280,62 @@ def test_bug_200709_register_namespace(self): self.assertEqual(ET.tostring(e), b'') + def test_pre_gh118416_register_default_namespace_behaviour(self): + # All these examples worked prior to fixing bug gh118416 + ET.register_namespace("", "gh118416") + + # If the registered default prefix's URI is not used, don't raise an + # error + e = ET.Element("{other}elem") + self.assertEqual( + serialize(e), + '', + ) + # no error even with unqualified tags + e = ET.Element("elem") + self.assertEqual( + serialize(e), + '', + ) + + # Uses registered default prefix, if used in tree + e = ET.Element("{gh118416}elem") + self.assertEqual( + serialize(e), + '', + ) + + # Use explicitly provided default_namespace if registered default + # prefix is not used. + e = ET.Element("{default}elem") + ET.SubElement(e, "{other}otherEl") + self.assertEqual( + serialize(e, default_namespace="default"), + ( + '' + '' + '' + ), + ) + + def test_gh118416_register_default_namespace(self): + ET.register_namespace("", "gh118416") + e = ET.Element("{gh118416}elem") + ET.SubElement(e, "noPrefixElem") + with self.assertRaises(ValueError) as cm: + serialize(e) + self.assertEqual( + str(cm.exception), + "cannot use non-qualified names with default_namespace option", + ) + + e = ET.Element("{gh118416}elem") + # explicitly set default_namespace takes precedence + self.assertEqual( + serialize(e, default_namespace="otherdefault"), + '', + ) + def test_bug_200709_element_comment(self): # Not sure if this can be fixed, really (since the serializer needs # ET.Comment, not cET.comment). diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py index 9e15d34d22aa6c4..dfa89a54f71087c 100644 --- a/Lib/xml/etree/ElementTree.py +++ b/Lib/xml/etree/ElementTree.py @@ -724,9 +724,13 @@ def write(self, file_or_filename, if method == "text": _serialize_text(write, self._root) else: - qnames, namespaces = _namespaces(self._root, default_namespace) + qnames, namespaces_to_declare = _namespaces(self._root, default_namespace) + if default_namespace: + # Always write the default_namespace if provided, even if + # it wasn't referenced in the xml + namespaces_to_declare[default_namespace] = "" serialize = _serialize[method] - serialize(write, self._root, qnames, namespaces, + serialize(write, self._root, qnames, namespaces_to_declare, short_empty_elements=short_empty_elements) def write_c14n(self, file): @@ -792,21 +796,41 @@ def _namespaces(elem, default_namespace=None): # maps qnames to *encoded* prefix:local names qnames = {None: None} + global_default_uri = None + for uri, prefix in list(_namespace_map.items()): + if prefix == "": + global_default_uri = uri + # register_namespaces() only allows a unique set of prefixes so "" + # will only occur once + break + # maps uri:s to prefixes namespaces = {} if default_namespace: - namespaces[default_namespace] = "" + namespace_map = _namespace_map.copy() + if global_default_uri: + del namespace_map[global_default_uri] + namespace_map[default_namespace] = "" + else: + namespace_map = _namespace_map def add_qname(qname): # calculate serialized qname representation + found_unqualified = False try: if qname[:1] == "{": uri, tag = qname[1:].rsplit("}", 1) prefix = namespaces.get(uri) if prefix is None: - prefix = _namespace_map.get(uri) + prefix = namespace_map.get(uri) if prefix is None: - prefix = "ns%d" % len(namespaces) + cnt = len(namespaces) + # Keep the pre-gh118416 numbering scheme where the + # default namespace was always considered the first + # namespace + if default_namespace and default_namespace not in namespaces: + cnt += 1 + prefix = "ns%d" % cnt if prefix != "xml": namespaces[uri] = prefix if prefix: @@ -820,31 +844,41 @@ def add_qname(qname): "cannot use non-qualified names with " "default_namespace option" ) + found_unqualified = True qnames[qname] = qname except TypeError: _raise_serialization_error(qname) + return found_unqualified # populate qname and namespaces table + found_unqualified = False for elem in elem.iter(): tag = elem.tag if isinstance(tag, QName): if tag.text not in qnames: - add_qname(tag.text) + found_unqualified |= add_qname(tag.text) elif isinstance(tag, str): if tag not in qnames: - add_qname(tag) + found_unqualified |= add_qname(tag) elif tag is not None and tag is not Comment and tag is not PI: _raise_serialization_error(tag) for key, value in elem.items(): if isinstance(key, QName): key = key.text if key not in qnames: - add_qname(key) + found_unqualified |= add_qname(key) if isinstance(value, QName) and value.text not in qnames: - add_qname(value.text) + found_unqualified |= add_qname(value.text) text = elem.text if isinstance(text, QName) and text.text not in qnames: - add_qname(text.text) + found_unqualified |= add_qname(text.text) + + if found_unqualified and global_default_uri in namespaces: + # A deferred check of using unqualified tags when a global default + # namespace is registered + raise ValueError( + "cannot use non-qualified names with default_namespace option" + ) return qnames, namespaces def _serialize_xml(write, elem, qnames, namespaces, diff --git a/Misc/NEWS.d/next/Library/2024-04-30-15-01-56.gh-issue-118416.3hzeti.rst b/Misc/NEWS.d/next/Library/2024-04-30-15-01-56.gh-issue-118416.3hzeti.rst new file mode 100644 index 000000000000000..0fc6892f2518187 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-04-30-15-01-56.gh-issue-118416.3hzeti.rst @@ -0,0 +1,3 @@ +Fix possible issues in :mod:`xml.etree.ElementTree` when serialising xml if +a default namespace prefix (``""``) has been registered with +:func:`xml.etree.ElementTree.register_namespace()`