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('' % name) + if self._pending_start_element: + self._write('/>') + self._pending_start_element = False + else: + self._write('' % 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('' % self._qname(name)) + if self._pending_start_element: + self._write('/>') + self._pending_start_element = False + else: + self._write('' % 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('' % (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.