Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Doc/library/xml.dom.minidom.rst
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ module documentation. This section lists the differences between the API and
.. versionchanged:: 3.9
The *standalone* parameter was added.

.. versionchanged:: next
Namespace declarations missing for the serialized element
and its attributes are now written.

.. method:: Node.toxml(encoding=None, standalone=None)

Return a string or byte string containing the XML represented by
Expand Down
86 changes: 86 additions & 0 deletions Lib/test/test_minidom.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,92 @@ def testWriteXML(self):
dom.unlink()
self.assertEqual(str, domstr)

def testWriteXMLNamespaceDeclarations(self):
dom = Document()
root = dom.appendChild(
dom.createElementNS("http://xml.python.org/ns", "p:root"))
child = root.appendChild(
dom.createElementNS("http://xml.python.org/ns", "p:child"))
child.setAttributeNS("http://xml.python.org/ns2", "q:attr", "value")
self.assertEqual(dom.documentElement.toxml(),
'<p:root xmlns:p="http://xml.python.org/ns">'
'<p:child xmlns:q="http://xml.python.org/ns2" '
'q:attr="value"/></p:root>')
dom.unlink()

def testWriteXMLDefaultNamespace(self):
dom = Document()
root = dom.appendChild(
dom.createElementNS("http://xml.python.org/ns", "root"))
root.appendChild(
dom.createElementNS("http://xml.python.org/ns", "child"))
# An element in no namespace undeclares the default namespace.
root.appendChild(dom.createElement("nons"))
self.assertEqual(dom.documentElement.toxml(),
'<root xmlns="http://xml.python.org/ns">'
'<child/><nons xmlns=""/></root>')
dom.unlink()

def testWriteXMLAttributeNamespacePrefix(self):
dom = Document()
root = dom.appendChild(dom.createElement("root"))
# Attributes cannot use the default namespace, a prefix is invented.
root.setAttributeNS("http://xml.python.org/ns", "attr", "value")
root.setAttributeNS("http://xml.python.org/ns2", "attr2", "value2")
self.assertEqual(dom.documentElement.toxml(),
'<root xmlns:ns0="http://xml.python.org/ns" '
'xmlns:ns1="http://xml.python.org/ns2" '
'ns0:attr="value" ns1:attr2="value2"/>')
dom.unlink()

def testWriteXMLXMLPrefix(self):
dom = Document()
root = dom.appendChild(dom.createElement("root"))
# The "xml" prefix is bound by definition and is never declared.
root.setAttributeNS(xml.dom.XML_NAMESPACE, "xml:lang", "en")
self.assertEqual(dom.documentElement.toxml(), '<root xml:lang="en"/>')
dom.unlink()

def testWriteXMLExistingNamespaceDeclarations(self):
for str in [
'<p:root xmlns:p="http://xml.python.org/ns"><p:child/></p:root>',
'<root xmlns="http://xml.python.org/ns"><child xmlns=""/></root>',
'<p:root xmlns:p="http://xml.python.org/ns">'
'<p:child xmlns:p="http://xml.python.org/ns2"/></p:root>',
'<root xmlns:p="http://xml.python.org/ns" p:attr="value"/>',
]:
with self.subTest(str=str):
dom = parseString(str)
self.assertEqual(dom.documentElement.toxml(), str)
dom.unlink()

def testWriteXMLNotANamespaceDeclaration(self):
# an attribute whose name only starts with "xmlns" is not one
dom = parseString('<root xmlns="http://xml.python.org/ns">'
'<child xmlnsabc="v"><g/></child></root>')
self.assertEqual(dom.documentElement.toxml(),
'<root xmlns="http://xml.python.org/ns">'
'<child xmlnsabc="v"><g/></child></root>')
dom.unlink()

dom = Document()
root = dom.appendChild(
dom.createElementNS("http://xml.python.org/ns", "root"))
child = root.appendChild(dom.createElement("child"))
child.setAttribute("xmlnsabc", "v")
self.assertEqual(dom.documentElement.toxml(),
'<root xmlns="http://xml.python.org/ns">'
'<child xmlns="" xmlnsabc="v"/></root>')
dom.unlink()

def testWriteXMLDoesNotModifyDocument(self):
dom = Document()
root = dom.appendChild(
dom.createElementNS("http://xml.python.org/ns", "p:root"))
root.toxml()
self.assertEqual(root.attributes.length, 0)
dom.unlink()

def test_toxml_quote_text(self):
dom = Document()
elem = dom.appendChild(dom.createElement('elem'))
Expand Down
119 changes: 112 additions & 7 deletions Lib/xml/dom/minidom.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
import xml
import xml.dom

from xml.dom import EMPTY_NAMESPACE, EMPTY_PREFIX, XMLNS_NAMESPACE, domreg
from xml.dom import (EMPTY_NAMESPACE, EMPTY_PREFIX, XML_NAMESPACE,
XMLNS_NAMESPACE, domreg)
from xml.dom.minicompat import *
from xml.dom.xmlbuilder import DOMImplementationLS, DocumentLS

Expand Down Expand Up @@ -346,6 +347,100 @@ def _write_data(writer, text, attr):
text = text.replace("\t", "&#9;")
writer.write(text)


# The "xml" prefix is bound by definition and is never declared.
_ROOT_NSMAP = {"xml": XML_NAMESPACE}


def _bind_namespace(nsmap, inherited, prefix, uri):
"""Bind *prefix* in *nsmap*, copying it if it is still the inherited one."""
if nsmap is inherited:
nsmap = dict(inherited)
nsmap[prefix] = uri
return nsmap


def _in_scope_namespaces(element):
"""Return the namespaces in scope for *element*, as written by writexml."""
ancestors = []
node = element.parentNode
while node is not None and node.nodeType == Node.ELEMENT_NODE:
ancestors.append(node)
node = node.parentNode
nsmap = _ROOT_NSMAP
for node in reversed(ancestors):
nsmap, _ = _fixup_namespaces(node, nsmap)
return nsmap


def _fixup_namespaces(element, nsmap):
"""Compute namespace declarations missing for the serialized element.

*nsmap* is the mapping of prefixes to namespace URIs in scope for the
element. Return the mapping in scope for its children and the list of
(name, value) pairs of the attributes to be written, starting with the
added namespace declarations. The element and its attributes are not
modified.
"""
attrs = element._attrs
uri = element.namespaceURI
if not attrs and not uri and not nsmap.get(None):
# Neither the element nor its attributes need a declaration.
return nsmap, ()

inherited = nsmap
declarations = []
# (name, value, namespace URI, attribute) of the attributes to write.
entries = []
if attrs:
for attr in attrs.values():
name = attr.name
attr_uri = attr.namespaceURI
if (attr_uri == XMLNS_NAMESPACE or name == "xmlns"
or name.startswith("xmlns:")):
# Declarations already present in the document take precedence.
nsmap = _bind_namespace(
nsmap, inherited,
attr.localName if attr.prefix else None, attr.value)
attr_uri = None
elif attr_uri == XML_NAMESPACE:
# The xml prefix is bound by definition.
attr_uri = None
entries.append((name, attr.value, attr_uri, attr))

if uri:
prefix, _, _ = element.tagName.rpartition(':')
prefix = prefix or None
if nsmap.get(prefix) != uri:
nsmap = _bind_namespace(nsmap, inherited, prefix, uri)
declarations.append(("xmlns:" + prefix if prefix else "xmlns", uri))
elif nsmap.get(None) and ':' not in element.tagName:
# The element is in no namespace, undeclare the default one.
nsmap = _bind_namespace(nsmap, inherited, None, None)
declarations.append(("xmlns", ""))

items = []
for name, value, attr_uri, attr in entries:
if attr_uri is not None:
# Unprefixed attributes are in no namespace, so an attribute
# in a namespace always needs a prefix.
prefix, _, _ = name.rpartition(':')
if not prefix:
n = 0
while nsmap.get("ns%d" % n) is not None:
n += 1
prefix = "ns%d" % n
name = "%s:%s" % (prefix, attr.localName)
if nsmap.get(prefix) != attr_uri:
nsmap = _bind_namespace(nsmap, inherited, prefix, attr_uri)
declarations.append(("xmlns:" + prefix, attr_uri))
items.append((name, value))

if declarations:
return nsmap, declarations + items
return nsmap, items


def _get_elements_by_tagName_helper(parent, name, rc):
for node in parent.childNodes:
if node.nodeType == Node.ELEMENT_NODE and \
Expand Down Expand Up @@ -914,7 +1009,8 @@ def getElementsByTagNameNS(self, namespaceURI, localName):
def __repr__(self):
return "<DOM Element: %s at %#x>" % (self.tagName, id(self))

def writexml(self, writer, indent="", addindent="", newl=""):
def writexml(self, writer, indent="", addindent="", newl="", *,
_nsmap=None):
"""Write an XML element to a file-like object

Write the element to the writer object that must provide
Expand All @@ -923,13 +1019,14 @@ def writexml(self, writer, indent="", addindent="", newl=""):
# indent = current indentation
# addindent = indentation to add to higher levels
# newl = newline string
if _nsmap is None:
_nsmap = _in_scope_namespaces(self)
writer.write(indent+"<" + self.tagName)

attrs = self._get_attributes()

for a_name in attrs.keys():
nsmap, items = _fixup_namespaces(self, _nsmap)
for a_name, value in items:
writer.write(" %s=\"" % a_name)
_write_data(writer, attrs[a_name].value, True)
_write_data(writer, value, True)
writer.write("\"")
if self.childNodes:
writer.write(">")
Expand All @@ -940,7 +1037,15 @@ def writexml(self, writer, indent="", addindent="", newl=""):
else:
writer.write(newl)
for node in self.childNodes:
node.writexml(writer, indent+addindent, addindent, newl)
if type(node).writexml is Element.writexml:
# Pass the namespaces in scope to the standard
# implementation; an overridden writexml() has the
# documented signature and computes them itself.
node.writexml(writer, indent+addindent, addindent,
newl, _nsmap=nsmap)
else:
node.writexml(writer, indent+addindent, addindent,
newl)
writer.write(indent)
writer.write("</%s>%s" % (self.tagName, newl))
else:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:meth:`~xml.dom.minidom.Node.writexml` in :mod:`xml.dom.minidom` now writes
the namespace declarations needed to serialize the namespaces of the element
and its attributes, if they are not already declared for an ancestor. The
document is not modified.
Loading