Skip to content

Commit 5c159e1

Browse files
committed
[xml.etree.ElementTree] Add fine-grained formatting classes
* ShortEmptyElements — make it possible to remove space between end of tag and slash, also make it possible to turn this on and off based on tag being processed via `defaultdict`. * XMLDeclarationQuotes — change quote char used in XML declaration from `'` to `"`.
1 parent cd26595 commit 5c159e1

File tree

1 file changed

+72
-6
lines changed

1 file changed

+72
-6
lines changed

Lib/xml/etree/ElementTree.py

+72-6
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@
8888
"XMLParser", "XMLPullParser",
8989
"register_namespace",
9090
"canonicalize", "C14NWriterTarget",
91-
]
91+
"XMLDeclarationQuotes",
92+
"ShortEmptyElements"
93+
]
9294

9395
VERSION = "1.3.0"
9496

@@ -99,6 +101,7 @@
99101
import collections
100102
import collections.abc
101103
import contextlib
104+
import enum
102105

103106
from . import ElementPath
104107

@@ -508,6 +511,58 @@ def __eq__(self, other):
508511

509512
# --------------------------------------------------------------------
510513

514+
class XMLDeclarationQuotes(enum.Enum):
515+
"""
516+
Whether or not single quotes or double quotes ought to be used in the XML
517+
declaration.
518+
519+
*SINGLE* (default): <?xml version='1.0' encoding='UTF-8'?>
520+
*DOUBLE*: <?xml version="1.0" encoding="UTF-8"?>
521+
"""
522+
SINGLE = "'"
523+
DOUBLE = '"'
524+
525+
def __str__(self):
526+
return self.value
527+
528+
class ShortEmptyElements(enum.Enum):
529+
"""
530+
This class creates backwards compatibility with the boolean value of
531+
*short_empty_elements* that existed prior to 3.??.
532+
533+
Assuming the tag `<q/>`, the results will be:
534+
535+
*SPACE* (default): `<q />`
536+
*NOSPACE*: `<q/>`
537+
*NONE*: `<q></q>`
538+
"""
539+
SPACE = " "
540+
NOSPACE = ""
541+
NONE = False
542+
543+
def __bool__(self):
544+
return self != ShortEmptyElements.NONE
545+
546+
@classmethod
547+
def _missing_(cls, value):
548+
if value is enum.no_arg:
549+
return cls.SPACE
550+
elif isinstance(value, bool):
551+
return cls.SPACE if value else cls.NONE
552+
else:
553+
return super()._missing_(value)
554+
555+
@classmethod
556+
def tag_defaultdict(cls, short_empty_elements):
557+
if not isinstance(short_empty_elements, collections.defaultdict):
558+
if isinstance(short_empty_elements, ShortEmptyElements):
559+
return collections.defaultdict(lambda: short_empty_elements)
560+
elif bool(short_empty_elements) is True:
561+
return collections.defaultdict(lambda: ShortEmptyElements.SPACE)
562+
else:
563+
return collections.defaultdict(lambda: ShortEmptyElements.NONE)
564+
else:
565+
return short_empty_elements
511566

512567
class ElementTree:
513568
"""An XML element hierarchy.
@@ -680,6 +735,7 @@ def iterfind(self, path, namespaces=None):
680735
def write(self, file_or_filename,
681736
encoding=None,
682737
xml_declaration=None,
738+
xml_declaration_quotes=XMLDeclarationQuotes.SINGLE,
683739
default_namespace=None,
684740
method=None, *,
685741
short_empty_elements=True):
@@ -695,6 +751,9 @@ def write(self, file_or_filename,
695751
is added if encoding IS NOT either of:
696752
US-ASCII, UTF-8, or Unicode
697753
754+
*xml_declaration_quotes* -- Changes character used in XML declaration,
755+
see *XMLDeclarationQuotes*.
756+
698757
*default_namespace* -- sets the default XML namespace (for "xmlns")
699758
700759
*method* -- either "xml" (default), "html, "text", or "c14n"
@@ -703,8 +762,12 @@ def write(self, file_or_filename,
703762
that contain no content. If True (default)
704763
they are emitted as a single self-closed
705764
tag, otherwise they are emitted as a pair
706-
of start/end tags
765+
of start/end tags.
707766
767+
For more control, can be a
768+
*ShortEmptyElements* object, or a
769+
defaultdict keyed by tags as strings and
770+
valued with such objects.
708771
"""
709772
if not method:
710773
method = "xml"
@@ -720,13 +783,16 @@ def write(self, file_or_filename,
720783
(xml_declaration is None and
721784
encoding.lower() != "unicode" and
722785
declared_encoding.lower() not in ("utf-8", "us-ascii"))):
723-
write("<?xml version='1.0' encoding='%s'?>\n" % (
724-
declared_encoding,))
786+
write("<?xml version={0}1.0{0} encoding={0}{1}{0}?>\n"
787+
.format(xml_declaration_quotes, declared_encoding))
788+
if not isinstance(xml_declaration_quotes, XMLDeclarationQuotes):
789+
raise ValueError("Unknown type for `xml_declaration_quotes`")
725790
if method == "text":
726791
_serialize_text(write, self._root)
727792
else:
728793
qnames, namespaces = _namespaces(self._root, default_namespace)
729794
serialize = _serialize[method]
795+
short_empty_elements = ShortEmptyElements.tag_defaultdict(short_empty_elements)
730796
serialize(write, self._root, qnames, namespaces,
731797
short_empty_elements=short_empty_elements)
732798

@@ -885,7 +951,7 @@ def _serialize_xml(write, elem, qnames, namespaces,
885951
else:
886952
v = _escape_attrib(v)
887953
write(" %s=\"%s\"" % (qnames[k], v))
888-
if text or len(elem) or not short_empty_elements:
954+
if text or len(elem) or not bool(short_empty_elements[tag]):
889955
write(">")
890956
if text:
891957
write(_escape_cdata(text))
@@ -894,7 +960,7 @@ def _serialize_xml(write, elem, qnames, namespaces,
894960
short_empty_elements=short_empty_elements)
895961
write("</" + tag + ">")
896962
else:
897-
write(" />")
963+
write(short_empty_elements[tag].value+"/>")
898964
if elem.tail:
899965
write(_escape_cdata(elem.tail))
900966

0 commit comments

Comments
 (0)