3 __applicationName__ = "doxypy"
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.
10 __doc__ = __blurb__ + \
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"
19 __date__ = "5th December 2008"
20 __website__ = "http://code.foosel.org/doxypy"
23 "Philippe 'demod' Neumann (doxypy at demod dot org)",
24 "Gina 'foosel' Haeussge (gina at foosel dot net)"
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.
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.
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/>.
45 from optparse import OptionParser, OptionGroup
48 """Implements a finite state machine.
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.
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
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
69 def setStartState(self, state):
70 self.current_state = state
72 def addTransition(self, from_state, to_state, condition, callback):
73 self.transitions.append([from_state, to_state, condition, callback])
75 def makeTransition(self, input):
76 """ Makes a transition based on the given input.
78 @param input input to parse by the FSM
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)
85 self.current_state = to_state
86 self.current_input = input
87 self.current_transition = transition
89 print >>sys.stderr, "# FSM: executing (%s -> %s) for line '%s'" % (from_state, to_state, input)
95 string_prefixes = "[uU]?[rR]?"
97 self.start_single_comment_re = re.compile("^\s*%s(''')" % string_prefixes)
98 self.end_single_comment_re = re.compile("(''')\s*$")
100 self.start_double_comment_re = re.compile("^\s*%s(\"\"\")" % string_prefixes)
101 self.end_double_comment_re = re.compile("(\"\"\")\s*$")
103 self.single_comment_re = re.compile("^\s*%s(''').*(''')\s*$" % string_prefixes)
104 self.double_comment_re = re.compile("^\s*%s(\"\"\").*(\"\"\")\s*$" % string_prefixes)
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)")
111 self.multiline_defclass_start_re = re.compile("^(\s*)(def|class)(\s.*)?$")
112 self.multiline_defclass_end_re = re.compile(":\s*$")
114 ## Transition list format
115 # ["FROM", "TO", condition, action]
119 # single line comments
120 ["FILEHEAD", "FILEHEAD", self.single_comment_re.search, self.appendCommentLine],
121 ["FILEHEAD", "FILEHEAD", self.double_comment_re.search, self.appendCommentLine],
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],
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],
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],
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],
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],
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],
166 ["DEFCLASS_MULTI", "DEFCLASS", self.multiline_defclass_end_re.search, self.appendDefclassLine],
167 ["DEFCLASS_MULTI", "DEFCLASS_MULTI", self.catchall, self.appendDefclassLine],
170 self.fsm = FSM("FILEHEAD", transitions)
171 self.outstream = sys.stdout
179 def __closeComment(self):
180 """Appends any open comment block and triggering block to the output."""
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])
188 block = self.makeCommentBlock()
189 self.output.extend(block)
192 self.output.extend(self.defclass)
194 def __docstringSummaryToBrief(self, line):
195 """Adds \\brief to the docstrings summary line.
197 A \\brief is prepended, provided no other doxygen command is at the
200 stripped = line.strip()
201 if stripped and not stripped[0] in ('@', '\\'):
202 return "\\brief " + line
206 def __flushBuffer(self):
207 """Flushes the current outputbuffer to the outstream."""
211 print >>sys.stderr, "# OUTPUT: ", self.output
212 print >>self.outstream, "\n".join(self.output)
213 self.outstream.flush()
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.
221 def catchall(self, input):
222 """The catchall-condition, always returns true."""
225 def resetCommentSearch(self, match):
226 """Restarts a new comment search for a different triggering line.
228 Closes the current commentblock and starts a new comment search.
231 print >>sys.stderr, "# CALLBACK: resetCommentSearch"
232 self.__closeComment()
233 self.startCommentSearch(match)
235 def startCommentSearch(self, match):
236 """Starts a new comment search.
238 Saves the triggering line, resets the current comment and saves
239 the current indentation.
242 print >>sys.stderr, "# CALLBACK: startCommentSearch"
243 self.defclass = [self.fsm.current_input]
245 self.indent = match.group(1)
247 def stopCommentSearch(self, match):
248 """Stops a comment search.
250 Closes the current commentblock, resets the triggering line and
251 appends the current line to the output.
254 print >>sys.stderr, "# CALLBACK: stopCommentSearch"
255 self.__closeComment()
258 self.output.append(self.fsm.current_input)
260 def appendFileheadLine(self, match):
261 """Appends a line in the FILEHEAD state.
263 Closes the open comment block, resets it and appends the current line.
266 print >>sys.stderr, "# CALLBACK: appendFileheadLine"
267 self.__closeComment()
269 self.output.append(self.fsm.current_input)
271 def appendCommentLine(self, match):
272 """Appends a comment line.
274 The comment delimiter is removed from multiline start and ends as
275 well as singleline comments.
278 print >>sys.stderr, "# CALLBACK: appendCommentLine"
279 (from_state, to_state, condition, callback) = self.fsm.current_transition
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)])
289 if (to_state == "DEFCLASS_BODY"):
290 self.__closeComment()
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):])
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()
307 # in multiline comment
309 # just append the comment line
310 self.comment.append(self.fsm.current_input)
312 def appendNormalLine(self, match):
313 """Appends a line to the output."""
315 print >>sys.stderr, "# CALLBACK: appendNormalLine"
316 self.output.append(self.fsm.current_input)
318 def appendDefclassLine(self, match):
319 """Appends a line to the triggering block."""
321 print >>sys.stderr, "# CALLBACK: appendDefclassLine"
322 self.defclass.append(self.fsm.current_input)
324 def makeCommentBlock(self):
325 """Indents the current comment block with respect to the current
328 @returns a list of indented comment lines
331 commentLines = self.comment
333 commentLines = map(lambda x: "%s# %s" % (self.indent, x), commentLines)
334 l = [self.indent + doxyStart]
335 l.extend(commentLines)
339 def parse(self, input):
340 """Parses a python file given as input string and returns the doxygen-
341 compatible representation.
343 @param input the python code to parse
344 @returns the modified python code
346 lines = input.split("\n")
349 self.fsm.makeTransition(line)
351 if self.fsm.current_state == "DEFCLASS":
352 self.__closeComment()
354 return "\n".join(self.output)
356 def parseFile(self, filename):
357 """Parses a python file given as input string and returns the doxygen-
358 compatible representation.
360 @param input the python code to parse
361 @returns the modified python code
363 f = open(filename, 'r')
366 self.parseLine(line.rstrip('\r\n'))
367 if self.fsm.current_state == "DEFCLASS":
368 self.__closeComment()
372 def parseLine(self, line):
373 """Parse one line of python and flush the resulting output to the
376 @param line the python code line to parse
378 self.fsm.makeTransition(line)
382 """Parses commandline options."""
383 parser = OptionParser(prog=__applicationName__, version="%prog " + __version__)
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"
390 parser.add_option("--debug",
391 action="store_true", dest="debug",
392 help="enable debug output on stderr"
397 (options, filename) = parser.parse_args()
400 print >>sys.stderr, "No filename given."
406 """Starts the parser on the file given by the filename as the first
407 argument on the commandline.
409 filename = optParse()
411 fsm.parseFile(filename)
413 if __name__ == "__main__":