Imported Upstream version 3.2.2
[debian/gnuradio] / docs / doxygen / other / doxypy.py
1 #!/usr/bin/env python
2
3 __applicationName__ = "doxypy"
4 __blurb__ = """
5 doxypy is an input filter for Doxygen. It preprocesses python
6 files so that docstrings of classes and functions are reformatted
7 into Doxygen-conform documentation blocks.
8 """
9
10 __doc__ = __blurb__ + \
11 """
12 In order to make Doxygen preprocess files through doxypy, simply
13 add the following lines to your Doxyfile:
14         FILTER_SOURCE_FILES = YES
15         INPUT_FILTER = "python /path/to/doxypy.py"
16 """
17
18 __version__ = "0.4.1"
19 __date__ = "5th December 2008"
20 __website__ = "http://code.foosel.org/doxypy"
21
22 __author__ = (
23         "Philippe 'demod' Neumann (doxypy at demod dot org)",
24         "Gina 'foosel' Haeussge (gina at foosel dot net)" 
25 )
26
27 __licenseName__ = "GPL v2"
28 __license__ = """This program is free software: you can redistribute it and/or modify
29 it under the terms of the GNU General Public License as published by
30 the Free Software Foundation, either version 2 of the License, or
31 (at your option) any later version.
32
33 This program is distributed in the hope that it will be useful,
34 but WITHOUT ANY WARRANTY; without even the implied warranty of
35 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
36 GNU General Public License for more details.
37
38 You should have received a copy of the GNU General Public License
39 along with this program.  If not, see <http://www.gnu.org/licenses/>.
40 """
41
42 import sys
43 import re
44
45 from optparse import OptionParser, OptionGroup
46
47 class FSM(object):
48         """Implements a finite state machine.
49         
50         Transitions are given as 4-tuples, consisting of an origin state, a target
51         state, a condition for the transition (given as a reference to a function
52         which gets called with a given piece of input) and a pointer to a function
53         to be called upon the execution of the given transition. 
54         """
55         
56         """
57         @var transitions holds the transitions
58         @var current_state holds the current state
59         @var current_input holds the current input
60         @var current_transition hold the currently active transition
61         """
62         
63         def __init__(self, start_state=None, transitions=[]):
64                 self.transitions = transitions
65                 self.current_state = start_state
66                 self.current_input = None
67                 self.current_transition = None
68                 
69         def setStartState(self, state):
70                 self.current_state = state
71
72         def addTransition(self, from_state, to_state, condition, callback):
73                 self.transitions.append([from_state, to_state, condition, callback])
74                 
75         def makeTransition(self, input):
76                 """ Makes a transition based on the given input.
77
78                 @param  input   input to parse by the FSM
79                 """
80                 for transition in self.transitions:
81                         [from_state, to_state, condition, callback] = transition
82                         if from_state == self.current_state:
83                                 match = condition(input)
84                                 if match:
85                                         self.current_state = to_state
86                                         self.current_input = input
87                                         self.current_transition = transition
88                                         if options.debug:
89                                                 print >>sys.stderr, "# FSM: executing (%s -> %s) for line '%s'" % (from_state, to_state, input)
90                                         callback(match)
91                                         return
92
93 class Doxypy(object):
94         def __init__(self):
95                 string_prefixes = "[uU]?[rR]?"
96                 
97                 self.start_single_comment_re = re.compile("^\s*%s(''')" % string_prefixes)
98                 self.end_single_comment_re = re.compile("(''')\s*$")
99                 
100                 self.start_double_comment_re = re.compile("^\s*%s(\"\"\")" % string_prefixes)
101                 self.end_double_comment_re = re.compile("(\"\"\")\s*$")
102                 
103                 self.single_comment_re = re.compile("^\s*%s(''').*(''')\s*$" % string_prefixes)
104                 self.double_comment_re = re.compile("^\s*%s(\"\"\").*(\"\"\")\s*$" % string_prefixes)
105                 
106                 self.defclass_re = re.compile("^(\s*)(def .+:|class .+:)")
107                 self.empty_re = re.compile("^\s*$")
108                 self.hashline_re = re.compile("^\s*#.*$")
109                 self.importline_re = re.compile("^\s*(import |from .+ import)")
110
111                 self.multiline_defclass_start_re = re.compile("^(\s*)(def|class)(\s.*)?$")
112                 self.multiline_defclass_end_re = re.compile(":\s*$")
113                 
114                 ## Transition list format
115                 #  ["FROM", "TO", condition, action]
116                 transitions = [
117                         ### FILEHEAD
118                         
119                         # single line comments
120                         ["FILEHEAD", "FILEHEAD", self.single_comment_re.search, self.appendCommentLine],
121                         ["FILEHEAD", "FILEHEAD", self.double_comment_re.search, self.appendCommentLine],
122                         
123                         # multiline comments
124                         ["FILEHEAD", "FILEHEAD_COMMENT_SINGLE", self.start_single_comment_re.search, self.appendCommentLine],
125                         ["FILEHEAD_COMMENT_SINGLE", "FILEHEAD", self.end_single_comment_re.search, self.appendCommentLine],
126                         ["FILEHEAD_COMMENT_SINGLE", "FILEHEAD_COMMENT_SINGLE", self.catchall, self.appendCommentLine],
127                         ["FILEHEAD", "FILEHEAD_COMMENT_DOUBLE", self.start_double_comment_re.search, self.appendCommentLine],
128                         ["FILEHEAD_COMMENT_DOUBLE", "FILEHEAD", self.end_double_comment_re.search, self.appendCommentLine],
129                         ["FILEHEAD_COMMENT_DOUBLE", "FILEHEAD_COMMENT_DOUBLE", self.catchall, self.appendCommentLine],
130                         
131                         # other lines
132                         ["FILEHEAD", "FILEHEAD", self.empty_re.search, self.appendFileheadLine],
133                         ["FILEHEAD", "FILEHEAD", self.hashline_re.search, self.appendFileheadLine],
134                         ["FILEHEAD", "FILEHEAD", self.importline_re.search, self.appendFileheadLine],
135                         ["FILEHEAD", "DEFCLASS", self.defclass_re.search, self.resetCommentSearch],
136                         ["FILEHEAD", "DEFCLASS_MULTI", self.multiline_defclass_start_re.search, self.resetCommentSearch],                       
137                         ["FILEHEAD", "DEFCLASS_BODY", self.catchall, self.appendFileheadLine],
138
139                         ### DEFCLASS
140                         
141                         # single line comments
142                         ["DEFCLASS", "DEFCLASS_BODY", self.single_comment_re.search, self.appendCommentLine],
143                         ["DEFCLASS", "DEFCLASS_BODY", self.double_comment_re.search, self.appendCommentLine],
144                         
145                         # multiline comments
146                         ["DEFCLASS", "COMMENT_SINGLE", self.start_single_comment_re.search, self.appendCommentLine],
147                         ["COMMENT_SINGLE", "DEFCLASS_BODY", self.end_single_comment_re.search, self.appendCommentLine],
148                         ["COMMENT_SINGLE", "COMMENT_SINGLE", self.catchall, self.appendCommentLine],
149                         ["DEFCLASS", "COMMENT_DOUBLE", self.start_double_comment_re.search, self.appendCommentLine],
150                         ["COMMENT_DOUBLE", "DEFCLASS_BODY", self.end_double_comment_re.search, self.appendCommentLine],
151                         ["COMMENT_DOUBLE", "COMMENT_DOUBLE", self.catchall, self.appendCommentLine],
152
153                         # other lines
154                         ["DEFCLASS", "DEFCLASS", self.empty_re.search, self.appendDefclassLine],
155                         ["DEFCLASS", "DEFCLASS", self.defclass_re.search, self.resetCommentSearch],
156                         ["DEFCLASS", "DEFCLASS_MULTI", self.multiline_defclass_start_re.search, self.resetCommentSearch],
157                         ["DEFCLASS", "DEFCLASS_BODY", self.catchall, self.stopCommentSearch],
158                         
159                         ### DEFCLASS_BODY
160                         
161                         ["DEFCLASS_BODY", "DEFCLASS", self.defclass_re.search, self.startCommentSearch],
162                         ["DEFCLASS_BODY", "DEFCLASS_MULTI", self.multiline_defclass_start_re.search, self.startCommentSearch],
163                         ["DEFCLASS_BODY", "DEFCLASS_BODY", self.catchall, self.appendNormalLine],
164
165                         ### DEFCLASS_MULTI
166                         ["DEFCLASS_MULTI", "DEFCLASS", self.multiline_defclass_end_re.search, self.appendDefclassLine],
167                         ["DEFCLASS_MULTI", "DEFCLASS_MULTI", self.catchall, self.appendDefclassLine],
168                 ]
169                 
170                 self.fsm = FSM("FILEHEAD", transitions)
171                 self.outstream = sys.stdout
172                 
173                 self.output = []
174                 self.comment = []
175                 self.filehead = []
176                 self.defclass = []
177                 self.indent = ""
178
179         def __closeComment(self):
180                 """Appends any open comment block and triggering block to the output."""
181                 
182                 if options.autobrief:
183                         if len(self.comment) == 1 \
184                         or (len(self.comment) > 2 and self.comment[1].strip() == ''):
185                                 self.comment[0] = self.__docstringSummaryToBrief(self.comment[0])
186                         
187                 if self.comment:
188                         block = self.makeCommentBlock()
189                         self.output.extend(block)
190                         
191                 if self.defclass:
192                         self.output.extend(self.defclass)
193
194         def __docstringSummaryToBrief(self, line):
195                 """Adds \\brief to the docstrings summary line.
196                 
197                 A \\brief is prepended, provided no other doxygen command is at the
198                 start of the line.
199                 """
200                 stripped = line.strip()
201                 if stripped and not stripped[0] in ('@', '\\'):
202                         return "\\brief " + line
203                 else:
204                         return line
205         
206         def __flushBuffer(self):
207                 """Flushes the current outputbuffer to the outstream."""
208                 if self.output:
209                         try:
210                                 if options.debug:
211                                         print >>sys.stderr, "# OUTPUT: ", self.output
212                                 print >>self.outstream, "\n".join(self.output)
213                                 self.outstream.flush()
214                         except IOError:
215                                 # Fix for FS#33. Catches "broken pipe" when doxygen closes 
216                                 # stdout prematurely upon usage of INPUT_FILTER, INLINE_SOURCES 
217                                 # and FILTER_SOURCE_FILES.
218                                 pass
219                 self.output = []
220
221         def catchall(self, input):
222                 """The catchall-condition, always returns true."""
223                 return True
224         
225         def resetCommentSearch(self, match):
226                 """Restarts a new comment search for a different triggering line.
227                 
228                 Closes the current commentblock and starts a new comment search.
229                 """
230                 if options.debug:
231                         print >>sys.stderr, "# CALLBACK: resetCommentSearch" 
232                 self.__closeComment()
233                 self.startCommentSearch(match)
234         
235         def startCommentSearch(self, match):
236                 """Starts a new comment search.
237                 
238                 Saves the triggering line, resets the current comment and saves
239                 the current indentation.
240                 """
241                 if options.debug:
242                         print >>sys.stderr, "# CALLBACK: startCommentSearch"
243                 self.defclass = [self.fsm.current_input]
244                 self.comment = []
245                 self.indent = match.group(1)
246         
247         def stopCommentSearch(self, match):
248                 """Stops a comment search.
249                 
250                 Closes the current commentblock, resets the triggering line and
251                 appends the current line to the output.
252                 """
253                 if options.debug:
254                         print >>sys.stderr, "# CALLBACK: stopCommentSearch" 
255                 self.__closeComment()
256                 
257                 self.defclass = []
258                 self.output.append(self.fsm.current_input)
259         
260         def appendFileheadLine(self, match):
261                 """Appends a line in the FILEHEAD state.
262                 
263                 Closes the open comment block, resets it and appends the current line.
264                 """ 
265                 if options.debug:
266                         print >>sys.stderr, "# CALLBACK: appendFileheadLine" 
267                 self.__closeComment()
268                 self.comment = []
269                 self.output.append(self.fsm.current_input)
270
271         def appendCommentLine(self, match):
272                 """Appends a comment line.
273                 
274                 The comment delimiter is removed from multiline start and ends as
275                 well as singleline comments.
276                 """
277                 if options.debug:
278                         print >>sys.stderr, "# CALLBACK: appendCommentLine" 
279                 (from_state, to_state, condition, callback) = self.fsm.current_transition
280                 
281                 # single line comment
282                 if (from_state == "DEFCLASS" and to_state == "DEFCLASS_BODY") \
283                 or (from_state == "FILEHEAD" and to_state == "FILEHEAD"):
284                         # remove comment delimiter from begin and end of the line
285                         activeCommentDelim = match.group(1)
286                         line = self.fsm.current_input
287                         self.comment.append(line[line.find(activeCommentDelim)+len(activeCommentDelim):line.rfind(activeCommentDelim)])
288
289                         if (to_state == "DEFCLASS_BODY"):
290                                 self.__closeComment()
291                                 self.defclass = []
292                 # multiline start
293                 elif from_state == "DEFCLASS" or from_state == "FILEHEAD":
294                         # remove comment delimiter from begin of the line
295                         activeCommentDelim = match.group(1)
296                         line = self.fsm.current_input
297                         self.comment.append(line[line.find(activeCommentDelim)+len(activeCommentDelim):])
298                 # multiline end
299                 elif to_state == "DEFCLASS_BODY" or to_state == "FILEHEAD":
300                         # remove comment delimiter from end of the line
301                         activeCommentDelim = match.group(1)
302                         line = self.fsm.current_input
303                         self.comment.append(line[0:line.rfind(activeCommentDelim)])
304                         if (to_state == "DEFCLASS_BODY"):
305                                 self.__closeComment()
306                                 self.defclass = []
307                 # in multiline comment
308                 else:
309                         # just append the comment line
310                         self.comment.append(self.fsm.current_input)
311         
312         def appendNormalLine(self, match):
313                 """Appends a line to the output."""
314                 if options.debug:
315                         print >>sys.stderr, "# CALLBACK: appendNormalLine" 
316                 self.output.append(self.fsm.current_input)
317                 
318         def appendDefclassLine(self, match):
319                 """Appends a line to the triggering block."""
320                 if options.debug:
321                         print >>sys.stderr, "# CALLBACK: appendDefclassLine" 
322                 self.defclass.append(self.fsm.current_input)
323         
324         def makeCommentBlock(self):
325                 """Indents the current comment block with respect to the current
326                 indentation level.
327
328                 @returns a list of indented comment lines
329                 """
330                 doxyStart = "##"
331                 commentLines = self.comment
332                 
333                 commentLines = map(lambda x: "%s# %s" % (self.indent, x), commentLines)
334                 l = [self.indent + doxyStart]
335                 l.extend(commentLines)
336                          
337                 return l
338         
339         def parse(self, input):
340                 """Parses a python file given as input string and returns the doxygen-
341                 compatible representation.
342                 
343                 @param  input   the python code to parse
344                 @returns the modified python code
345                 """ 
346                 lines = input.split("\n")
347                 
348                 for line in lines:
349                         self.fsm.makeTransition(line)
350                         
351                 if self.fsm.current_state == "DEFCLASS":
352                         self.__closeComment()
353                 
354                 return "\n".join(self.output)
355         
356         def parseFile(self, filename):
357                 """Parses a python file given as input string and returns the doxygen-
358                 compatible representation.
359                 
360                 @param  input   the python code to parse
361                 @returns the modified python code
362                 """ 
363                 f = open(filename, 'r')
364                 
365                 for line in f:
366                         self.parseLine(line.rstrip('\r\n'))
367                 if self.fsm.current_state == "DEFCLASS":
368                         self.__closeComment()
369                         self.__flushBuffer()
370                 f.close()
371         
372         def parseLine(self, line):
373                 """Parse one line of python and flush the resulting output to the 
374                 outstream.
375                 
376                 @param  line    the python code line to parse
377                 """
378                 self.fsm.makeTransition(line)
379                 self.__flushBuffer()
380         
381 def optParse():
382         """Parses commandline options."""
383         parser = OptionParser(prog=__applicationName__, version="%prog " + __version__)
384         
385         parser.set_usage("%prog [options] filename")
386         parser.add_option("--autobrief",
387                 action="store_true", dest="autobrief",
388                 help="use the docstring summary line as \\brief description"
389         )
390         parser.add_option("--debug",
391                 action="store_true", dest="debug",
392                 help="enable debug output on stderr"
393         )
394         
395         ## parse options
396         global options
397         (options, filename) = parser.parse_args()
398         
399         if not filename:
400                 print >>sys.stderr, "No filename given."
401                 sys.exit(-1)
402         
403         return filename[0]
404
405 def main():
406         """Starts the parser on the file given by the filename as the first 
407         argument on the commandline.
408         """
409         filename = optParse()
410         fsm = Doxypy()
411         fsm.parseFile(filename)
412
413 if __name__ == "__main__":
414         main()