easy_xml.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. # Copyright (c) 2011 Google Inc. All rights reserved.
  2. # Use of this source code is governed by a BSD-style license that can be
  3. # found in the LICENSE file.
  4. import locale
  5. import os
  6. import re
  7. import sys
  8. from functools import reduce
  9. def XmlToString(content, encoding="utf-8", pretty=False):
  10. """ Writes the XML content to disk, touching the file only if it has changed.
  11. Visual Studio files have a lot of pre-defined structures. This function makes
  12. it easy to represent these structures as Python data structures, instead of
  13. having to create a lot of function calls.
  14. Each XML element of the content is represented as a list composed of:
  15. 1. The name of the element, a string,
  16. 2. The attributes of the element, a dictionary (optional), and
  17. 3+. The content of the element, if any. Strings are simple text nodes and
  18. lists are child elements.
  19. Example 1:
  20. <test/>
  21. becomes
  22. ['test']
  23. Example 2:
  24. <myelement a='value1' b='value2'>
  25. <childtype>This is</childtype>
  26. <childtype>it!</childtype>
  27. </myelement>
  28. becomes
  29. ['myelement', {'a':'value1', 'b':'value2'},
  30. ['childtype', 'This is'],
  31. ['childtype', 'it!'],
  32. ]
  33. Args:
  34. content: The structured content to be converted.
  35. encoding: The encoding to report on the first XML line.
  36. pretty: True if we want pretty printing with indents and new lines.
  37. Returns:
  38. The XML content as a string.
  39. """
  40. # We create a huge list of all the elements of the file.
  41. xml_parts = ['<?xml version="1.0" encoding="%s"?>' % encoding]
  42. if pretty:
  43. xml_parts.append("\n")
  44. _ConstructContentList(xml_parts, content, pretty)
  45. # Convert it to a string
  46. return "".join(xml_parts)
  47. def _ConstructContentList(xml_parts, specification, pretty, level=0):
  48. """ Appends the XML parts corresponding to the specification.
  49. Args:
  50. xml_parts: A list of XML parts to be appended to.
  51. specification: The specification of the element. See EasyXml docs.
  52. pretty: True if we want pretty printing with indents and new lines.
  53. level: Indentation level.
  54. """
  55. # The first item in a specification is the name of the element.
  56. if pretty:
  57. indentation = " " * level
  58. new_line = "\n"
  59. else:
  60. indentation = ""
  61. new_line = ""
  62. name = specification[0]
  63. if not isinstance(name, str):
  64. raise Exception(
  65. "The first item of an EasyXml specification should be "
  66. "a string. Specification was " + str(specification)
  67. )
  68. xml_parts.append(indentation + "<" + name)
  69. # Optionally in second position is a dictionary of the attributes.
  70. rest = specification[1:]
  71. if rest and isinstance(rest[0], dict):
  72. for at, val in sorted(rest[0].items()):
  73. xml_parts.append(f' {at}="{_XmlEscape(val, attr=True)}"')
  74. rest = rest[1:]
  75. if rest:
  76. xml_parts.append(">")
  77. all_strings = reduce(lambda x, y: x and isinstance(y, str), rest, True)
  78. multi_line = not all_strings
  79. if multi_line and new_line:
  80. xml_parts.append(new_line)
  81. for child_spec in rest:
  82. # If it's a string, append a text node.
  83. # Otherwise recurse over that child definition
  84. if isinstance(child_spec, str):
  85. xml_parts.append(_XmlEscape(child_spec))
  86. else:
  87. _ConstructContentList(xml_parts, child_spec, pretty, level + 1)
  88. if multi_line and indentation:
  89. xml_parts.append(indentation)
  90. xml_parts.append(f"</{name}>{new_line}")
  91. else:
  92. xml_parts.append("/>%s" % new_line)
  93. def WriteXmlIfChanged(content, path, encoding="utf-8", pretty=False,
  94. win32=(sys.platform == "win32")):
  95. """ Writes the XML content to disk, touching the file only if it has changed.
  96. Args:
  97. content: The structured content to be written.
  98. path: Location of the file.
  99. encoding: The encoding to report on the first line of the XML file.
  100. pretty: True if we want pretty printing with indents and new lines.
  101. """
  102. xml_string = XmlToString(content, encoding, pretty)
  103. if win32 and os.linesep != "\r\n":
  104. xml_string = xml_string.replace("\n", "\r\n")
  105. try: # getdefaultlocale() was removed in Python 3.11
  106. default_encoding = locale.getdefaultlocale()[1]
  107. except AttributeError:
  108. default_encoding = locale.getencoding()
  109. if default_encoding and default_encoding.upper() != encoding.upper():
  110. xml_string = xml_string.encode(encoding)
  111. # Get the old content
  112. try:
  113. with open(path) as file:
  114. existing = file.read()
  115. except OSError:
  116. existing = None
  117. # It has changed, write it
  118. if existing != xml_string:
  119. with open(path, "wb") as file:
  120. file.write(xml_string)
  121. _xml_escape_map = {
  122. '"': "&quot;",
  123. "'": "&apos;",
  124. "<": "&lt;",
  125. ">": "&gt;",
  126. "&": "&amp;",
  127. "\n": "&#xA;",
  128. "\r": "&#xD;",
  129. }
  130. _xml_escape_re = re.compile("(%s)" % "|".join(map(re.escape, _xml_escape_map.keys())))
  131. def _XmlEscape(value, attr=False):
  132. """ Escape a string for inclusion in XML."""
  133. def replace(match):
  134. m = match.string[match.start() : match.end()]
  135. # don't replace single quotes in attrs
  136. if attr and m == "'":
  137. return m
  138. return _xml_escape_map[m]
  139. return _xml_escape_re.sub(replace, value)