diff --git a/Doc/library/xml.sax.utils.rst b/Doc/library/xml.sax.utils.rst
index 95099f67092..ff36fd89e89 100644
--- a/Doc/library/xml.sax.utils.rst
+++ b/Doc/library/xml.sax.utils.rst
@@ -50,13 +50,19 @@ or as base classes.
using the reference concrete syntax.
-.. class:: XMLGenerator(out=None, encoding='iso-8859-1')
+.. class:: XMLGenerator(out=None, encoding='iso-8859-1', short_empty_elements=False)
This class implements the :class:`ContentHandler` interface by writing SAX
events back into an XML document. In other words, using an :class:`XMLGenerator`
as the content handler will reproduce the original document being parsed. *out*
should be a file-like object which will default to *sys.stdout*. *encoding* is
the encoding of the output stream which defaults to ``'iso-8859-1'``.
+ *short_empty_elements* controls the formatting of elements that contain no
+ content: if *False* (the default) they are emitted as a pair of start/end
+ tags, if set to *True* they are emitted as a single self-closed tag.
+
+ .. versionadded:: 3.2
+ short_empty_elements
.. class:: XMLFilterBase(base)
diff --git a/Lib/test/test_sax.py b/Lib/test/test_sax.py
index 911e7634c24..143ddf2940d 100644
--- a/Lib/test/test_sax.py
+++ b/Lib/test/test_sax.py
@@ -170,6 +170,16 @@ class XmlgenTest(unittest.TestCase):
self.assertEquals(result.getvalue(), start + "")
+ def test_xmlgen_basic_empty(self):
+ result = StringIO()
+ gen = XMLGenerator(result, short_empty_elements=True)
+ gen.startDocument()
+ gen.startElement("doc", {})
+ gen.endElement("doc")
+ gen.endDocument()
+
+ self.assertEquals(result.getvalue(), start + "")
+
def test_xmlgen_content(self):
result = StringIO()
gen = XMLGenerator(result)
@@ -182,6 +192,18 @@ class XmlgenTest(unittest.TestCase):
self.assertEquals(result.getvalue(), start + "huhei")
+ def test_xmlgen_content_empty(self):
+ result = StringIO()
+ gen = XMLGenerator(result, short_empty_elements=True)
+
+ gen.startDocument()
+ gen.startElement("doc", {})
+ gen.characters("huhei")
+ gen.endElement("doc")
+ gen.endDocument()
+
+ self.assertEquals(result.getvalue(), start + "huhei")
+
def test_xmlgen_pi(self):
result = StringIO()
gen = XMLGenerator(result)
@@ -239,6 +261,18 @@ class XmlgenTest(unittest.TestCase):
self.assertEquals(result.getvalue(), start + " ")
+ def test_xmlgen_ignorable_empty(self):
+ result = StringIO()
+ gen = XMLGenerator(result, short_empty_elements=True)
+
+ gen.startDocument()
+ gen.startElement("doc", {})
+ gen.ignorableWhitespace(" ")
+ gen.endElement("doc")
+ gen.endDocument()
+
+ self.assertEquals(result.getvalue(), start + " ")
+
def test_xmlgen_ns(self):
result = StringIO()
gen = XMLGenerator(result)
@@ -257,6 +291,24 @@ class XmlgenTest(unittest.TestCase):
('' %
ns_uri))
+ def test_xmlgen_ns_empty(self):
+ result = StringIO()
+ gen = XMLGenerator(result, short_empty_elements=True)
+
+ gen.startDocument()
+ gen.startPrefixMapping("ns1", ns_uri)
+ gen.startElementNS((ns_uri, "doc"), "ns1:doc", {})
+ # add an unqualified name
+ gen.startElementNS((None, "udoc"), None, {})
+ gen.endElementNS((None, "udoc"), None)
+ gen.endElementNS((ns_uri, "doc"), "ns1:doc")
+ gen.endPrefixMapping("ns1")
+ gen.endDocument()
+
+ self.assertEquals(result.getvalue(), start + \
+ ('' %
+ ns_uri))
+
def test_1463026_1(self):
result = StringIO()
gen = XMLGenerator(result)
@@ -268,6 +320,17 @@ class XmlgenTest(unittest.TestCase):
self.assertEquals(result.getvalue(), start+'')
+ def test_1463026_1_empty(self):
+ result = StringIO()
+ gen = XMLGenerator(result, short_empty_elements=True)
+
+ gen.startDocument()
+ gen.startElementNS((None, 'a'), 'a', {(None, 'b'):'c'})
+ gen.endElementNS((None, 'a'), 'a')
+ gen.endDocument()
+
+ self.assertEquals(result.getvalue(), start+'')
+
def test_1463026_2(self):
result = StringIO()
gen = XMLGenerator(result)
@@ -281,6 +344,19 @@ class XmlgenTest(unittest.TestCase):
self.assertEquals(result.getvalue(), start+'')
+ def test_1463026_2_empty(self):
+ result = StringIO()
+ gen = XMLGenerator(result, short_empty_elements=True)
+
+ gen.startDocument()
+ gen.startPrefixMapping(None, 'qux')
+ gen.startElementNS(('qux', 'a'), 'a', {})
+ gen.endElementNS(('qux', 'a'), 'a')
+ gen.endPrefixMapping(None)
+ gen.endDocument()
+
+ self.assertEquals(result.getvalue(), start+'')
+
def test_1463026_3(self):
result = StringIO()
gen = XMLGenerator(result)
@@ -295,6 +371,20 @@ class XmlgenTest(unittest.TestCase):
self.assertEquals(result.getvalue(),
start+'')
+ def test_1463026_3_empty(self):
+ result = StringIO()
+ gen = XMLGenerator(result, short_empty_elements=True)
+
+ gen.startDocument()
+ gen.startPrefixMapping('my', 'qux')
+ gen.startElementNS(('qux', 'a'), 'a', {(None, 'b'):'c'})
+ gen.endElementNS(('qux', 'a'), 'a')
+ gen.endPrefixMapping('my')
+ gen.endDocument()
+
+ self.assertEquals(result.getvalue(),
+ start+'')
+
class XMLFilterBaseTest(unittest.TestCase):
def test_filter_basic(self):
diff --git a/Lib/xml/sax/saxutils.py b/Lib/xml/sax/saxutils.py
index e8450158cf2..46946fcf72e 100644
--- a/Lib/xml/sax/saxutils.py
+++ b/Lib/xml/sax/saxutils.py
@@ -78,7 +78,7 @@ def quoteattr(data, entities={}):
class XMLGenerator(handler.ContentHandler):
- def __init__(self, out=None, encoding="iso-8859-1"):
+ def __init__(self, out=None, encoding="iso-8859-1", short_empty_elements=False):
if out is None:
import sys
out = sys.stdout
@@ -88,6 +88,8 @@ class XMLGenerator(handler.ContentHandler):
self._current_context = self._ns_contexts[-1]
self._undeclared_ns_maps = []
self._encoding = encoding
+ self._short_empty_elements = short_empty_elements
+ self._pending_start_element = False
def _write(self, text):
if isinstance(text, str):
@@ -106,6 +108,11 @@ class XMLGenerator(handler.ContentHandler):
# Return the unqualified name
return name[1]
+ def _finish_pending_start_element(self,endElement=False):
+ if self._pending_start_element:
+ self._write('>')
+ self._pending_start_element = False
+
# ContentHandler methods
def startDocument(self):
@@ -122,15 +129,24 @@ class XMLGenerator(handler.ContentHandler):
del self._ns_contexts[-1]
def startElement(self, name, attrs):
+ self._finish_pending_start_element()
self._write('<' + name)
for (name, value) in attrs.items():
self._write(' %s=%s' % (name, quoteattr(value)))
- self._write('>')
+ if self._short_empty_elements:
+ self._pending_start_element = True
+ else:
+ self._write(">")
def endElement(self, name):
- self._write('%s>' % name)
+ if self._pending_start_element:
+ self._write('/>')
+ self._pending_start_element = False
+ else:
+ self._write('%s>' % name)
def startElementNS(self, name, qname, attrs):
+ self._finish_pending_start_element()
self._write('<' + self._qname(name))
for prefix, uri in self._undeclared_ns_maps:
@@ -142,18 +158,30 @@ class XMLGenerator(handler.ContentHandler):
for (name, value) in attrs.items():
self._write(' %s=%s' % (self._qname(name), quoteattr(value)))
- self._write('>')
+ if self._short_empty_elements:
+ self._pending_start_element = True
+ else:
+ self._write(">")
def endElementNS(self, name, qname):
- self._write('%s>' % self._qname(name))
+ if self._pending_start_element:
+ self._write('/>')
+ self._pending_start_element = False
+ else:
+ self._write('%s>' % self._qname(name))
def characters(self, content):
- self._write(escape(content))
+ if content:
+ self._finish_pending_start_element()
+ self._write(escape(content))
def ignorableWhitespace(self, content):
- self._write(content)
+ if content:
+ self._finish_pending_start_element()
+ self._write(content)
def processingInstruction(self, target, data):
+ self._finish_pending_start_element()
self._write('%s %s?>' % (target, data))
diff --git a/Misc/NEWS b/Misc/NEWS
index fa3725a8da7..188947833ef 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -34,6 +34,9 @@ Core and Builtins
Library
-------
+- Issue #1343: xml.sax.saxutils.XMLGenerator now has an option
+ short_empty_elements to direct it to use self-closing tags when appropriate.
+
- Issue #9807 (part 1): Expose the ABI flags in sys.abiflags. Add --abiflags
switch to python-config for command line access.