summaryrefslogtreecommitdiffstats
path: root/tools/json_schema_compiler/code.py
blob: a0a2a5fab6dd23af92b6aa08029a44a9659d3e10 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

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_level = 0
    self._indent_size = indent_size
    self._comment_length = comment_length

  def Append(self, line='', substitute=True, indent_level=None):
    """Appends a line of code at the current indent level or just a newline if
    line is not specified. Trailing whitespace is stripped.

    substitute: indicated whether this line should be affected by
    code.Substitute().
    """
    if indent_level is None:
      indent_level = self._indent_level
    self._code.append(Line(((' ' * indent_level) + line).rstrip(),
                      substitute=substitute))
    return self

  def IsEmpty(self):
    """Returns True if the Code object is empty.
    """
    return not bool(self._code)

  def Concat(self, obj):
    """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
    for line in obj._code:
      try:
        # line % () will fail if any substitution tokens are left in line
        if line.substitute:
          line.value %= ()
      except TypeError:
        raise TypeError('Unsubstituted value when concatting\n' + line)
      except ValueError:
        raise ValueError('Stray % character when concatting\n' + line)
      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):
    """Starts a code block.

    Appends a line of code and then increases the indent level.
    """
    if line is not None:
      self.Append(line)
    self._indent_level += 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).
    """
    # TODO(calamity): Decide if type checking is necessary
    #if not isinstance(line, basestring):
    #  raise TypeError
    self._indent_level -= self._indent_size
    if line is not None:
      self.Append(line)
    return self

  def Comment(self, comment, comment_prefix='// '):
    """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().
    """
    max_len = self._comment_length - self._indent_level - len(comment_prefix)
    while len(comment) >= max_len:
      line = comment[0:max_len]
      last_space = line.rfind(' ')
      if last_space != -1:
        line = line[0:last_space]
        comment = comment[last_space + 1:]
      else:
        comment = comment[max_len:]
      self.Append(comment_prefix + line, substitute=False)
    self.Append(comment_prefix + comment, 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:
        # Only need to check %s because arg is a dict and python will allow
        # '%s %(named)s' but just about nothing else
        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 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