class Code(object):
"""A convenience object for constructing code.
Logically each object should be a block of code. All methods except |Render|
and |IsEmpty| return self.
"""
def __init__(self, indent_size=2, comment_length=80):
self._code = []
self._indent_size = indent_size
self._comment_length = comment_length
self._line_prefixes = []
def Append(self,
line='',
substitute=True,
indent_level=None,
new_line=True,
strip_right=True):
"""Appends a line of code at the current indent level or just a newline if
line is not specified.
substitute: indicated whether this line should be affected by
code.Substitute().
new_line: whether this should be added as a new line, or should be appended
to the last line of the code.
strip_right: whether or not trailing whitespace should be stripped.
"""
if line:
prefix = indent_level * ' ' if indent_level else ''.join(
self._line_prefixes)
else:
prefix = ''
if strip_right:
line = line.rstrip()
if not new_line and self._code:
self._code[-1].value += line
else:
self._code.append(Line(prefix + line, substitute=substitute))
return self
def IsEmpty(self):
"""Returns True if the Code object is empty.
"""
return not bool(self._code)
def Concat(self, obj, new_line=True):
"""Concatenate another Code object onto this one. Trailing whitespace is
stripped.
Appends the code at the current indent level. Will fail if there are any
un-interpolated format specifiers eg %s, %(something)s which helps
isolate any strings that haven't been substituted.
"""
if not isinstance(obj, Code):
raise TypeError(type(obj))
assert self is not obj
if not obj._code:
return self
for line in obj._code:
try:
if line.substitute:
line.value %= ()
except TypeError:
raise TypeError('Unsubstituted value when concatting\n' + line.value)
except ValueError:
raise ValueError('Stray % character when concatting\n' + line.value)
first_line = obj._code.pop(0)
self.Append(first_line.value, first_line.substitute, new_line=new_line)
for line in obj._code:
self.Append(line.value, line.substitute)
return self
def Cblock(self, code):
"""Concatenates another Code object |code| onto this one followed by a
blank line, if |code| is non-empty."""
if not code.IsEmpty():
self.Concat(code).Append()
return self
def Sblock(self, line=None, line_prefix=None, new_line=True):
"""Starts a code block.
Appends a line of code and then increases the indent level. If |line_prefix|
is present, it will be treated as the extra prefix for the code block.
Otherwise, the prefix will be the default indent level.
"""
if line is not None:
self.Append(line, new_line=new_line)
self._line_prefixes.append(line_prefix or ' ' * self._indent_size)
return self
def Eblock(self, line=None):
"""Ends a code block by decreasing and then appending a line (or a blank
line if not given).
"""
self._line_prefixes.pop()
if line is not None:
self.Append(line)
return self
def Comment(self,
comment,
comment_prefix='// ',
wrap_indent=0,
new_line=True):
"""Adds the given string as a comment.
Will split the comment if it's too long. Use mainly for variable length
comments. Otherwise just use code.Append('// ...') for comments.
Unaffected by code.Substitute().
"""
def trim_comment(comment, max_len):
if len(comment) <= max_len:
return comment, ''
if max_len <= 1:
return '', comment.lstrip()
last_space = comment.rfind(' ', 0, max_len + 1)
if last_space != -1:
line = comment[0:last_space]
comment = comment[last_space + 1:]
else:
line = comment
comment = ''
return line, comment.lstrip()
if not new_line and self._code:
max_len = self._comment_length - len(self._code[-1].value)
else:
max_len = (self._comment_length - len(''.join(self._line_prefixes)) -
len(comment_prefix))
line, comment = trim_comment(comment, max_len)
self.Append(comment_prefix + line, substitute=False, new_line=new_line)
max_len = (self._comment_length - len(''.join(self._line_prefixes)) -
len(comment_prefix) - wrap_indent)
assert max_len > 1
while len(comment):
line, comment = trim_comment(comment, max_len)
self.Append(comment_prefix + (' ' * wrap_indent) + line, substitute=False)
return self
def Substitute(self, d):
"""Goes through each line and interpolates using the given dict.
Raises type error if passed something that isn't a dict
Use for long pieces of code using interpolation with the same variables
repeatedly. This will reduce code and allow for named placeholders which
are more clear.
"""
if not isinstance(d, dict):
raise TypeError('Passed argument is not a dictionary: ' + d)
for i, line in enumerate(self._code):
if self._code[i].substitute:
if '%s' in self._code[i].value or '%r' in self._code[i].value:
raise TypeError('"%s" or "%r" found in substitution. '
'Named arguments only. Use "%" to escape')
self._code[i].value = line.value % d
self._code[i].substitute = False
return self
def TrimTrailingNewlines(self):
"""Removes any trailing empty Line objects.
"""
while self._code:
if self._code[-1].value != '':
return
self._code = self._code[:-1]
def Render(self):
"""Renders Code as a string.
"""
return '\n'.join([l.value for l in self._code])
class Line(object):
"""A line of code.
"""
def __init__(self, value, substitute=True):
self.value = value
self.substitute = substitute