Added Mako report library
[odoo/odoo.git] / bin / mako / pygen.py
1 # pygen.py
2 # Copyright (C) 2006, 2007, 2008 Michael Bayer mike_mp@zzzcomputing.com
3 #
4 # This module is part of Mako and is released under
5 # the MIT License: http://www.opensource.org/licenses/mit-license.php
6
7 """utilities for generating and formatting literal Python code."""
8
9 import re, string
10 from StringIO import StringIO
11
12 class PythonPrinter(object):
13     def __init__(self, stream):
14         # indentation counter
15         self.indent = 0
16         
17         # a stack storing information about why we incremented 
18         # the indentation counter, to help us determine if we
19         # should decrement it
20         self.indent_detail = []
21         
22         # the string of whitespace multiplied by the indent
23         # counter to produce a line
24         self.indentstring = "    "
25         
26         # the stream we are writing to
27         self.stream = stream
28         
29         # a list of lines that represents a buffered "block" of code,
30         # which can be later printed relative to an indent level 
31         self.line_buffer = []
32         
33         self.in_indent_lines = False
34         
35         self._reset_multi_line_flags()
36
37     def write(self, text):
38         self.stream.write(text)
39         
40     def write_indented_block(self, block):
41         """print a line or lines of python which already contain indentation.
42         
43         The indentation of the total block of lines will be adjusted to that of
44         the current indent level.""" 
45         self.in_indent_lines = False
46         for l in re.split(r'\r?\n', block):
47             self.line_buffer.append(l)
48     
49     def writelines(self, *lines):
50         """print a series of lines of python."""
51         for line in lines:
52             self.writeline(line)
53                 
54     def writeline(self, line):
55         """print a line of python, indenting it according to the current indent level.
56         
57         this also adjusts the indentation counter according to the content of the line."""
58
59         if not self.in_indent_lines:
60             self._flush_adjusted_lines()
61             self.in_indent_lines = True
62
63         decreased_indent = False
64     
65         if (line is None or 
66             re.match(r"^\s*#",line) or
67             re.match(r"^\s*$", line)
68             ):
69             hastext = False
70         else:
71             hastext = True
72
73         is_comment = line and len(line) and line[0] == '#'
74         
75         # see if this line should decrease the indentation level
76         if (not decreased_indent and 
77             not is_comment and 
78             (not hastext or self._is_unindentor(line))
79             ):
80             
81             if self.indent > 0: 
82                 self.indent -=1
83                 # if the indent_detail stack is empty, the user
84                 # probably put extra closures - the resulting
85                 # module wont compile.  
86                 if len(self.indent_detail) == 0:  
87                     raise "Too many whitespace closures"
88                 self.indent_detail.pop()
89         
90         if line is None:
91             return
92                 
93         # write the line
94         self.stream.write(self._indent_line(line) + "\n")
95         
96         # see if this line should increase the indentation level.
97         # note that a line can both decrase (before printing) and 
98         # then increase (after printing) the indentation level.
99
100         if re.search(r":[ \t]*(?:#.*)?$", line):
101             # increment indentation count, and also
102             # keep track of what the keyword was that indented us,
103             # if it is a python compound statement keyword
104             # where we might have to look for an "unindent" keyword
105             match = re.match(r"^\s*(if|try|elif|while|for)", line)
106             if match:
107                 # its a "compound" keyword, so we will check for "unindentors"
108                 indentor = match.group(1)
109                 self.indent +=1
110                 self.indent_detail.append(indentor)
111             else:
112                 indentor = None
113                 # its not a "compound" keyword.  but lets also
114                 # test for valid Python keywords that might be indenting us,
115                 # else assume its a non-indenting line
116                 m2 = re.match(r"^\s*(def|class|else|elif|except|finally)", line)
117                 if m2:
118                     self.indent += 1
119                     self.indent_detail.append(indentor)
120
121     def close(self):
122         """close this printer, flushing any remaining lines."""
123         self._flush_adjusted_lines()
124     
125     def _is_unindentor(self, line):
126         """return true if the given line is an 'unindentor', relative to the last 'indent' event received."""
127                 
128         # no indentation detail has been pushed on; return False
129         if len(self.indent_detail) == 0: 
130             return False
131
132         indentor = self.indent_detail[-1]
133         
134         # the last indent keyword we grabbed is not a 
135         # compound statement keyword; return False
136         if indentor is None: 
137             return False
138         
139         # if the current line doesnt have one of the "unindentor" keywords,
140         # return False
141         match = re.match(r"^\s*(else|elif|except|finally).*\:", line)
142         if not match: 
143             return False
144         
145         # whitespace matches up, we have a compound indentor,
146         # and this line has an unindentor, this
147         # is probably good enough
148         return True
149         
150         # should we decide that its not good enough, heres
151         # more stuff to check.
152         #keyword = match.group(1)
153         
154         # match the original indent keyword 
155         #for crit in [
156         #   (r'if|elif', r'else|elif'),
157         #   (r'try', r'except|finally|else'),
158         #   (r'while|for', r'else'),
159         #]:
160         #   if re.match(crit[0], indentor) and re.match(crit[1], keyword): return True
161         
162         #return False
163         
164     def _indent_line(self, line, stripspace = ''):
165         """indent the given line according to the current indent level.
166         
167         stripspace is a string of space that will be truncated from the start of the line
168         before indenting."""
169         return re.sub(r"^%s" % stripspace, self.indentstring * self.indent, line)
170
171     def _reset_multi_line_flags(self):
172         """reset the flags which would indicate we are in a backslashed or triple-quoted section."""
173         (self.backslashed, self.triplequoted) = (False, False) 
174         
175     def _in_multi_line(self, line):
176         """return true if the given line is part of a multi-line block, via backslash or triple-quote."""
177         # we are only looking for explicitly joined lines here,
178         # not implicit ones (i.e. brackets, braces etc.).  this is just
179         # to guard against the possibility of modifying the space inside 
180         # of a literal multiline string with unfortunately placed whitespace
181          
182         current_state = (self.backslashed or self.triplequoted) 
183                         
184         if re.search(r"\\$", line):
185             self.backslashed = True
186         else:
187             self.backslashed = False
188             
189         triples = len(re.findall(r"\"\"\"|\'\'\'", line))
190         if triples == 1 or triples % 2 != 0:
191             self.triplequoted = not self.triplequoted
192             
193         return current_state
194
195     def _flush_adjusted_lines(self):
196         stripspace = None
197         self._reset_multi_line_flags()
198         
199         for entry in self.line_buffer:
200             if self._in_multi_line(entry):
201                 self.stream.write(entry + "\n")
202             else:
203                 entry = string.expandtabs(entry)
204                 if stripspace is None and re.search(r"^[ \t]*[^# \t]", entry):
205                     stripspace = re.match(r"^([ \t]*)", entry).group(1)
206                 self.stream.write(self._indent_line(entry, stripspace) + "\n")
207             
208         self.line_buffer = []
209         self._reset_multi_line_flags()
210
211
212 def adjust_whitespace(text):
213     """remove the left-whitespace margin of a block of Python code."""
214     state = [False, False]
215     (backslashed, triplequoted) = (0, 1)
216
217     def in_multi_line(line):
218         start_state = (state[backslashed] or state[triplequoted])
219         
220         if re.search(r"\\$", line):
221             state[backslashed] = True
222         else:
223             state[backslashed] = False
224         
225         def match(reg, t):
226             m = re.match(reg, t)
227             if m:
228                 return m, t[len(m.group(0)):]
229             else:
230                 return None, t
231                 
232         while line:
233             if state[triplequoted]:
234                 m, line = match(r"%s" % state[triplequoted], line)
235                 if m:
236                     state[triplequoted] = False
237                 else:
238                     m, line = match(r".*?(?=%s|$)" % state[triplequoted], line)
239             else:
240                 m, line = match(r'#', line)
241                 if m:
242                     return start_state
243                 
244                 m, line = match(r"\"\"\"|\'\'\'", line)
245                 if m:
246                     state[triplequoted] = m.group(0)
247                     continue
248
249                 m, line = match(r".*?(?=\"\"\"|\'\'\'|#|$)", line)
250             
251         return start_state
252
253     def _indent_line(line, stripspace = ''):
254         return re.sub(r"^%s" % stripspace, '', line)
255
256     lines = []
257     stripspace = None
258
259     for line in re.split(r'\r?\n', text):
260         if in_multi_line(line):
261             lines.append(line)
262         else:
263             line = string.expandtabs(line)
264             if stripspace is None and re.search(r"^[ \t]*[^# \t]", line):
265                 stripspace = re.match(r"^([ \t]*)", line).group(1)
266             lines.append(_indent_line(line, stripspace))
267     return "\n".join(lines)