code.py revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
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
5class Code(object):
6  """A convenience object for constructing code.
7
8  Logically each object should be a block of code. All methods except |Render|
9  and |IsEmpty| return self.
10  """
11  def __init__(self, indent_size=2, comment_length=80):
12    self._code = []
13    self._indent_level = 0
14    self._indent_size = indent_size
15    self._comment_length = comment_length
16
17  def Append(self, line='', substitute=True, indent_level=None):
18    """Appends a line of code at the current indent level or just a newline if
19    line is not specified. Trailing whitespace is stripped.
20
21    substitute: indicated whether this line should be affected by
22    code.Substitute().
23    """
24    if indent_level is None:
25      indent_level = self._indent_level
26    self._code.append(Line(((' ' * indent_level) + line).rstrip(),
27                      substitute=substitute))
28    return self
29
30  def IsEmpty(self):
31    """Returns True if the Code object is empty.
32    """
33    return not bool(self._code)
34
35  def Concat(self, obj):
36    """Concatenate another Code object onto this one. Trailing whitespace is
37    stripped.
38
39    Appends the code at the current indent level. Will fail if there are any
40    un-interpolated format specifiers eg %s, %(something)s which helps
41    isolate any strings that haven't been substituted.
42    """
43    if not isinstance(obj, Code):
44      raise TypeError(type(obj))
45    assert self is not obj
46    for line in obj._code:
47      try:
48        # line % () will fail if any substitution tokens are left in line
49        if line.substitute:
50          line.value %= ()
51      except TypeError:
52        raise TypeError('Unsubstituted value when concatting\n' + line)
53      except ValueError:
54        raise ValueError('Stray % character when concatting\n' + line)
55      self.Append(line.value, line.substitute)
56
57    return self
58
59  def Cblock(self, code):
60    """Concatenates another Code object |code| onto this one followed by a
61    blank line, if |code| is non-empty."""
62    if not code.IsEmpty():
63      self.Concat(code).Append()
64    return self
65
66  def Sblock(self, line=None):
67    """Starts a code block.
68
69    Appends a line of code and then increases the indent level.
70    """
71    if line is not None:
72      self.Append(line)
73    self._indent_level += self._indent_size
74    return self
75
76  def Eblock(self, line=None):
77    """Ends a code block by decreasing and then appending a line (or a blank
78    line if not given).
79    """
80    # TODO(calamity): Decide if type checking is necessary
81    #if not isinstance(line, basestring):
82    #  raise TypeError
83    self._indent_level -= self._indent_size
84    if line is not None:
85      self.Append(line)
86    return self
87
88  def Comment(self, comment, comment_prefix='// '):
89    """Adds the given string as a comment.
90
91    Will split the comment if it's too long. Use mainly for variable length
92    comments. Otherwise just use code.Append('// ...') for comments.
93
94    Unaffected by code.Substitute().
95    """
96    max_len = self._comment_length - self._indent_level - len(comment_prefix)
97    while len(comment) >= max_len:
98      line = comment[0:max_len]
99      last_space = line.rfind(' ')
100      if last_space != -1:
101        line = line[0:last_space]
102        comment = comment[last_space + 1:]
103      else:
104        comment = comment[max_len:]
105      self.Append(comment_prefix + line, substitute=False)
106    self.Append(comment_prefix + comment, substitute=False)
107    return self
108
109  def Substitute(self, d):
110    """Goes through each line and interpolates using the given dict.
111
112    Raises type error if passed something that isn't a dict
113
114    Use for long pieces of code using interpolation with the same variables
115    repeatedly. This will reduce code and allow for named placeholders which
116    are more clear.
117    """
118    if not isinstance(d, dict):
119      raise TypeError('Passed argument is not a dictionary: ' + d)
120    for i, line in enumerate(self._code):
121      if self._code[i].substitute:
122        # Only need to check %s because arg is a dict and python will allow
123        # '%s %(named)s' but just about nothing else
124        if '%s' in self._code[i].value or '%r' in self._code[i].value:
125          raise TypeError('"%s" or "%r" found in substitution. '
126                          'Named arguments only. Use "%" to escape')
127        self._code[i].value = line.value % d
128        self._code[i].substitute = False
129    return self
130
131  def Render(self):
132    """Renders Code as a string.
133    """
134    return '\n'.join([l.value for l in self._code])
135
136class Line(object):
137  """A line of code.
138  """
139  def __init__(self, value, substitute=True):
140    self.value = value
141    self.substitute = substitute
142