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

Skip to content

Commit a90032a

Browse files
committed
#1343: Add short_empty_elements option to XMLGenerator.
Patch and tests by Neil Muller.
1 parent 961aaf5 commit a90032a

4 files changed

Lines changed: 135 additions & 8 deletions

File tree

Doc/library/xml.sax.utils.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,19 @@ or as base classes.
5050
using the reference concrete syntax.
5151

5252

53-
.. class:: XMLGenerator(out=None, encoding='iso-8859-1')
53+
.. class:: XMLGenerator(out=None, encoding='iso-8859-1', short_empty_elements=False)
5454

5555
This class implements the :class:`ContentHandler` interface by writing SAX
5656
events back into an XML document. In other words, using an :class:`XMLGenerator`
5757
as the content handler will reproduce the original document being parsed. *out*
5858
should be a file-like object which will default to *sys.stdout*. *encoding* is
5959
the encoding of the output stream which defaults to ``'iso-8859-1'``.
60+
*short_empty_elements* controls the formatting of elements that contain no
61+
content: if *False* (the default) they are emitted as a pair of start/end
62+
tags, if set to *True* they are emitted as a single self-closed tag.
63+
64+
.. versionadded:: 3.2
65+
short_empty_elements
6066

6167

6268
.. class:: XMLFilterBase(base)

Lib/test/test_sax.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,16 @@ def test_xmlgen_basic(self):
170170

171171
self.assertEquals(result.getvalue(), start + "<doc></doc>")
172172

173+
def test_xmlgen_basic_empty(self):
174+
result = StringIO()
175+
gen = XMLGenerator(result, short_empty_elements=True)
176+
gen.startDocument()
177+
gen.startElement("doc", {})
178+
gen.endElement("doc")
179+
gen.endDocument()
180+
181+
self.assertEquals(result.getvalue(), start + "<doc/>")
182+
173183
def test_xmlgen_content(self):
174184
result = StringIO()
175185
gen = XMLGenerator(result)
@@ -182,6 +192,18 @@ def test_xmlgen_content(self):
182192

183193
self.assertEquals(result.getvalue(), start + "<doc>huhei</doc>")
184194

195+
def test_xmlgen_content_empty(self):
196+
result = StringIO()
197+
gen = XMLGenerator(result, short_empty_elements=True)
198+
199+
gen.startDocument()
200+
gen.startElement("doc", {})
201+
gen.characters("huhei")
202+
gen.endElement("doc")
203+
gen.endDocument()
204+
205+
self.assertEquals(result.getvalue(), start + "<doc>huhei</doc>")
206+
185207
def test_xmlgen_pi(self):
186208
result = StringIO()
187209
gen = XMLGenerator(result)
@@ -239,6 +261,18 @@ def test_xmlgen_ignorable(self):
239261

240262
self.assertEquals(result.getvalue(), start + "<doc> </doc>")
241263

264+
def test_xmlgen_ignorable_empty(self):
265+
result = StringIO()
266+
gen = XMLGenerator(result, short_empty_elements=True)
267+
268+
gen.startDocument()
269+
gen.startElement("doc", {})
270+
gen.ignorableWhitespace(" ")
271+
gen.endElement("doc")
272+
gen.endDocument()
273+
274+
self.assertEquals(result.getvalue(), start + "<doc> </doc>")
275+
242276
def test_xmlgen_ns(self):
243277
result = StringIO()
244278
gen = XMLGenerator(result)
@@ -257,6 +291,24 @@ def test_xmlgen_ns(self):
257291
('<ns1:doc xmlns:ns1="%s"><udoc></udoc></ns1:doc>' %
258292
ns_uri))
259293

294+
def test_xmlgen_ns_empty(self):
295+
result = StringIO()
296+
gen = XMLGenerator(result, short_empty_elements=True)
297+
298+
gen.startDocument()
299+
gen.startPrefixMapping("ns1", ns_uri)
300+
gen.startElementNS((ns_uri, "doc"), "ns1:doc", {})
301+
# add an unqualified name
302+
gen.startElementNS((None, "udoc"), None, {})
303+
gen.endElementNS((None, "udoc"), None)
304+
gen.endElementNS((ns_uri, "doc"), "ns1:doc")
305+
gen.endPrefixMapping("ns1")
306+
gen.endDocument()
307+
308+
self.assertEquals(result.getvalue(), start + \
309+
('<ns1:doc xmlns:ns1="%s"><udoc/></ns1:doc>' %
310+
ns_uri))
311+
260312
def test_1463026_1(self):
261313
result = StringIO()
262314
gen = XMLGenerator(result)
@@ -268,6 +320,17 @@ def test_1463026_1(self):
268320

269321
self.assertEquals(result.getvalue(), start+'<a b="c"></a>')
270322

323+
def test_1463026_1_empty(self):
324+
result = StringIO()
325+
gen = XMLGenerator(result, short_empty_elements=True)
326+
327+
gen.startDocument()
328+
gen.startElementNS((None, 'a'), 'a', {(None, 'b'):'c'})
329+
gen.endElementNS((None, 'a'), 'a')
330+
gen.endDocument()
331+
332+
self.assertEquals(result.getvalue(), start+'<a b="c"/>')
333+
271334
def test_1463026_2(self):
272335
result = StringIO()
273336
gen = XMLGenerator(result)
@@ -281,6 +344,19 @@ def test_1463026_2(self):
281344

282345
self.assertEquals(result.getvalue(), start+'<a xmlns="qux"></a>')
283346

347+
def test_1463026_2_empty(self):
348+
result = StringIO()
349+
gen = XMLGenerator(result, short_empty_elements=True)
350+
351+
gen.startDocument()
352+
gen.startPrefixMapping(None, 'qux')
353+
gen.startElementNS(('qux', 'a'), 'a', {})
354+
gen.endElementNS(('qux', 'a'), 'a')
355+
gen.endPrefixMapping(None)
356+
gen.endDocument()
357+
358+
self.assertEquals(result.getvalue(), start+'<a xmlns="qux"/>')
359+
284360
def test_1463026_3(self):
285361
result = StringIO()
286362
gen = XMLGenerator(result)
@@ -295,6 +371,20 @@ def test_1463026_3(self):
295371
self.assertEquals(result.getvalue(),
296372
start+'<my:a xmlns:my="qux" b="c"></my:a>')
297373

374+
def test_1463026_3_empty(self):
375+
result = StringIO()
376+
gen = XMLGenerator(result, short_empty_elements=True)
377+
378+
gen.startDocument()
379+
gen.startPrefixMapping('my', 'qux')
380+
gen.startElementNS(('qux', 'a'), 'a', {(None, 'b'):'c'})
381+
gen.endElementNS(('qux', 'a'), 'a')
382+
gen.endPrefixMapping('my')
383+
gen.endDocument()
384+
385+
self.assertEquals(result.getvalue(),
386+
start+'<my:a xmlns:my="qux" b="c"/>')
387+
298388

299389
class XMLFilterBaseTest(unittest.TestCase):
300390
def test_filter_basic(self):

Lib/xml/sax/saxutils.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def quoteattr(data, entities={}):
7878

7979
class XMLGenerator(handler.ContentHandler):
8080

81-
def __init__(self, out=None, encoding="iso-8859-1"):
81+
def __init__(self, out=None, encoding="iso-8859-1", short_empty_elements=False):
8282
if out is None:
8383
import sys
8484
out = sys.stdout
@@ -88,6 +88,8 @@ def __init__(self, out=None, encoding="iso-8859-1"):
8888
self._current_context = self._ns_contexts[-1]
8989
self._undeclared_ns_maps = []
9090
self._encoding = encoding
91+
self._short_empty_elements = short_empty_elements
92+
self._pending_start_element = False
9193

9294
def _write(self, text):
9395
if isinstance(text, str):
@@ -106,6 +108,11 @@ def _qname(self, name):
106108
# Return the unqualified name
107109
return name[1]
108110

111+
def _finish_pending_start_element(self,endElement=False):
112+
if self._pending_start_element:
113+
self._write('>')
114+
self._pending_start_element = False
115+
109116
# ContentHandler methods
110117

111118
def startDocument(self):
@@ -122,15 +129,24 @@ def endPrefixMapping(self, prefix):
122129
del self._ns_contexts[-1]
123130

124131
def startElement(self, name, attrs):
132+
self._finish_pending_start_element()
125133
self._write('<' + name)
126134
for (name, value) in attrs.items():
127135
self._write(' %s=%s' % (name, quoteattr(value)))
128-
self._write('>')
136+
if self._short_empty_elements:
137+
self._pending_start_element = True
138+
else:
139+
self._write(">")
129140

130141
def endElement(self, name):
131-
self._write('</%s>' % name)
142+
if self._pending_start_element:
143+
self._write('/>')
144+
self._pending_start_element = False
145+
else:
146+
self._write('</%s>' % name)
132147

133148
def startElementNS(self, name, qname, attrs):
149+
self._finish_pending_start_element()
134150
self._write('<' + self._qname(name))
135151

136152
for prefix, uri in self._undeclared_ns_maps:
@@ -142,18 +158,30 @@ def startElementNS(self, name, qname, attrs):
142158

143159
for (name, value) in attrs.items():
144160
self._write(' %s=%s' % (self._qname(name), quoteattr(value)))
145-
self._write('>')
161+
if self._short_empty_elements:
162+
self._pending_start_element = True
163+
else:
164+
self._write(">")
146165

147166
def endElementNS(self, name, qname):
148-
self._write('</%s>' % self._qname(name))
167+
if self._pending_start_element:
168+
self._write('/>')
169+
self._pending_start_element = False
170+
else:
171+
self._write('</%s>' % self._qname(name))
149172

150173
def characters(self, content):
151-
self._write(escape(content))
174+
if content:
175+
self._finish_pending_start_element()
176+
self._write(escape(content))
152177

153178
def ignorableWhitespace(self, content):
154-
self._write(content)
179+
if content:
180+
self._finish_pending_start_element()
181+
self._write(content)
155182

156183
def processingInstruction(self, target, data):
184+
self._finish_pending_start_element()
157185
self._write('<?%s %s?>' % (target, data))
158186

159187

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ Core and Builtins
3434
Library
3535
-------
3636

37+
- Issue #1343: xml.sax.saxutils.XMLGenerator now has an option
38+
short_empty_elements to direct it to use self-closing tags when appropriate.
39+
3740
- Issue #9807 (part 1): Expose the ABI flags in sys.abiflags. Add --abiflags
3841
switch to python-config for command line access.
3942

0 commit comments

Comments
 (0)