1# Code modified from cPython's Lib/xml/etree/ElementTree.py
2# The write() code is modified to allow specifying a particular namespace
3# uri -> prefix mapping.
4#
5# ---------------------------------------------------------------------
6# Licensed to PSF under a Contributor Agreement.
7# See https://www.python.org/psf/license for licensing details.
8#
9# ElementTree
10# Copyright (c) 1999-2008 by Fredrik Lundh. All rights reserved.
11#
12# fredrik@pythonware.com
13# http://www.pythonware.com
14# --------------------------------------------------------------------
15# The ElementTree toolkit is
16#
17# Copyright (c) 1999-2008 by Fredrik Lundh
18#
19# By obtaining, using, and/or copying this software and/or its
20# associated documentation, you agree that you have read, understood,
21# and will comply with the following terms and conditions:
22#
23# Permission to use, copy, modify, and distribute this software and
24# its associated documentation for any purpose and without fee is
25# hereby granted, provided that the above copyright notice appears in
26# all copies, and that both that copyright notice and this permission
27# notice appear in supporting documentation, and that the name of
28# Secret Labs AB or the author not be used in advertising or publicity
29# pertaining to distribution of the software without specific, written
30# prior permission.
31#
32# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
33# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
34# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
35# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
36# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
37# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
38# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
39# OF THIS SOFTWARE.
40# --------------------------------------------------------------------
41import contextlib
42import io
43
44import xml.etree.ElementTree as ET
45
46
47def current_global_nsmap():
48 return {
49 prefix: uri for uri, prefix in ET._namespace_map.items()
50 }
51
52
53class IncrementalTree(ET.ElementTree):
54
55 def write(
56 self,
57 file_or_filename,
58 encoding=None,
59 xml_declaration=None,
60 default_namespace=None,
61 method=None,
62 *,
63 short_empty_elements=True,
64 nsmap=None,
65 root_ns_only=False,
66 minimal_ns_only=False,
67 ):
68 """Write element tree to a file as XML.
69
70 Arguments:
71 *file_or_filename* -- file name or a file object opened for writing
72
73 *encoding* -- the output encoding (default: US-ASCII)
74
75 *xml_declaration* -- bool indicating if an XML declaration should be
76 added to the output. If None, an XML declaration
77 is added if encoding IS NOT either of:
78 US-ASCII, UTF-8, or Unicode
79
80 *default_namespace* -- sets the default XML namespace (for "xmlns").
81 Takes precedence over any default namespace
82 provided in nsmap or
83 xml.etree.ElementTree.register_namespace().
84
85 *method* -- either "xml" (default), "html, "text", or "c14n"
86
87 *short_empty_elements* -- controls the formatting of elements
88 that contain no content. If True (default)
89 they are emitted as a single self-closed
90 tag, otherwise they are emitted as a pair
91 of start/end tags
92
93 *nsmap* -- a mapping of namespace prefixes to URIs. These take
94 precedence over any mappings registered using
95 xml.etree.ElementTree.register_namespace(). The
96 default_namespace argument, if supplied, takes precedence
97 over any default namespace supplied in nsmap. All supplied
98 namespaces will be declared on the root element, even if
99 unused in the document.
100
101 *root_ns_only* -- bool indicating namespace declrations should only
102 be written on the root element. This requires two
103 passes of the xml tree adding additional time to
104 the writing process. This is primarily meant to
105 mimic xml.etree.ElementTree's behaviour.
106
107 *minimal_ns_only* -- bool indicating only namespaces that were used
108 to qualify elements or attributes should be
109 declared. All namespace declarations will be
110 written on the root element regardless of the
111 value of the root_ns_only arg. Requires two
112 passes of the xml tree adding additional time to
113 the writing process.
114
115 """
116 if not method:
117 method = "xml"
118 elif method not in ("text", "xml", "html"):
119 raise ValueError("unknown method %r" % method)
120 if not encoding:
121 encoding = "us-ascii"
122
123 with _get_writer(file_or_filename, encoding) as (write, declared_encoding):
124 if method == "xml" and (
125 xml_declaration
126 or (
127 xml_declaration is None
128 and encoding.lower() != "unicode"
129 and declared_encoding.lower() not in ("utf-8", "us-ascii")
130 )
131 ):
132 write("<?xml version='1.0' encoding='%s'?>\n" % (declared_encoding,))
133 if method == "text":
134 ET._serialize_text(write, self._root)
135 else:
136 if method == "xml":
137 is_html = False
138 else:
139 is_html = True
140 if nsmap:
141 if None in nsmap:
142 raise ValueError(
143 'Found None as default nsmap prefix in nsmap. '
144 'Use "" as the default namespace prefix.'
145 )
146 new_nsmap = nsmap.copy()
147 else:
148 new_nsmap = {}
149 if default_namespace:
150 new_nsmap[""] = default_namespace
151 if root_ns_only or minimal_ns_only:
152 # _namespaces returns a mapping of only the namespaces that
153 # were used.
154 new_nsmap = _namespaces(
155 self._root,
156 default_namespace,
157 new_nsmap,
158 )
159 if not minimal_ns_only:
160 if nsmap:
161 # We want all namespaces defined in the provided
162 # nsmap to be declared regardless of whether
163 # they've been used.
164 new_nsmap.update(nsmap)
165 if default_namespace:
166 new_nsmap[""] = default_namespace
167 global_nsmap = {
168 prefix: uri for uri, prefix in ET._namespace_map.items()
169 }
170 if None in global_nsmap:
171 raise ValueError(
172 'Found None as default nsmap prefix in nsmap registered with '
173 'register_namespace. Use "" for the default namespace prefix.'
174 )
175 nsmap_scope = {}
176 _serialize_ns_xml(
177 write,
178 self._root,
179 nsmap_scope,
180 global_nsmap,
181 is_html=is_html,
182 is_root=True,
183 short_empty_elements=short_empty_elements,
184 new_nsmap=new_nsmap,
185 )
186
187
188def _make_new_ns_prefix(
189 nsmap_scope,
190 global_prefixes,
191 local_nsmap=None,
192 default_namespace=None,
193):
194 i = len(nsmap_scope)
195 if default_namespace is not None and "" not in nsmap_scope:
196 # Keep the same numbering scheme as python which assumes the default
197 # namespace is present if supplied.
198 i += 1
199
200 while True:
201 prefix = f"ns{i}"
202 if (
203 prefix not in nsmap_scope
204 and prefix not in global_prefixes
205 and (
206 not local_nsmap or prefix not in local_nsmap
207 )
208 ):
209 return prefix
210 i += 1
211
212
213def _get_or_create_prefix(
214 uri,
215 nsmap_scope,
216 global_nsmap,
217 new_namespace_prefixes,
218 uri_to_prefix,
219 for_default_namespace_attr_prefix=False,
220):
221 """Find a prefix that doesn't conflict with the ns scope or create a new prefix
222
223 This function mutates nsmap_scope, global_nsmap, new_namespace_prefixes and
224 uri_to_prefix. It is intended to keep state in _serialize_ns_xml consistent
225 while deduplicating the house keeping code or updating these dictionaries.
226 """
227 # Check if we can reuse an existing (global) prefix within the current
228 # namespace scope. There maybe many prefixes pointing to a single URI by
229 # this point and we need to select a prefix that is not in use in the
230 # current scope.
231 for global_prefix, global_uri in global_nsmap.items():
232 if uri == global_uri and global_prefix not in nsmap_scope:
233 prefix = global_prefix
234 break
235 else: # no break
236 # We couldn't find a suitable existing prefix for this namespace scope,
237 # let's create a new one.
238 prefix = _make_new_ns_prefix(nsmap_scope, global_prefixes=global_nsmap)
239 global_nsmap[prefix] = uri
240 nsmap_scope[prefix] = uri
241 if not for_default_namespace_attr_prefix:
242 # Don't override the actual default namespace prefix
243 uri_to_prefix[uri] = prefix
244 if prefix != "xml":
245 new_namespace_prefixes.add(prefix)
246 return prefix
247
248
249def _find_default_namespace_attr_prefix(
250 default_namespace,
251 nsmap,
252 local_nsmap,
253 global_prefixes,
254 provided_default_namespace=None,
255):
256 # Search the provided nsmap for any prefixes for this uri that aren't the
257 # default namespace ""
258 for prefix, uri in nsmap.items():
259 if uri == default_namespace and prefix != "":
260 return prefix
261
262 for prefix, uri in local_nsmap.items():
263 if uri == default_namespace and prefix != "":
264 return prefix
265
266 # _namespace_map is a 1:1 mapping of uri -> prefix
267 prefix = ET._namespace_map.get(default_namespace)
268 if prefix and prefix not in nsmap:
269 return prefix
270
271 return _make_new_ns_prefix(
272 nsmap,
273 global_prefixes,
274 local_nsmap,
275 provided_default_namespace,
276 )
277
278
279def process_attribs(
280 elem,
281 is_nsmap_scope_changed,
282 default_ns_attr_prefix,
283 nsmap_scope,
284 global_nsmap,
285 new_namespace_prefixes,
286 uri_to_prefix,
287):
288 item_parts = []
289 for k, v in elem.items():
290 if isinstance(k, ET.QName):
291 k = k.text
292 try:
293 if k[:1] == "{":
294 uri_and_name = k[1:].rsplit("}", 1)
295 try:
296 prefix = uri_to_prefix[uri_and_name[0]]
297 except KeyError:
298 if not is_nsmap_scope_changed:
299 # We're about to mutate the these dicts so
300 # let's copy them first. We don't have to
301 # recompute other mappings as we're looking up
302 # or creating a new prefix
303 nsmap_scope = nsmap_scope.copy()
304 uri_to_prefix = uri_to_prefix.copy()
305 is_nsmap_scope_changed = True
306 prefix = _get_or_create_prefix(
307 uri_and_name[0],
308 nsmap_scope,
309 global_nsmap,
310 new_namespace_prefixes,
311 uri_to_prefix,
312 )
313
314 if not prefix:
315 if default_ns_attr_prefix:
316 prefix = default_ns_attr_prefix
317 else:
318 for prefix, known_uri in nsmap_scope.items():
319 if known_uri == uri_and_name[0] and prefix != "":
320 default_ns_attr_prefix = prefix
321 break
322 else: # no break
323 if not is_nsmap_scope_changed:
324 # We're about to mutate the these dicts so
325 # let's copy them first. We don't have to
326 # recompute other mappings as we're looking up
327 # or creating a new prefix
328 nsmap_scope = nsmap_scope.copy()
329 uri_to_prefix = uri_to_prefix.copy()
330 is_nsmap_scope_changed = True
331 prefix = _get_or_create_prefix(
332 uri_and_name[0],
333 nsmap_scope,
334 global_nsmap,
335 new_namespace_prefixes,
336 uri_to_prefix,
337 for_default_namespace_attr_prefix=True,
338 )
339 default_ns_attr_prefix = prefix
340 k = f"{prefix}:{uri_and_name[1]}"
341 except TypeError:
342 ET._raise_serialization_error(k)
343
344 if isinstance(v, ET.QName):
345 if v.text[:1] != "{":
346 v = v.text
347 else:
348 uri_and_name = v.text[1:].rsplit("}", 1)
349 try:
350 prefix = uri_to_prefix[uri_and_name[0]]
351 except KeyError:
352 if not is_nsmap_scope_changed:
353 # We're about to mutate the these dicts so
354 # let's copy them first. We don't have to
355 # recompute other mappings as we're looking up
356 # or creating a new prefix
357 nsmap_scope = nsmap_scope.copy()
358 uri_to_prefix = uri_to_prefix.copy()
359 is_nsmap_scope_changed = True
360 prefix = _get_or_create_prefix(
361 uri_and_name[0],
362 nsmap_scope,
363 global_nsmap,
364 new_namespace_prefixes,
365 uri_to_prefix,
366 )
367 v = f"{prefix}:{uri_and_name[1]}"
368 item_parts.append((k, v))
369 return item_parts, default_ns_attr_prefix, nsmap_scope
370
371
372def write_elem_start(
373 write,
374 elem,
375 nsmap_scope,
376 global_nsmap,
377 short_empty_elements,
378 is_html,
379 is_root=False,
380 uri_to_prefix=None,
381 default_ns_attr_prefix=None,
382 new_nsmap=None,
383 **kwargs,
384):
385 """Write the opening tag (including self closing) and element text.
386
387 Refer to _serialize_ns_xml for description of arguments.
388
389 nsmap_scope should be an empty dictionary on first call. All nsmap prefixes
390 must be strings with the default namespace prefix represented by "".
391
392 eg.
393 - <foo attr1="one"> (returns tag = 'foo')
394 - <foo attr1="one">text (returns tag = 'foo')
395 - <foo attr1="one" /> (returns tag = None)
396
397 Returns:
398 tag:
399 The tag name to be closed or None if no closing required.
400 nsmap_scope:
401 The current nsmap after any prefix to uri additions from this
402 element. This is the input dict if unmodified or an updated copy.
403 default_ns_attr_prefix:
404 The prefix for the default namespace to use with attrs.
405 uri_to_prefix:
406 The current uri to prefix map after any uri to prefix additions
407 from this element. This is the input dict if unmodified or an
408 updated copy.
409 next_remains_root:
410 A bool indicating if the child element(s) should be treated as
411 their own roots.
412 """
413 tag = elem.tag
414 text = elem.text
415
416 if tag is ET.Comment:
417 write("<!--%s-->" % text)
418 tag = None
419 next_remains_root = False
420 elif tag is ET.ProcessingInstruction:
421 write("<?%s?>" % text)
422 tag = None
423 next_remains_root = False
424 else:
425 if new_nsmap:
426 is_nsmap_scope_changed = True
427 nsmap_scope = nsmap_scope.copy()
428 nsmap_scope.update(new_nsmap)
429 new_namespace_prefixes = set(new_nsmap.keys())
430 new_namespace_prefixes.discard("xml")
431 # We need to recompute the uri to prefixes
432 uri_to_prefix = None
433 default_ns_attr_prefix = None
434 else:
435 is_nsmap_scope_changed = False
436 new_namespace_prefixes = set()
437
438 if uri_to_prefix is None:
439 if None in nsmap_scope:
440 raise ValueError(
441 'Found None as a namespace prefix. Use "" as the default namespace prefix.'
442 )
443 uri_to_prefix = {uri: prefix for prefix, uri in nsmap_scope.items()}
444 if "" in nsmap_scope:
445 # There may be multiple prefixes for the default namespace but
446 # we want to make sure we preferentially use "" (for elements)
447 uri_to_prefix[nsmap_scope[""]] = ""
448
449 if tag is None:
450 # tag supression where tag is set to None
451 # Don't change is_root so namespaces can be passed down
452 next_remains_root = is_root
453 if text:
454 write(ET._escape_cdata(text))
455 else:
456 next_remains_root = False
457 if isinstance(tag, ET.QName):
458 tag = tag.text
459 try:
460 # These splits / fully qualified tag creationg are the
461 # bottleneck in this implementation vs the python
462 # implementation.
463 # The following split takes ~42ns with no uri and ~85ns if a
464 # prefix is present. If the uri was present, we then need to
465 # look up a prefix (~14ns) and create the fully qualified
466 # string (~41ns). This gives a total of ~140ns where a uri is
467 # present.
468 # Python's implementation needs to preprocess the tree to
469 # create a dict of qname -> tag by traversing the tree which
470 # takes a bit of extra time but it quickly makes that back by
471 # only having to do a dictionary look up (~14ns) for each tag /
472 # attrname vs our splitting (~140ns).
473 # So here we have the flexibility of being able to redefine the
474 # uri a prefix points to midway through serialisation at the
475 # expense of performance (~10% slower for a 1mb file on my
476 # machine).
477 if tag[:1] == "{":
478 uri_and_name = tag[1:].rsplit("}", 1)
479 try:
480 prefix = uri_to_prefix[uri_and_name[0]]
481 except KeyError:
482 if not is_nsmap_scope_changed:
483 # We're about to mutate the these dicts so let's
484 # copy them first. We don't have to recompute other
485 # mappings as we're looking up or creating a new
486 # prefix
487 nsmap_scope = nsmap_scope.copy()
488 uri_to_prefix = uri_to_prefix.copy()
489 is_nsmap_scope_changed = True
490 prefix = _get_or_create_prefix(
491 uri_and_name[0],
492 nsmap_scope,
493 global_nsmap,
494 new_namespace_prefixes,
495 uri_to_prefix,
496 )
497 if prefix:
498 tag = f"{prefix}:{uri_and_name[1]}"
499 else:
500 tag = uri_and_name[1]
501 elif "" in nsmap_scope:
502 raise ValueError(
503 "cannot use non-qualified names with default_namespace option"
504 )
505 except TypeError:
506 ET._raise_serialization_error(tag)
507
508 write("<" + tag)
509
510 if elem.attrib:
511 item_parts, default_ns_attr_prefix, nsmap_scope = process_attribs(
512 elem,
513 is_nsmap_scope_changed,
514 default_ns_attr_prefix,
515 nsmap_scope,
516 global_nsmap,
517 new_namespace_prefixes,
518 uri_to_prefix,
519 )
520 else:
521 item_parts = []
522 if new_namespace_prefixes:
523 ns_attrs = []
524 for k in sorted(new_namespace_prefixes):
525 v = nsmap_scope[k]
526 if k:
527 k = "xmlns:" + k
528 else:
529 k = "xmlns"
530 ns_attrs.append((k, v))
531 if is_html:
532 write("".join([f' {k}="{ET._escape_attrib_html(v)}"' for k, v in ns_attrs]))
533 else:
534 write("".join([f' {k}="{ET._escape_attrib(v)}"' for k, v in ns_attrs]))
535 if item_parts:
536 if is_html:
537 write("".join([f' {k}="{ET._escape_attrib_html(v)}"' for k, v in item_parts]))
538 else:
539 write("".join([f' {k}="{ET._escape_attrib(v)}"' for k, v in item_parts]))
540 if is_html:
541 write(">")
542 ltag = tag.lower()
543 if text:
544 if ltag == "script" or ltag == "style":
545 write(text)
546 else:
547 write(ET._escape_cdata(text))
548 if ltag in ET.HTML_EMPTY:
549 tag = None
550 elif text or len(elem) or not short_empty_elements:
551 write(">")
552 if text:
553 write(ET._escape_cdata(text))
554 else:
555 tag = None
556 write(" />")
557 return (
558 tag,
559 nsmap_scope,
560 default_ns_attr_prefix,
561 uri_to_prefix,
562 next_remains_root,
563 )
564
565
566def _serialize_ns_xml(
567 write,
568 elem,
569 nsmap_scope,
570 global_nsmap,
571 short_empty_elements,
572 is_html,
573 is_root=False,
574 uri_to_prefix=None,
575 default_ns_attr_prefix=None,
576 new_nsmap=None,
577 **kwargs,
578):
579 """Serialize an element or tree using 'write' for output.
580
581 Args:
582 write:
583 A function to write the xml to its destination.
584 elem:
585 The element to serialize.
586 nsmap_scope:
587 The current prefix to uri mapping for this element. This should be
588 an empty dictionary for the root element. Additional namespaces are
589 progressively added using the new_nsmap arg.
590 global_nsmap:
591 A dict copy of the globally registered _namespace_map in uri to
592 prefix form
593 short_empty_elements:
594 Controls the formatting of elements that contain no content. If True
595 (default) they are emitted as a single self-closed tag, otherwise
596 they are emitted as a pair of start/end tags.
597 is_html:
598 Set to True to serialize as HTML otherwise XML.
599 is_root:
600 Boolean indicating if this is a root element.
601 uri_to_prefix:
602 Current state of the mapping of uri to prefix.
603 default_ns_attr_prefix:
604 new_nsmap:
605 New prefix -> uri mapping to be applied to this element.
606 """
607 (
608 tag,
609 nsmap_scope,
610 default_ns_attr_prefix,
611 uri_to_prefix,
612 next_remains_root,
613 ) = write_elem_start(
614 write,
615 elem,
616 nsmap_scope,
617 global_nsmap,
618 short_empty_elements,
619 is_html,
620 is_root,
621 uri_to_prefix,
622 default_ns_attr_prefix,
623 new_nsmap=new_nsmap,
624 )
625 for e in elem:
626 _serialize_ns_xml(
627 write,
628 e,
629 nsmap_scope,
630 global_nsmap,
631 short_empty_elements,
632 is_html,
633 next_remains_root,
634 uri_to_prefix,
635 default_ns_attr_prefix,
636 new_nsmap=None,
637 )
638 if tag:
639 write(f"</{tag}>")
640 if elem.tail:
641 write(ET._escape_cdata(elem.tail))
642
643
644def _qnames_iter(elem):
645 """Iterate through all the qualified names in elem"""
646 seen_el_qnames = set()
647 seen_other_qnames = set()
648 for this_elem in elem.iter():
649 tag = this_elem.tag
650 if isinstance(tag, str):
651 if tag not in seen_el_qnames:
652 seen_el_qnames.add(tag)
653 yield tag, True
654 elif isinstance(tag, ET.QName):
655 tag = tag.text
656 if tag not in seen_el_qnames:
657 seen_el_qnames.add(tag)
658 yield tag, True
659 elif (
660 tag is not None
661 and tag is not ET.ProcessingInstruction
662 and tag is not ET.Comment
663 ):
664 ET._raise_serialization_error(tag)
665
666 for key, value in this_elem.items():
667 if isinstance(key, ET.QName):
668 key = key.text
669 if key not in seen_other_qnames:
670 seen_other_qnames.add(key)
671 yield key, False
672
673 if isinstance(value, ET.QName):
674 if value.text not in seen_other_qnames:
675 seen_other_qnames.add(value.text)
676 yield value.text, False
677
678 text = this_elem.text
679 if isinstance(text, ET.QName):
680 if text.text not in seen_other_qnames:
681 seen_other_qnames.add(text.text)
682 yield text.text, False
683
684
685def _namespaces(
686 elem,
687 default_namespace=None,
688 nsmap=None,
689):
690 """Find all namespaces used in the document and return a prefix to uri map"""
691 if nsmap is None:
692 nsmap = {}
693
694 out_nsmap = {}
695
696 seen_uri_to_prefix = {}
697 # Multiple prefixes may be present for a single uri. This will select the
698 # last prefix found in nsmap for a given uri.
699 local_prefix_map = {uri: prefix for prefix, uri in nsmap.items()}
700 if default_namespace is not None:
701 local_prefix_map[default_namespace] = ""
702 elif "" in nsmap:
703 # but we make sure the default prefix always take precedence
704 local_prefix_map[nsmap[""]] = ""
705
706 global_prefixes = set(ET._namespace_map.values())
707 has_unqual_el = False
708 default_namespace_attr_prefix = None
709 for qname, is_el in _qnames_iter(elem):
710 try:
711 if qname[:1] == "{":
712 uri_and_name = qname[1:].rsplit("}", 1)
713
714 prefix = seen_uri_to_prefix.get(uri_and_name[0])
715 if prefix is None:
716 prefix = local_prefix_map.get(uri_and_name[0])
717 if prefix is None or prefix in out_nsmap:
718 prefix = ET._namespace_map.get(uri_and_name[0])
719 if prefix is None or prefix in out_nsmap:
720 prefix = _make_new_ns_prefix(
721 out_nsmap,
722 global_prefixes,
723 nsmap,
724 default_namespace,
725 )
726 if prefix or is_el:
727 out_nsmap[prefix] = uri_and_name[0]
728 seen_uri_to_prefix[uri_and_name[0]] = prefix
729
730 if not is_el and not prefix and not default_namespace_attr_prefix:
731 # Find the alternative prefix to use with non-element
732 # names
733 default_namespace_attr_prefix = _find_default_namespace_attr_prefix(
734 uri_and_name[0],
735 out_nsmap,
736 nsmap,
737 global_prefixes,
738 default_namespace,
739 )
740 out_nsmap[default_namespace_attr_prefix] = uri_and_name[0]
741 # Don't add this uri to prefix mapping as it might override
742 # the uri -> "" default mapping. We'll fix this up at the
743 # end of the fn.
744 # local_prefix_map[uri_and_name[0]] = default_namespace_attr_prefix
745 else:
746 if is_el:
747 has_unqual_el = True
748 except TypeError:
749 ET._raise_serialization_error(qname)
750
751 if "" in out_nsmap and has_unqual_el:
752 # FIXME: can this be handled in XML 1.0?
753 raise ValueError(
754 "cannot use non-qualified names with default_namespace option"
755 )
756
757 # The xml prefix doesn't need to be declared but may have been used to
758 # prefix names. Let's remove it if it has been used
759 out_nsmap.pop("xml", None)
760 return out_nsmap
761
762
763def tostring(
764 element,
765 encoding=None,
766 method=None,
767 *,
768 xml_declaration=None,
769 default_namespace=None,
770 short_empty_elements=True,
771 nsmap=None,
772 root_ns_only=False,
773 minimal_ns_only=False,
774 tree_cls=IncrementalTree,
775):
776 """Generate string representation of XML element.
777
778 All subelements are included. If encoding is "unicode", a string
779 is returned. Otherwise a bytestring is returned.
780
781 *element* is an Element instance, *encoding* is an optional output
782 encoding defaulting to US-ASCII, *method* is an optional output which can
783 be one of "xml" (default), "html", "text" or "c14n", *default_namespace*
784 sets the default XML namespace (for "xmlns").
785
786 Returns an (optionally) encoded string containing the XML data.
787
788 """
789 stream = io.StringIO() if encoding == "unicode" else io.BytesIO()
790 tree_cls(element).write(
791 stream,
792 encoding,
793 xml_declaration=xml_declaration,
794 default_namespace=default_namespace,
795 method=method,
796 short_empty_elements=short_empty_elements,
797 nsmap=nsmap,
798 root_ns_only=root_ns_only,
799 minimal_ns_only=minimal_ns_only,
800 )
801 return stream.getvalue()
802
803
804def tostringlist(
805 element,
806 encoding=None,
807 method=None,
808 *,
809 xml_declaration=None,
810 default_namespace=None,
811 short_empty_elements=True,
812 nsmap=None,
813 root_ns_only=False,
814 minimal_ns_only=False,
815 tree_cls=IncrementalTree,
816):
817 lst = []
818 stream = ET._ListDataStream(lst)
819 tree_cls(element).write(
820 stream,
821 encoding,
822 xml_declaration=xml_declaration,
823 default_namespace=default_namespace,
824 method=method,
825 short_empty_elements=short_empty_elements,
826 nsmap=nsmap,
827 root_ns_only=root_ns_only,
828 minimal_ns_only=minimal_ns_only,
829 )
830 return lst
831
832
833def compat_tostring(
834 element,
835 encoding=None,
836 method=None,
837 *,
838 xml_declaration=None,
839 default_namespace=None,
840 short_empty_elements=True,
841 nsmap=None,
842 root_ns_only=True,
843 minimal_ns_only=False,
844 tree_cls=IncrementalTree,
845):
846 """tostring with options that produce the same results as xml.etree.ElementTree.tostring
847
848 root_ns_only=True is a bit slower than False as it needs to traverse the
849 tree one more time to collect all the namespaces.
850 """
851 return tostring(
852 element,
853 encoding=encoding,
854 method=method,
855 xml_declaration=xml_declaration,
856 default_namespace=default_namespace,
857 short_empty_elements=short_empty_elements,
858 nsmap=nsmap,
859 root_ns_only=root_ns_only,
860 minimal_ns_only=minimal_ns_only,
861 tree_cls=tree_cls,
862 )
863
864
865# --------------------------------------------------------------------
866# serialization support
867
868@contextlib.contextmanager
869def _get_writer(file_or_filename, encoding):
870 # Copied from Python 3.12
871 # returns text write method and release all resources after using
872 try:
873 write = file_or_filename.write
874 except AttributeError:
875 # file_or_filename is a file name
876 if encoding.lower() == "unicode":
877 encoding = "utf-8"
878 with open(file_or_filename, "w", encoding=encoding,
879 errors="xmlcharrefreplace") as file:
880 yield file.write, encoding
881 else:
882 # file_or_filename is a file-like object
883 # encoding determines if it is a text or binary writer
884 if encoding.lower() == "unicode":
885 # use a text writer as is
886 yield write, getattr(file_or_filename, "encoding", None) or "utf-8"
887 else:
888 # wrap a binary writer with TextIOWrapper
889 with contextlib.ExitStack() as stack:
890 if isinstance(file_or_filename, io.BufferedIOBase):
891 file = file_or_filename
892 elif isinstance(file_or_filename, io.RawIOBase):
893 file = io.BufferedWriter(file_or_filename)
894 # Keep the original file open when the BufferedWriter is
895 # destroyed
896 stack.callback(file.detach)
897 else:
898 # This is to handle passed objects that aren't in the
899 # IOBase hierarchy, but just have a write method
900 file = io.BufferedIOBase()
901 file.writable = lambda: True
902 file.write = write
903 try:
904 # TextIOWrapper uses this methods to determine
905 # if BOM (for UTF-16, etc) should be added
906 file.seekable = file_or_filename.seekable
907 file.tell = file_or_filename.tell
908 except AttributeError:
909 pass
910 file = io.TextIOWrapper(file,
911 encoding=encoding,
912 errors="xmlcharrefreplace",
913 newline="\n")
914 # Keep the original file open when the TextIOWrapper is
915 # destroyed
916 stack.callback(file.detach)
917 yield file.write, encoding