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()`