Skip to content

Commit

Permalink
bpo-36227: ElementTree.tostring() default_namespace and xml_declarati…
Browse files Browse the repository at this point in the history
…on arguments (pythonGH-12225)

Add new keyword arguments "default_namespace" and "xml_declaration" to functions ET.tostring() and ET.tostringlist(), as known from ElementTree.write().
  • Loading branch information
codeape2 authored and scoder committed Apr 14, 2019
1 parent 830b43d commit ffca16e
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 9 deletions.
21 changes: 15 additions & 6 deletions Doc/library/xml.etree.elementtree.rst
Original file line number Diff line number Diff line change
Expand Up @@ -594,38 +594,47 @@ Functions


.. function:: tostring(element, encoding="us-ascii", method="xml", *, \
xml_declaration=None, default_namespace=None,
short_empty_elements=True)

Generates a string representation of an XML element, including all
subelements. *element* is an :class:`Element` instance. *encoding* [1]_ is
the output encoding (default is US-ASCII). Use ``encoding="unicode"`` to
generate a Unicode string (otherwise, a bytestring is generated). *method*
is either ``"xml"``, ``"html"`` or ``"text"`` (default is ``"xml"``).
*short_empty_elements* has the same meaning as in :meth:`ElementTree.write`.
Returns an (optionally) encoded string containing the XML data.
*xml_declaration*, *default_namespace* and *short_empty_elements* has the same
meaning as in :meth:`ElementTree.write`. Returns an (optionally) encoded string
containing the XML data.

.. versionadded:: 3.4
The *short_empty_elements* parameter.

.. versionadded:: 3.8
The *xml_declaration* and *default_namespace* parameters.


.. function:: tostringlist(element, encoding="us-ascii", method="xml", *, \
xml_declaration=None, default_namespace=None,
short_empty_elements=True)

Generates a string representation of an XML element, including all
subelements. *element* is an :class:`Element` instance. *encoding* [1]_ is
the output encoding (default is US-ASCII). Use ``encoding="unicode"`` to
generate a Unicode string (otherwise, a bytestring is generated). *method*
is either ``"xml"``, ``"html"`` or ``"text"`` (default is ``"xml"``).
*short_empty_elements* has the same meaning as in :meth:`ElementTree.write`.
Returns a list of (optionally) encoded strings containing the XML data.
It does not guarantee any specific sequence, except that
``b"".join(tostringlist(element)) == tostring(element)``.
*xml_declaration*, *default_namespace* and *short_empty_elements* has the same
meaning as in :meth:`ElementTree.write`. Returns a list of (optionally) encoded
strings containing the XML data. It does not guarantee any specific sequence,
except that ``b"".join(tostringlist(element)) == tostring(element)``.

.. versionadded:: 3.2

.. versionadded:: 3.4
The *short_empty_elements* parameter.

.. versionadded:: 3.8
The *xml_declaration* and *default_namespace* parameters.


.. function:: XML(text, parser=None)

Expand Down
123 changes: 123 additions & 0 deletions Lib/test/test_xml_etree.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import functools
import html
import io
import locale
import operator
import pickle
import sys
Expand Down Expand Up @@ -756,6 +757,128 @@ def test_writestring(self):
elem = ET.fromstring("<html><body>text</body></html>")
self.assertEqual(ET.tostring(elem), b'<html><body>text</body></html>')

def test_tostring_default_namespace(self):
elem = ET.XML('<body xmlns="http://effbot.org/ns"><tag/></body>')
self.assertEqual(
ET.tostring(elem, encoding='unicode'),
'<ns0:body xmlns:ns0="http://effbot.org/ns"><ns0:tag /></ns0:body>'
)
self.assertEqual(
ET.tostring(elem, encoding='unicode', default_namespace='http://effbot.org/ns'),
'<body xmlns="http://effbot.org/ns"><tag /></body>'
)

def test_tostring_default_namespace_different_namespace(self):
elem = ET.XML('<body xmlns="http://effbot.org/ns"><tag/></body>')
self.assertEqual(
ET.tostring(elem, encoding='unicode', default_namespace='foobar'),
'<ns1:body xmlns="foobar" xmlns:ns1="http://effbot.org/ns"><ns1:tag /></ns1:body>'
)

def test_tostring_default_namespace_original_no_namespace(self):
elem = ET.XML('<body><tag/></body>')
EXPECTED_MSG = '^cannot use non-qualified names with default_namespace option$'
with self.assertRaisesRegex(ValueError, EXPECTED_MSG):
ET.tostring(elem, encoding='unicode', default_namespace='foobar')

def test_tostring_no_xml_declaration(self):
elem = ET.XML('<body><tag/></body>')
self.assertEqual(
ET.tostring(elem, encoding='unicode'),
'<body><tag /></body>'
)

def test_tostring_xml_declaration(self):
elem = ET.XML('<body><tag/></body>')
self.assertEqual(
ET.tostring(elem, encoding='utf8', xml_declaration=True),
b"<?xml version='1.0' encoding='utf8'?>\n<body><tag /></body>"
)

def test_tostring_xml_declaration_unicode_encoding(self):
elem = ET.XML('<body><tag/></body>')
preferredencoding = locale.getpreferredencoding()
self.assertEqual(
f"<?xml version='1.0' encoding='{preferredencoding}'?>\n<body><tag /></body>",
ET.tostring(elem, encoding='unicode', xml_declaration=True)
)

def test_tostring_xml_declaration_cases(self):
elem = ET.XML('<body><tag>ø</tag></body>')
preferredencoding = locale.getpreferredencoding()
TESTCASES = [
# (expected_retval, encoding, xml_declaration)
# ... xml_declaration = None
(b'<body><tag>&#248;</tag></body>', None, None),
(b'<body><tag>\xc3\xb8</tag></body>', 'UTF-8', None),
(b'<body><tag>&#248;</tag></body>', 'US-ASCII', None),
(b"<?xml version='1.0' encoding='ISO-8859-1'?>\n"
b"<body><tag>\xf8</tag></body>", 'ISO-8859-1', None),
('<body><tag>ø</tag></body>', 'unicode', None),

# ... xml_declaration = False
(b"<body><tag>&#248;</tag></body>", None, False),
(b"<body><tag>\xc3\xb8</tag></body>", 'UTF-8', False),
(b"<body><tag>&#248;</tag></body>", 'US-ASCII', False),
(b"<body><tag>\xf8</tag></body>", 'ISO-8859-1', False),
("<body><tag>ø</tag></body>", 'unicode', False),

# ... xml_declaration = True
(b"<?xml version='1.0' encoding='us-ascii'?>\n"
b"<body><tag>&#248;</tag></body>", None, True),
(b"<?xml version='1.0' encoding='UTF-8'?>\n"
b"<body><tag>\xc3\xb8</tag></body>", 'UTF-8', True),
(b"<?xml version='1.0' encoding='US-ASCII'?>\n"
b"<body><tag>&#248;</tag></body>", 'US-ASCII', True),
(b"<?xml version='1.0' encoding='ISO-8859-1'?>\n"
b"<body><tag>\xf8</tag></body>", 'ISO-8859-1', True),
(f"<?xml version='1.0' encoding='{preferredencoding}'?>\n"
"<body><tag>ø</tag></body>", 'unicode', True),

]
for expected_retval, encoding, xml_declaration in TESTCASES:
with self.subTest(f'encoding={encoding} '
f'xml_declaration={xml_declaration}'):
self.assertEqual(
ET.tostring(
elem,
encoding=encoding,
xml_declaration=xml_declaration
),
expected_retval
)

def test_tostringlist_default_namespace(self):
elem = ET.XML('<body xmlns="http://effbot.org/ns"><tag/></body>')
self.assertEqual(
''.join(ET.tostringlist(elem, encoding='unicode')),
'<ns0:body xmlns:ns0="http://effbot.org/ns"><ns0:tag /></ns0:body>'
)
self.assertEqual(
''.join(ET.tostringlist(elem, encoding='unicode', default_namespace='http://effbot.org/ns')),
'<body xmlns="http://effbot.org/ns"><tag /></body>'
)

def test_tostringlist_xml_declaration(self):
elem = ET.XML('<body><tag/></body>')
self.assertEqual(
''.join(ET.tostringlist(elem, encoding='unicode')),
'<body><tag /></body>'
)
self.assertEqual(
b''.join(ET.tostringlist(elem, xml_declaration=True)),
b"<?xml version='1.0' encoding='us-ascii'?>\n<body><tag /></body>"
)

preferredencoding = locale.getpreferredencoding()
stringlist = ET.tostringlist(elem, encoding='unicode', xml_declaration=True)
self.assertEqual(
''.join(stringlist),
f"<?xml version='1.0' encoding='{preferredencoding}'?>\n<body><tag /></body>"
)
self.assertRegex(stringlist[0], r"^<\?xml version='1.0' encoding='.+'?>")
self.assertEqual(['<body', '>', '<tag', ' />', '</body>'], stringlist[1:])

def test_encoding(self):
def check(encoding, body=''):
xml = ("<?xml version='1.0' encoding='%s'?><xml>%s</xml>" %
Expand Down
15 changes: 12 additions & 3 deletions Lib/xml/etree/ElementTree.py
Original file line number Diff line number Diff line change
Expand Up @@ -1113,6 +1113,7 @@ def _escape_attrib_html(text):
# --------------------------------------------------------------------

def tostring(element, encoding=None, method=None, *,
xml_declaration=None, default_namespace=None,
short_empty_elements=True):
"""Generate string representation of XML element.
Expand All @@ -1121,13 +1122,17 @@ def tostring(element, encoding=None, method=None, *,
*element* is an Element instance, *encoding* is an optional output
encoding defaulting to US-ASCII, *method* is an optional output which can
be one of "xml" (default), "html", "text" or "c14n".
be one of "xml" (default), "html", "text" or "c14n", *default_namespace*
sets the default XML namespace (for "xmlns").
Returns an (optionally) encoded string containing the XML data.
"""
stream = io.StringIO() if encoding == 'unicode' else io.BytesIO()
ElementTree(element).write(stream, encoding, method=method,
ElementTree(element).write(stream, encoding,
xml_declaration=xml_declaration,
default_namespace=default_namespace,
method=method,
short_empty_elements=short_empty_elements)
return stream.getvalue()

Expand All @@ -1149,10 +1154,14 @@ def tell(self):
return len(self.lst)

def tostringlist(element, encoding=None, method=None, *,
xml_declaration=None, default_namespace=None,
short_empty_elements=True):
lst = []
stream = _ListDataStream(lst)
ElementTree(element).write(stream, encoding, method=method,
ElementTree(element).write(stream, encoding,
xml_declaration=xml_declaration,
default_namespace=default_namespace,
method=method,
short_empty_elements=short_empty_elements)
return lst

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added support for keyword arguments `default_namespace` and `xml_declaration` in functions
ElementTree.tostring() and ElementTree.tostringlist().

0 comments on commit ffca16e

Please sign in to comment.