Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit da5a155

Browse files
gh-44376: Write missing namespace declarations in xml.dom.minidom (GH-156674)
Element.writexml() now writes the xmlns declarations needed for the namespaces of the element and its attributes, if they are not already declared for an ancestor. A prefix in scope is reused for an attribute in a namespace without a prefix, or a new one is invented, because attributes cannot use the default namespace. The document is not modified by the serialization.
1 parent 01399a3 commit da5a155

4 files changed

Lines changed: 251 additions & 6 deletions

File tree

Doc/library/xml.dom.minidom.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ module documentation. This section lists the differences between the API and
154154
.. versionchanged:: 3.9
155155
The *standalone* parameter was added.
156156

157+
.. versionchanged:: next
158+
Namespace declarations missing for the serialized element
159+
and its attributes are now written.
160+
157161
.. method:: Node.toxml(encoding=None, standalone=None)
158162

159163
Return a string or byte string containing the XML represented by

Lib/test/test_minidom.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,130 @@ def testWriteXML(self):
571571
dom.unlink()
572572
self.assertEqual(str, domstr)
573573

574+
def testWriteXMLNamespaceDeclarations(self):
575+
dom = Document()
576+
root = dom.appendChild(
577+
dom.createElementNS("http://xml.python.org/ns", "p:root"))
578+
child = root.appendChild(
579+
dom.createElementNS("http://xml.python.org/ns", "p:child"))
580+
child.setAttributeNS("http://xml.python.org/ns2", "q:attr", "value")
581+
self.assertEqual(dom.documentElement.toxml(),
582+
'<p:root xmlns:p="http://xml.python.org/ns">'
583+
'<p:child xmlns:q="http://xml.python.org/ns2" '
584+
'q:attr="value"/></p:root>')
585+
dom.unlink()
586+
587+
def testWriteXMLDefaultNamespace(self):
588+
dom = Document()
589+
root = dom.appendChild(
590+
dom.createElementNS("http://xml.python.org/ns", "root"))
591+
root.appendChild(
592+
dom.createElementNS("http://xml.python.org/ns", "child"))
593+
# An element in no namespace undeclares the default namespace.
594+
root.appendChild(dom.createElement("nons"))
595+
self.assertEqual(dom.documentElement.toxml(),
596+
'<root xmlns="http://xml.python.org/ns">'
597+
'<child/><nons xmlns=""/></root>')
598+
dom.unlink()
599+
600+
def testWriteXMLAttributeNamespacePrefix(self):
601+
dom = Document()
602+
root = dom.appendChild(dom.createElement("root"))
603+
# Attributes cannot use the default namespace, a prefix is invented.
604+
root.setAttributeNS("http://xml.python.org/ns", "attr", "value")
605+
root.setAttributeNS("http://xml.python.org/ns2", "attr2", "value2")
606+
self.assertEqual(dom.documentElement.toxml(),
607+
'<root xmlns:ns0="http://xml.python.org/ns" '
608+
'xmlns:ns1="http://xml.python.org/ns2" '
609+
'ns0:attr="value" ns1:attr2="value2"/>')
610+
# The same namespace gets the same prefix.
611+
root.setAttributeNS("http://xml.python.org/ns", "attr3", "value3")
612+
self.assertEqual(dom.documentElement.toxml(),
613+
'<root xmlns:ns0="http://xml.python.org/ns" '
614+
'xmlns:ns1="http://xml.python.org/ns2" '
615+
'ns0:attr="value" ns1:attr2="value2" ns0:attr3="value3"/>')
616+
dom.unlink()
617+
618+
def testWriteXMLAttributeNamespacePrefixReused(self):
619+
# A prefix already bound to the namespace of the attribute is used.
620+
dom = Document()
621+
root = dom.appendChild(
622+
dom.createElementNS("http://xml.python.org/ns", "p:root"))
623+
root.setAttributeNS("http://xml.python.org/ns", "attr", "value")
624+
self.assertEqual(dom.documentElement.toxml(),
625+
'<p:root xmlns:p="http://xml.python.org/ns" p:attr="value"/>')
626+
# The prefix can be bound for an ancestor.
627+
child = root.appendChild(dom.createElement("child"))
628+
child.setAttributeNS("http://xml.python.org/ns", "attr", "value")
629+
self.assertEqual(child.toxml(), '<child p:attr="value"/>')
630+
# The prefix bound for a preceding attribute is reused.
631+
root.setAttributeNS("http://xml.python.org/ns3", "q:attr3", "value3")
632+
root.setAttributeNS("http://xml.python.org/ns3", "attr4", "value4")
633+
self.assertEqual(dom.documentElement.toxml(),
634+
'<p:root xmlns:p="http://xml.python.org/ns" '
635+
'xmlns:q="http://xml.python.org/ns3" '
636+
'p:attr="value" q:attr3="value3" q:attr4="value4">'
637+
'<child p:attr="value"/></p:root>')
638+
root.removeAttributeNS("http://xml.python.org/ns3", "attr3")
639+
root.removeAttributeNS("http://xml.python.org/ns3", "attr4")
640+
# The prefix must not be taken by an explicit declaration.
641+
root.setAttributeNS(xml.dom.XMLNS_NAMESPACE, "xmlns:ns0", "other")
642+
root.setAttributeNS("http://xml.python.org/ns2", "attr2", "value2")
643+
self.assertEqual(dom.documentElement.toxml(),
644+
'<p:root xmlns:p="http://xml.python.org/ns" '
645+
'xmlns:ns1="http://xml.python.org/ns2" '
646+
'p:attr="value" xmlns:ns0="other" ns1:attr2="value2">'
647+
'<child p:attr="value"/></p:root>')
648+
dom.unlink()
649+
650+
def testWriteXMLXMLPrefix(self):
651+
dom = Document()
652+
root = dom.appendChild(dom.createElement("root"))
653+
# The "xml" prefix is bound by definition and is never declared.
654+
root.setAttributeNS(xml.dom.XML_NAMESPACE, "xml:lang", "en")
655+
self.assertEqual(dom.documentElement.toxml(), '<root xml:lang="en"/>')
656+
dom.unlink()
657+
658+
def testWriteXMLExistingNamespaceDeclarations(self):
659+
for str in [
660+
'<p:root xmlns:p="http://xml.python.org/ns"><p:child/></p:root>',
661+
'<root xmlns="http://xml.python.org/ns"><child xmlns=""/></root>',
662+
'<p:root xmlns:p="http://xml.python.org/ns">'
663+
'<p:child xmlns:p="http://xml.python.org/ns2"/></p:root>',
664+
'<root xmlns:p="http://xml.python.org/ns" p:attr="value"/>',
665+
]:
666+
with self.subTest(str=str):
667+
dom = parseString(str)
668+
self.assertEqual(dom.documentElement.toxml(), str)
669+
dom.unlink()
670+
671+
def testWriteXMLNotANamespaceDeclaration(self):
672+
# an attribute whose name only starts with "xmlns" is not one
673+
dom = parseString('<root xmlns="http://xml.python.org/ns">'
674+
'<child xmlnsabc="v"><g/></child></root>')
675+
self.assertEqual(dom.documentElement.toxml(),
676+
'<root xmlns="http://xml.python.org/ns">'
677+
'<child xmlnsabc="v"><g/></child></root>')
678+
dom.unlink()
679+
680+
dom = Document()
681+
root = dom.appendChild(
682+
dom.createElementNS("http://xml.python.org/ns", "root"))
683+
child = root.appendChild(dom.createElement("child"))
684+
child.setAttribute("xmlnsabc", "v")
685+
self.assertEqual(dom.documentElement.toxml(),
686+
'<root xmlns="http://xml.python.org/ns">'
687+
'<child xmlns="" xmlnsabc="v"/></root>')
688+
dom.unlink()
689+
690+
def testWriteXMLDoesNotModifyDocument(self):
691+
dom = Document()
692+
root = dom.appendChild(
693+
dom.createElementNS("http://xml.python.org/ns", "p:root"))
694+
root.toxml()
695+
self.assertEqual(root.attributes.length, 0)
696+
dom.unlink()
697+
574698
def test_toxml_quote_text(self):
575699
dom = Document()
576700
elem = dom.appendChild(dom.createElement('elem'))

Lib/xml/dom/minidom.py

Lines changed: 119 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,109 @@ def _write_data(writer, text, attr):
379379
text = text.replace("\t", "&#9;")
380380
writer.write(text)
381381

382+
383+
# The "xml" prefix is bound by definition and is never declared.
384+
_ROOT_NSMAP = {"xml": XML_NAMESPACE}
385+
386+
387+
def _bind_namespace(nsmap, inherited, prefix, uri):
388+
"""Bind *prefix* in *nsmap*, copying it if it is still the inherited one."""
389+
if nsmap is inherited:
390+
nsmap = dict(inherited)
391+
nsmap[prefix] = uri
392+
return nsmap
393+
394+
395+
def _in_scope_namespaces(element):
396+
"""Return the namespaces in scope for *element*, as written by writexml."""
397+
ancestors = []
398+
node = element.parentNode
399+
while node is not None and node.nodeType == Node.ELEMENT_NODE:
400+
ancestors.append(node)
401+
node = node.parentNode
402+
nsmap = _ROOT_NSMAP
403+
for node in reversed(ancestors):
404+
nsmap, _ = _fixup_namespaces(node, nsmap)
405+
return nsmap
406+
407+
408+
def _fixup_namespaces(element, nsmap):
409+
"""Compute namespace declarations missing for the serialized element.
410+
411+
*nsmap* is the mapping of prefixes to namespace URIs in scope for the
412+
element. Return the mapping in scope for its children and the list of
413+
(name, value) pairs of the attributes to be written, starting with the
414+
added namespace declarations. The element and its attributes are not
415+
modified.
416+
"""
417+
attrs = element._attrs
418+
uri = element.namespaceURI
419+
if not attrs and not uri and not nsmap.get(None):
420+
# Neither the element nor its attributes need a declaration.
421+
return nsmap, ()
422+
423+
inherited = nsmap
424+
declarations = []
425+
# (name, value, namespace URI, attribute) of the attributes to write.
426+
entries = []
427+
if attrs:
428+
for attr in attrs.values():
429+
name = attr.name
430+
attr_uri = attr.namespaceURI
431+
if (attr_uri == XMLNS_NAMESPACE or name == "xmlns"
432+
or name.startswith("xmlns:")):
433+
# Declarations already present in the document take precedence.
434+
nsmap = _bind_namespace(
435+
nsmap, inherited,
436+
attr.localName if attr.prefix else None, attr.value)
437+
attr_uri = None
438+
elif attr_uri == XML_NAMESPACE:
439+
# The xml prefix is bound by definition.
440+
attr_uri = None
441+
entries.append((name, attr.value, attr_uri, attr))
442+
443+
if uri:
444+
prefix, _, _ = element.tagName.rpartition(':')
445+
prefix = prefix or None
446+
if nsmap.get(prefix) != uri:
447+
nsmap = _bind_namespace(nsmap, inherited, prefix, uri)
448+
declarations.append(("xmlns:" + prefix if prefix else "xmlns", uri))
449+
elif nsmap.get(None) and ':' not in element.tagName:
450+
# The element is in no namespace, undeclare the default one.
451+
nsmap = _bind_namespace(nsmap, inherited, None, None)
452+
declarations.append(("xmlns", ""))
453+
454+
items = []
455+
prefixes = None # namespace URI -> prefix, built only when needed
456+
n = 0
457+
for name, value, attr_uri, attr in entries:
458+
if attr_uri is not None:
459+
# Unprefixed attributes are in no namespace, so an attribute
460+
# in a namespace always needs a prefix.
461+
prefix, _, _ = name.rpartition(':')
462+
if not prefix:
463+
# Reuse a prefix bound to the namespace, or invent one.
464+
if prefixes is None:
465+
prefixes = {u: p for p, u in nsmap.items()
466+
if p is not None}
467+
prefix = prefixes.get(attr_uri)
468+
if prefix is None:
469+
while nsmap.get("ns%d" % n) is not None:
470+
n += 1
471+
prefix = "ns%d" % n
472+
name = "%s:%s" % (prefix, attr.localName)
473+
if nsmap.get(prefix) != attr_uri:
474+
nsmap = _bind_namespace(nsmap, inherited, prefix, attr_uri)
475+
declarations.append(("xmlns:" + prefix, attr_uri))
476+
if prefixes is not None:
477+
prefixes[attr_uri] = prefix
478+
items.append((name, value))
479+
480+
if declarations:
481+
return nsmap, declarations + items
482+
return nsmap, items
483+
484+
382485
def _get_elements_by_tagName_helper(parent, name, rc):
383486
for node in parent.childNodes:
384487
if node.nodeType == Node.ELEMENT_NODE and \
@@ -944,7 +1047,8 @@ def getElementsByTagNameNS(self, namespaceURI, localName):
9441047
def __repr__(self):
9451048
return "<DOM Element: %s at %#x>" % (self.tagName, id(self))
9461049

947-
def writexml(self, writer, indent="", addindent="", newl=""):
1050+
def writexml(self, writer, indent="", addindent="", newl="", *,
1051+
_nsmap=None):
9481052
"""Write an XML element to a file-like object
9491053
9501054
Write the element to the writer object that must provide
@@ -953,13 +1057,14 @@ def writexml(self, writer, indent="", addindent="", newl=""):
9531057
# indent = current indentation
9541058
# addindent = indentation to add to higher levels
9551059
# newl = newline string
1060+
if _nsmap is None:
1061+
_nsmap = _in_scope_namespaces(self)
9561062
writer.write(indent+"<" + self.tagName)
9571063

958-
attrs = self._get_attributes()
959-
960-
for a_name in attrs.keys():
1064+
nsmap, items = _fixup_namespaces(self, _nsmap)
1065+
for a_name, value in items:
9611066
writer.write(" %s=\"" % a_name)
962-
_write_data(writer, attrs[a_name].value, True)
1067+
_write_data(writer, value, True)
9631068
writer.write("\"")
9641069
if self.childNodes:
9651070
writer.write(">")
@@ -974,7 +1079,15 @@ def writexml(self, writer, indent="", addindent="", newl=""):
9741079
else:
9751080
writer.write(newl)
9761081
for node in self.childNodes:
977-
node.writexml(writer, indent+addindent, addindent, newl)
1082+
if type(node).writexml is Element.writexml:
1083+
# Pass the namespaces in scope to the standard
1084+
# implementation; an overridden writexml() has the
1085+
# documented signature and computes them itself.
1086+
node.writexml(writer, indent+addindent, addindent,
1087+
newl, _nsmap=nsmap)
1088+
else:
1089+
node.writexml(writer, indent+addindent, addindent,
1090+
newl)
9781091
writer.write(indent)
9791092
writer.write("</%s>%s" % (self.tagName, newl))
9801093
else:
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:meth:`~xml.dom.minidom.Node.writexml` in :mod:`xml.dom.minidom` now writes
2+
the namespace declarations needed to serialize the namespaces of the element
3+
and its attributes, if they are not already declared for an ancestor. The
4+
document is not modified.

0 commit comments

Comments
 (0)