Skip to content

Commit 0b69d6f

Browse files
gh-57587: Add the namespaces parameter to the ElementTree serializer
tostring(), tostringlist() and ElementTree.write() accept a mapping from namespace prefixes to URIs which chooses the prefixes for this serialization, instead of the global registry of register_namespace(). Only the namespaces used in the tree are declared; the empty prefix sets the default namespace; a registered prefix which the mapping reserves for another namespace is not used.
1 parent 8fd4fc7 commit 0b69d6f

5 files changed

Lines changed: 158 additions & 19 deletions

File tree

Doc/library/xml.etree.elementtree.rst

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,11 @@ Functions
742742
:exc:`ValueError` is raised if *prefix* is invalid or reserved
743743
(``ns`` followed by digits is reserved for the serializer).
744744

745+
The registry is meant for well-known prefixes of the application.
746+
To choose the prefixes for a particular serialization,
747+
use the *namespaces* parameter of :func:`tostring`, :func:`tostringlist`
748+
and :meth:`ElementTree.write` instead.
749+
745750
.. versionadded:: 3.2
746751

747752
.. versionchanged:: next
@@ -767,15 +772,17 @@ Functions
767772

768773
.. function:: tostring(element, encoding="us-ascii", method="xml", *, \
769774
xml_declaration=None, default_namespace=None, \
770-
short_empty_elements=True, standalone=None)
775+
short_empty_elements=True, standalone=None, \
776+
namespaces=None)
771777

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

781788
.. versionchanged:: 3.4
@@ -789,20 +796,22 @@ Functions
789796
specified by the user.
790797

791798
.. versionchanged:: next
792-
Added the *standalone* parameter.
799+
Added the *standalone* and *namespaces* parameters.
793800

794801

795802
.. function:: tostringlist(element, encoding="us-ascii", method="xml", *, \
796803
xml_declaration=None, default_namespace=None, \
797-
short_empty_elements=True, standalone=None)
804+
short_empty_elements=True, standalone=None, \
805+
namespaces=None)
798806

799807
Generates a string representation of an XML element, including all
800808
subelements. *element* is an :class:`Element` instance. *encoding* [1]_ is
801809
the output encoding (default is US-ASCII). Use ``encoding="unicode"`` to
802810
generate a Unicode string (otherwise, a bytestring is generated). *method*
803811
is either ``"xml"``, ``"html"`` or ``"text"`` (default is ``"xml"``).
804-
*xml_declaration*, *default_namespace*, *short_empty_elements* and
805-
*standalone* has the same meaning as in :meth:`ElementTree.write`.
812+
*xml_declaration*, *default_namespace*, *short_empty_elements*,
813+
*standalone* and *namespaces* have the same meaning as in
814+
:meth:`ElementTree.write`.
806815
Returns a list of (optionally) encoded strings containing the XML data.
807816
It does not guarantee any specific sequence,
808817
except that ``b"".join(tostringlist(element)) == tostring(element)``.
@@ -820,7 +829,7 @@ Functions
820829
specified by the user.
821830

822831
.. versionchanged:: next
823-
Added the *standalone* parameter.
832+
Added the *standalone* and *namespaces* parameters.
824833

825834

826835
.. function:: XML(text, parser=None)
@@ -1258,7 +1267,8 @@ ElementTree Objects
12581267

12591268
.. method:: write(file, encoding="us-ascii", xml_declaration=None, \
12601269
default_namespace=None, method="xml", *, \
1261-
short_empty_elements=True, standalone=None)
1270+
short_empty_elements=True, standalone=None, \
1271+
namespaces=None)
12621272
12631273
Writes the element tree to a file, as XML. *file* is a file name, or a
12641274
:term:`file object` opened for writing. *encoding* [1]_ is the output
@@ -1281,6 +1291,14 @@ ElementTree Objects
12811291
An XML declaration is written if *standalone* is not ``None``;
12821292
combining it with ``xml_declaration=False`` raises a :exc:`ValueError`.
12831293

1294+
The keyword-only *namespaces* parameter is a mapping from namespace
1295+
prefixes to URIs, which is used to choose the prefixes for this
1296+
serialization instead of the prefixes registered with
1297+
:func:`register_namespace`.
1298+
Only the namespaces used in the tree are declared.
1299+
The empty prefix sets the default namespace, like *default_namespace*.
1300+
The prefixes are validated as in :func:`register_namespace`.
1301+
12841302
The output is either a string (:class:`str`) or binary (:class:`bytes`).
12851303
This is controlled by the *encoding* argument. If *encoding* is
12861304
``"unicode"``, the output is a string; otherwise, it's binary. Note that
@@ -1296,7 +1314,7 @@ ElementTree Objects
12961314
by the user.
12971315

12981316
.. versionchanged:: next
1299-
Added the *standalone* parameter.
1317+
Added the *standalone* and *namespaces* parameters.
13001318

13011319

13021320
This is the XML file that is going to be manipulated::

Doc/whatsnew/3.16.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,6 +751,13 @@ xml
751751
now work for :class:`!DocumentFragment` nodes in :mod:`xml.dom.minidom`.
752752
(Contributed by Serhiy Storchaka in :gh:`54092`.)
753753

754+
* Add the *namespaces* parameter to :func:`~xml.etree.ElementTree.tostring`,
755+
:func:`~xml.etree.ElementTree.tostringlist` and
756+
:meth:`ElementTree.write <xml.etree.ElementTree.ElementTree.write>`,
757+
a mapping from namespace prefixes to URIs which chooses the prefixes
758+
for this serialization instead of the global registry.
759+
(Contributed by Serhiy Storchaka in :gh:`57587`.)
760+
754761
* :class:`~xml.etree.ElementTree.XMLPullParser` and
755762
:func:`~xml.etree.ElementTree.iterparse` now support the *target* parameter.
756763
The reported object is the value returned by the corresponding method of

Lib/test/test_xml_etree.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,6 +1082,84 @@ def test_tostring_default_namespace_attributes_html(self):
10821082
'<body xmlns="http://effbot.org/ns" attr="value"></body>'
10831083
)
10841084

1085+
def test_tostring_namespaces(self):
1086+
# gh-57587: the prefixes for a particular serialization
1087+
house = 'http://localhost/house'
1088+
geo = 'http://localhost/geo'
1089+
elem = ET.XML('<house:iq xmlns:house="%s"/>' % house)
1090+
self.assertEqual(serialize(elem),
1091+
'<ns0:iq xmlns:ns0="http://localhost/house" />')
1092+
self.assertEqual(serialize(elem, namespaces={'house': house}),
1093+
'<house:iq xmlns:house="http://localhost/house" />')
1094+
self.assertEqual(serialize(elem, namespaces={'home': house}),
1095+
'<home:iq xmlns:home="http://localhost/house" />')
1096+
# the empty prefix sets the default namespace
1097+
self.assertEqual(serialize(elem, namespaces={'': house}),
1098+
'<iq xmlns="http://localhost/house" />')
1099+
self.assertEqual(serialize(elem, namespaces={'': house},
1100+
default_namespace=house),
1101+
'<iq xmlns="http://localhost/house" />')
1102+
with self.assertRaisesRegex(ValueError, 'conflicting default'):
1103+
serialize(elem, namespaces={'': house}, default_namespace=geo)
1104+
# only the namespaces used in the tree are declared
1105+
self.assertEqual(serialize(elem, namespaces={'house': house, 'geo': geo}),
1106+
'<house:iq xmlns:house="http://localhost/house" />')
1107+
1108+
def test_tostring_namespaces_registry(self):
1109+
house = 'http://localhost/house'
1110+
geo = 'http://localhost/geo'
1111+
elem = ET.XML('<doc><geo:town xmlns:geo="%s">'
1112+
'<house:iq xmlns:house="%s"/></geo:town></doc>'
1113+
% (geo, house))
1114+
ET.register_namespace('geo', geo)
1115+
self.addCleanup(ET._namespace_map.pop, geo, None)
1116+
self.assertEqual(serialize(elem),
1117+
'<doc xmlns:geo="http://localhost/geo" '
1118+
'xmlns:ns1="http://localhost/house">'
1119+
'<geo:town><ns1:iq /></geo:town></doc>')
1120+
# the mapping takes precedence over the registry
1121+
self.assertEqual(serialize(elem, namespaces={'g': geo}),
1122+
'<doc xmlns:g="http://localhost/geo" '
1123+
'xmlns:ns1="http://localhost/house">'
1124+
'<g:town><ns1:iq /></g:town></doc>')
1125+
# a registered prefix is not used if the mapping reserves it
1126+
# for another namespace
1127+
self.assertEqual(serialize(elem, namespaces={'geo': house}),
1128+
'<doc xmlns:geo="http://localhost/house" '
1129+
'xmlns:ns0="http://localhost/geo">'
1130+
'<ns0:town><geo:iq /></ns0:town></doc>')
1131+
1132+
def test_tostring_namespaces_attributes(self):
1133+
house = 'http://localhost/house'
1134+
geo = 'http://localhost/geo'
1135+
elem = ET.Element('{%s}a' % house, {'{%s}k' % geo: 'v', 'x': '1'})
1136+
self.assertEqual(serialize(elem, namespaces={'': house, 'g': geo}),
1137+
'<a xmlns="http://localhost/house" '
1138+
'xmlns:g="http://localhost/geo" g:k="v" x="1" />')
1139+
# an attribute cannot use the default namespace
1140+
elem = ET.Element('{%s}a' % house, {'{%s}k' % house: 'v'})
1141+
self.assertEqual(serialize(elem, namespaces={'': house, 'h': house}),
1142+
'<a xmlns="http://localhost/house" '
1143+
'xmlns:h="http://localhost/house" h:k="v" />')
1144+
1145+
def test_tostring_namespaces_invalid(self):
1146+
elem = ET.XML('<a/>')
1147+
for namespaces in [{'ns0': 'uri'}, {'ns12': 'uri'}, {'xml': 'uri'},
1148+
{'xmlns': 'uri'}, {'a:b': 'uri'}, {'1': 'uri'},
1149+
{'a b': 'uri'}]:
1150+
with self.subTest(namespaces=namespaces):
1151+
self.assertRaises(ValueError, serialize, elem,
1152+
namespaces=namespaces)
1153+
for namespaces in [{1: 'uri'}, {'a': 1}, {b'a': 'uri'}]:
1154+
with self.subTest(namespaces=namespaces):
1155+
self.assertRaises(TypeError, serialize, elem,
1156+
namespaces=namespaces)
1157+
# the xml prefix can only be mapped to its namespace
1158+
self.assertEqual(
1159+
serialize(elem, namespaces={
1160+
'xml': 'http://www.w3.org/XML/1998/namespace'}),
1161+
'<a />')
1162+
10851163
def test_tostring_standalone(self):
10861164
elem = ET.XML('<body><tag/></body>')
10871165
self.assertEqual(

Lib/xml/etree/ElementTree.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -698,7 +698,8 @@ def write(self, file_or_filename,
698698
default_namespace=None,
699699
method=None, *,
700700
short_empty_elements=True,
701-
standalone=None):
701+
standalone=None,
702+
namespaces=None):
702703
"""Write element tree to a file as XML.
703704
704705
Arguments:
@@ -727,6 +728,11 @@ def write(self, file_or_filename,
727728
the XML declaration. If None (default), the
728729
standalone document declaration is omitted
729730
731+
*namespaces* -- a mapping from namespace prefixes to URIs which
732+
overrides the prefixes registered with
733+
register_namespace() for this serialization.
734+
The empty prefix sets the default namespace
735+
730736
"""
731737
if self._root is None:
732738
raise TypeError('ElementTree not initialized')
@@ -759,7 +765,7 @@ def write(self, file_or_filename,
759765
_serialize_text(write, self._root)
760766
else:
761767
qnames, attr_qnames, namespaces = _namespaces(
762-
self._root, default_namespace)
768+
self._root, default_namespace, namespaces)
763769
serialize = _serialize[method]
764770
serialize(write, self._root, qnames, attr_qnames, namespaces,
765771
short_empty_elements=short_empty_elements)
@@ -817,9 +823,24 @@ def _get_writer(file_or_filename, encoding):
817823
stack.callback(file.detach)
818824
yield file.write, encoding
819825

820-
def _namespaces(elem, default_namespace=None):
826+
def _namespaces(elem, default_namespace=None, prefix_map=None):
821827
# identify namespaces used in this tree
822828

829+
# maps uri:s to the prefixes preferred for this serialization
830+
preferred = {}
831+
if prefix_map is None:
832+
prefix_map = {}
833+
else:
834+
for prefix, uri in prefix_map.items():
835+
_check_prefix(prefix, uri)
836+
if not prefix:
837+
if default_namespace is None:
838+
default_namespace = uri
839+
elif default_namespace != uri:
840+
raise ValueError("conflicting default namespace")
841+
else:
842+
preferred.setdefault(uri, prefix)
843+
823844
# maps qnames to *encoded* prefix:local names
824845
qnames = {None: None}
825846
# The default namespace declaration does not apply to attribute names,
@@ -848,7 +869,12 @@ def get_prefix(uri, isattr):
848869
prefix = prefixes.get(uri)
849870
if prefix is not None:
850871
return prefix
851-
prefix = _namespace_map.get(uri)
872+
prefix = preferred.get(uri)
873+
if prefix is None:
874+
prefix = _namespace_map.get(uri)
875+
if prefix is not None and prefix in prefix_map:
876+
# the prefix is reserved for other uri in this serialization
877+
prefix = None
852878
if prefix is None or not prefix and (isattr or default_namespace):
853879
# the empty prefix is of no use for an attribute name,
854880
# and the default namespace is used for other uri
@@ -1152,7 +1178,7 @@ def _escape_attrib_html(text):
11521178

11531179
def tostring(element, encoding=None, method=None, *,
11541180
xml_declaration=None, default_namespace=None,
1155-
short_empty_elements=True, standalone=None):
1181+
short_empty_elements=True, standalone=None, namespaces=None):
11561182
"""Generate string representation of XML element.
11571183
11581184
All subelements are included. If encoding is "unicode", a string
@@ -1163,7 +1189,9 @@ def tostring(element, encoding=None, method=None, *,
11631189
can be one of "xml" (default), "html" or "text",
11641190
*default_namespace* sets the default XML namespace (for "xmlns"),
11651191
*standalone* is the value of the standalone document declaration
1166-
in the XML declaration (omitted if None).
1192+
in the XML declaration (omitted if None),
1193+
*namespaces* is a mapping from namespace prefixes to URIs which
1194+
overrides the prefixes registered with register_namespace().
11671195
11681196
Returns an (optionally) encoded string containing the XML data.
11691197
@@ -1174,7 +1202,8 @@ def tostring(element, encoding=None, method=None, *,
11741202
default_namespace=default_namespace,
11751203
method=method,
11761204
short_empty_elements=short_empty_elements,
1177-
standalone=standalone)
1205+
standalone=standalone,
1206+
namespaces=namespaces)
11781207
return stream.getvalue()
11791208

11801209
class _ListDataStream(io.BufferedIOBase):
@@ -1196,15 +1225,17 @@ def tell(self):
11961225

11971226
def tostringlist(element, encoding=None, method=None, *,
11981227
xml_declaration=None, default_namespace=None,
1199-
short_empty_elements=True, standalone=None):
1228+
short_empty_elements=True, standalone=None,
1229+
namespaces=None):
12001230
lst = []
12011231
stream = _ListDataStream(lst)
12021232
ElementTree(element).write(stream, encoding,
12031233
xml_declaration=xml_declaration,
12041234
default_namespace=default_namespace,
12051235
method=method,
12061236
short_empty_elements=short_empty_elements,
1207-
standalone=standalone)
1237+
standalone=standalone,
1238+
namespaces=namespaces)
12081239
return lst
12091240

12101241

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Add the *namespaces* parameter to :func:`~xml.etree.ElementTree.tostring`,
2+
:func:`~xml.etree.ElementTree.tostringlist` and
3+
:meth:`~xml.etree.ElementTree.ElementTree.write`: a mapping from namespace
4+
prefixes to URIs which chooses the prefixes for this serialization instead of
5+
the global registry of :func:`~xml.etree.ElementTree.register_namespace`.

0 commit comments

Comments
 (0)