code.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. # Copyright (c) 2012 The Chromium Authors. 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. class Code(object):
  5. """A convenience object for constructing code.
  6. Logically each object should be a block of code. All methods except |Render|
  7. and |IsEmpty| return self.
  8. """
  9. def __init__(self, indent_size=2, comment_length=80):
  10. self._code = []
  11. self._indent_size = indent_size
  12. self._comment_length = comment_length
  13. self._line_prefixes = []
  14. def Append(self, line='',
  15. substitute=True,
  16. indent_level=None,
  17. new_line=True,
  18. strip_right=True):
  19. """Appends a line of code at the current indent level or just a newline if
  20. line is not specified.
  21. substitute: indicated whether this line should be affected by
  22. code.Substitute().
  23. new_line: whether this should be added as a new line, or should be appended
  24. to the last line of the code.
  25. strip_right: whether or not trailing whitespace should be stripped.
  26. """
  27. if line:
  28. prefix = indent_level * ' ' if indent_level else ''.join(
  29. self._line_prefixes)
  30. else:
  31. prefix = ''
  32. if strip_right:
  33. line = line.rstrip()
  34. if not new_line and self._code:
  35. self._code[-1].value += line
  36. else:
  37. self._code.append(Line(prefix + line, substitute=substitute))
  38. return self
  39. def IsEmpty(self):
  40. """Returns True if the Code object is empty.
  41. """
  42. return not bool(self._code)
  43. def Concat(self, obj, new_line=True):
  44. """Concatenate another Code object onto this one. Trailing whitespace is
  45. stripped.
  46. Appends the code at the current indent level. Will fail if there are any
  47. un-interpolated format specifiers eg %s, %(something)s which helps
  48. isolate any strings that haven't been substituted.
  49. """
  50. if not isinstance(obj, Code):
  51. raise TypeError(type(obj))
  52. assert self is not obj
  53. if not obj._code:
  54. return self
  55. for line in obj._code:
  56. try:
  57. # line % () will fail if any substitution tokens are left in line
  58. if line.substitute:
  59. line.value %= ()
  60. except TypeError:
  61. raise TypeError('Unsubstituted value when concatting\n' + line.value)
  62. except ValueError:
  63. raise ValueError('Stray % character when concatting\n' + line.value)
  64. first_line = obj._code.pop(0)
  65. self.Append(first_line.value, first_line.substitute, new_line=new_line)
  66. for line in obj._code:
  67. self.Append(line.value, line.substitute)
  68. return self
  69. def Cblock(self, code):
  70. """Concatenates another Code object |code| onto this one followed by a
  71. blank line, if |code| is non-empty."""
  72. if not code.IsEmpty():
  73. self.Concat(code).Append()
  74. return self
  75. def Sblock(self, line=None, line_prefix=None, new_line=True):
  76. """Starts a code block.
  77. Appends a line of code and then increases the indent level. If |line_prefix|
  78. is present, it will be treated as the extra prefix for the code block.
  79. Otherwise, the prefix will be the default indent level.
  80. """
  81. if line is not None:
  82. self.Append(line, new_line=new_line)
  83. self._line_prefixes.append(line_prefix or ' ' * self._indent_size)
  84. return self
  85. def Eblock(self, line=None):
  86. """Ends a code block by decreasing and then appending a line (or a blank
  87. line if not given).
  88. """
  89. # TODO(calamity): Decide if type checking is necessary
  90. #if not isinstance(line, basestring):
  91. # raise TypeError
  92. self._line_prefixes.pop()
  93. if line is not None:
  94. self.Append(line)
  95. return self
  96. def Comment(self, comment, comment_prefix='// ',
  97. wrap_indent=0, new_line=True):
  98. """Adds the given string as a comment.
  99. Will split the comment if it's too long. Use mainly for variable length
  100. comments. Otherwise just use code.Append('// ...') for comments.
  101. Unaffected by code.Substitute().
  102. """
  103. # Helper function to trim a comment to the maximum length, and return one
  104. # line and the remainder of the comment.
  105. def trim_comment(comment, max_len):
  106. if len(comment) <= max_len:
  107. return comment, ''
  108. # If we ran out of space due to existing content, don't try to wrap.
  109. if max_len <= 1:
  110. return '', comment.lstrip()
  111. last_space = comment.rfind(' ', 0, max_len + 1)
  112. if last_space != -1:
  113. line = comment[0:last_space]
  114. comment = comment[last_space + 1:]
  115. else:
  116. # If the line can't be split, then don't try. The comments might be
  117. # important (e.g. JSDoc) where splitting it breaks things.
  118. line = comment
  119. comment = ''
  120. return line, comment.lstrip()
  121. # First line has the full maximum length.
  122. if not new_line and self._code:
  123. max_len = self._comment_length - len(self._code[-1].value)
  124. else:
  125. max_len = (self._comment_length - len(''.join(self._line_prefixes)) -
  126. len(comment_prefix))
  127. line, comment = trim_comment(comment, max_len)
  128. self.Append(comment_prefix + line, substitute=False, new_line=new_line)
  129. # Any subsequent lines be subject to the wrap indent.
  130. max_len = (self._comment_length - len(''.join(self._line_prefixes)) -
  131. len(comment_prefix) - wrap_indent)
  132. assert max_len > 1
  133. while len(comment):
  134. line, comment = trim_comment(comment, max_len)
  135. self.Append(comment_prefix + (' ' * wrap_indent) + line, substitute=False)
  136. return self
  137. def Substitute(self, d):
  138. """Goes through each line and interpolates using the given dict.
  139. Raises type error if passed something that isn't a dict
  140. Use for long pieces of code using interpolation with the same variables
  141. repeatedly. This will reduce code and allow for named placeholders which
  142. are more clear.
  143. """
  144. if not isinstance(d, dict):
  145. raise TypeError('Passed argument is not a dictionary: ' + d)
  146. for i, line in enumerate(self._code):
  147. if self._code[i].substitute:
  148. # Only need to check %s because arg is a dict and python will allow
  149. # '%s %(named)s' but just about nothing else
  150. if '%s' in self._code[i].value or '%r' in self._code[i].value:
  151. raise TypeError('"%s" or "%r" found in substitution. '
  152. 'Named arguments only. Use "%" to escape')
  153. self._code[i].value = line.value % d
  154. self._code[i].substitute = False
  155. return self
  156. def TrimTrailingNewlines(self):
  157. """Removes any trailing empty Line objects.
  158. """
  159. while self._code:
  160. if self._code[-1].value != '':
  161. return
  162. self._code = self._code[:-1]
  163. def Render(self):
  164. """Renders Code as a string.
  165. """
  166. return '\n'.join([l.value for l in self._code])
  167. class Line(object):
  168. """A line of code.
  169. """
  170. def __init__(self, value, substitute=True):
  171. self.value = value
  172. self.substitute = substitute