00001
00002
00003 __applicationName__ = "doxypy"
00004 __blurb__ = """
00005 doxypy is an input filter for Doxygen. It preprocesses python
00006 files so that docstrings of classes and functions are reformatted
00007 into Doxygen-conform documentation blocks.
00008 """
00009
00010 __doc__ = __blurb__ + \
00011 """
00012 In order to make Doxygen preprocess files through doxypy, simply
00013 add the following lines to your Doxyfile:
00014 FILTER_SOURCE_FILES = YES
00015 INPUT_FILTER = "python /path/to/doxypy.py"
00016 """
00017
00018 __version__ = "0.3rc2"
00019 __date__ = "18th December 2007"
00020 __website__ = "http://code.foosel.org/doxypy"
00021
00022 __author__ = (
00023 "Philippe 'demod' Neumann (doxypy at demod dot org)",
00024 "Gina 'foosel' Haeussge (gina at foosel dot net)"
00025 )
00026
00027 __licenseName__ = "GPL v2"
00028 __license__ = """This program is free software: you can redistribute it and/or modify
00029 it under the terms of the GNU General Public License as published by
00030 the Free Software Foundation, either version 2 of the License, or
00031 (at your option) any later version.
00032
00033 This program is distributed in the hope that it will be useful,
00034 but WITHOUT ANY WARRANTY; without even the implied warranty of
00035 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
00036 GNU General Public License for more details.
00037
00038 You should have received a copy of the GNU General Public License
00039 along with this program. If not, see <http://www.gnu.org/licenses/>.
00040 """
00041
00042 import sys
00043 import re
00044
00045 from optparse import OptionParser, OptionGroup
00046
00047 class FSM(object):
00048 """ FSM implements a finite state machine. Transitions are given as
00049 4-tuples, consisting of an origin state, a target state, a condition
00050 for the transition (given as a reference to a function which gets called
00051 with a given piece of input) and a pointer to a function to be called
00052 upon the execution of the given transition.
00053 """
00054
00055 def __init__(self, start_state=None, transitions=[]):
00056 self.transitions = transitions
00057 self.current_state = start_state
00058 self.current_input = None
00059 self.current_transition = None
00060
00061 def setStartState(self, state):
00062 self.current_state = state
00063
00064 def addTransition(self, from_state, to_state, condition, callback):
00065 self.transitions.append([from_state, to_state, condition, callback])
00066
00067 def makeTransition(self, input):
00068 """ Makes a transition based on the given input.
00069 @param input input to parse by the FSM
00070 """
00071 for transition in self.transitions:
00072 [from_state, to_state, condition, callback] = transition
00073 if from_state == self.current_state:
00074 match = condition(input)
00075 if match:
00076 self.current_state = to_state
00077 self.current_input = input
00078 self.current_transition = transition
00079 callback(match)
00080 return
00081
00082
00083 class Doxypy(object):
00084 def __init__(self):
00085 self.start_single_comment_re = re.compile("^\s*(''')")
00086 self.end_single_comment_re = re.compile("(''')\s*$")
00087
00088 self.start_double_comment_re = re.compile("^\s*(\"\"\")")
00089 self.end_double_comment_re = re.compile("(\"\"\")\s*$")
00090
00091 self.single_comment_re = re.compile("^\s*(''').*(''')\s*$")
00092 self.double_comment_re = re.compile("^\s*(\"\"\").*(\"\"\")\s*$")
00093
00094 self.defclass_re = re.compile("^(\s*)(def .+:|class .+:)")
00095 self.empty_re = re.compile("^\s*$")
00096 self.hashline_re = re.compile("^\s*#.*$")
00097 self.importline_re = re.compile("^\s*(import |from .+ import)")
00098
00099 self.multiline_defclass_start_re = re.compile("^(\s*)(def|class)(\s.*)?$")
00100 self.multiline_defclass_end_re = re.compile(":\s*$")
00101
00102
00103
00104 transitions = [
00105
00106
00107
00108 ["FILEHEAD", "FILEHEAD", self.single_comment_re.search, self.appendCommentLine],
00109 ["FILEHEAD", "FILEHEAD", self.double_comment_re.search, self.appendCommentLine],
00110
00111
00112 ["FILEHEAD", "FILEHEAD_COMMENT_SINGLE", self.start_single_comment_re.search, self.appendCommentLine],
00113 ["FILEHEAD_COMMENT_SINGLE", "FILEHEAD", self.end_single_comment_re.search, self.appendCommentLine],
00114 ["FILEHEAD_COMMENT_SINGLE", "FILEHEAD_COMMENT_SINGLE", self.catchall, self.appendCommentLine],
00115 ["FILEHEAD", "FILEHEAD_COMMENT_DOUBLE", self.start_double_comment_re.search, self.appendCommentLine],
00116 ["FILEHEAD_COMMENT_DOUBLE", "FILEHEAD", self.end_double_comment_re.search, self.appendCommentLine],
00117 ["FILEHEAD_COMMENT_DOUBLE", "FILEHEAD_COMMENT_DOUBLE", self.catchall, self.appendCommentLine],
00118
00119
00120 ["FILEHEAD", "FILEHEAD", self.empty_re.search, self.appendFileheadLine],
00121 ["FILEHEAD", "FILEHEAD", self.hashline_re.search, self.appendFileheadLine],
00122 ["FILEHEAD", "FILEHEAD", self.importline_re.search, self.appendFileheadLine],
00123 ["FILEHEAD", "DEFCLASS", self.defclass_re.search, self.resetCommentSearch],
00124 ["FILEHEAD", "DEFCLASS_MULTI", self.multiline_defclass_start_re.search, self.resetCommentSearch],
00125 ["FILEHEAD", "DEFCLASS_BODY", self.catchall, self.appendFileheadLine],
00126
00127
00128
00129
00130 ["DEFCLASS", "DEFCLASS_BODY", self.single_comment_re.search, self.appendCommentLine],
00131 ["DEFCLASS", "DEFCLASS_BODY", self.double_comment_re.search, self.appendCommentLine],
00132
00133
00134 ["DEFCLASS", "COMMENT_SINGLE", self.start_single_comment_re.search, self.appendCommentLine],
00135 ["COMMENT_SINGLE", "DEFCLASS_BODY", self.end_single_comment_re.search, self.appendCommentLine],
00136 ["COMMENT_SINGLE", "COMMENT_SINGLE", self.catchall, self.appendCommentLine],
00137 ["DEFCLASS", "COMMENT_DOUBLE", self.start_double_comment_re.search, self.appendCommentLine],
00138 ["COMMENT_DOUBLE", "DEFCLASS_BODY", self.end_double_comment_re.search, self.appendCommentLine],
00139 ["COMMENT_DOUBLE", "COMMENT_DOUBLE", self.catchall, self.appendCommentLine],
00140
00141
00142 ["DEFCLASS", "DEFCLASS", self.empty_re.search, self.appendDefclassLine],
00143 ["DEFCLASS", "DEFCLASS", self.defclass_re.search, self.resetCommentSearch],
00144 ["DEFCLASS", "DEFCLASS_BODY", self.catchall, self.stopCommentSearch],
00145
00146
00147
00148 ["DEFCLASS_BODY", "DEFCLASS", self.defclass_re.search, self.startCommentSearch],
00149 ["DEFCLASS_BODY", "DEFCLASS_MULTI", self.multiline_defclass_start_re.search, self.startCommentSearch],
00150 ["DEFCLASS_BODY", "DEFCLASS_BODY", self.catchall, self.appendNormalLine],
00151
00152
00153 ["DEFCLASS_MULTI", "DEFCLASS", self.multiline_defclass_end_re.search, self.appendDefclassLine],
00154 ["DEFCLASS_MULTI", "DEFCLASS_MULTI", self.catchall, self.appendDefclassLine],
00155 ]
00156
00157 self.fsm = FSM("FILEHEAD", transitions)
00158
00159 self.output = []
00160
00161 self.comment = []
00162 self.filehead = []
00163 self.defclass = []
00164 self.indent = ""
00165
00166 def __closeComment(self):
00167 """ Appends any open comment block and triggering block to the output. """
00168
00169 if options.autobrief:
00170 if len(self.comment) == 1 \
00171 or (len(self.comment) > 2 and self.comment[1].strip() == ''):
00172 self.comment[0] = self.__docstringSummaryToBrief(self.comment[0])
00173
00174 if self.comment:
00175 block = self.makeCommentBlock()
00176 self.output.extend(block)
00177
00178 if self.defclass:
00179 self.output.extend(self.defclass)
00180
00181 def __docstringSummaryToBrief(self, line):
00182 """ Adds \\brief to the docstrings summary line.
00183
00184 A \\brief is prepended, provided no other doxygen command is at the start of the line.
00185 """
00186 stripped = line.strip()
00187 if stripped and not stripped[0] in ('@', '\\'):
00188 return "\\brief " + line
00189 else:
00190 return line
00191
00192 def catchall(self, input):
00193 """ The catchall-condition, always returns true. """
00194 return True
00195
00196 def resetCommentSearch(self, match):
00197 """ Restarts a new comment search for a different triggering line.
00198 Closes the current commentblock and starts a new comment search.
00199 """
00200 self.__closeComment()
00201 self.startCommentSearch(match)
00202
00203 def startCommentSearch(self, match):
00204 """ Starts a new comment search.
00205 Saves the triggering line, resets the current comment and saves
00206 the current indentation.
00207 """
00208 self.defclass = [self.fsm.current_input]
00209 self.comment = []
00210 self.indent = match.group(1)
00211
00212 def stopCommentSearch(self, match):
00213 """ Stops a comment search.
00214 Closes the current commentblock, resets the triggering line and
00215 appends the current line to the output.
00216 """
00217 self.__closeComment()
00218
00219 self.defclass = []
00220 self.output.append(self.fsm.current_input)
00221
00222 def appendFileheadLine(self, match):
00223 """ Appends a line in the FILEHEAD state.
00224 Closes the open comment block, resets it and appends the current line.
00225 """
00226 self.__closeComment()
00227 self.comment = []
00228 self.output.append(self.fsm.current_input)
00229
00230 def appendCommentLine(self, match):
00231 """ Appends a comment line.
00232 The comment delimiter is removed from multiline start and ends as
00233 well as singleline comments.
00234 """
00235 (from_state, to_state, condition, callback) = self.fsm.current_transition
00236
00237
00238 if (from_state == "DEFCLASS" and to_state == "DEFCLASS_BODY") \
00239 or (from_state == "FILEHEAD" and to_state == "FILEHEAD"):
00240
00241 activeCommentDelim = match.group(1)
00242 line = self.fsm.current_input
00243 self.comment.append(line[line.find(activeCommentDelim)+len(activeCommentDelim):line.rfind(activeCommentDelim)])
00244
00245 if (to_state == "DEFCLASS_BODY"):
00246 self.__closeComment()
00247 self.defclass = []
00248
00249 elif from_state == "DEFCLASS" or from_state == "FILEHEAD":
00250
00251 activeCommentDelim = match.group(1)
00252 line = self.fsm.current_input
00253 self.comment.append(line[line.find(activeCommentDelim)+len(activeCommentDelim):])
00254
00255 elif to_state == "DEFCLASS_BODY" or to_state == "FILEHEAD":
00256
00257 activeCommentDelim = match.group(1)
00258 line = self.fsm.current_input
00259 self.comment.append(line[0:line.rfind(activeCommentDelim)])
00260 if (to_state == "DEFCLASS_BODY"):
00261 self.__closeComment()
00262 self.defclass = []
00263
00264 else:
00265
00266 self.comment.append(self.fsm.current_input)
00267
00268 def appendNormalLine(self, match):
00269 """ Appends a line to the output. """
00270 self.output.append(self.fsm.current_input)
00271
00272 def appendDefclassLine(self, match):
00273 """ Appends a line to the triggering block. """
00274 self.defclass.append(self.fsm.current_input)
00275
00276 def makeCommentBlock(self):
00277 """ Indents the current comment block with respect to the current
00278 indentation level.
00279 @returns a list of indented comment lines
00280 """
00281 doxyStart = "##"
00282 commentLines = self.comment
00283
00284 commentLines = map(lambda x: "%s# %s" % (self.indent, x), commentLines)
00285 l = [self.indent + doxyStart]
00286 l.extend(commentLines)
00287
00288 return l
00289
00290 def parse(self, input):
00291 """ Parses a python file given as input string and returns the doxygen-
00292 compatible representation.
00293 @param input the python code to parse
00294 @returns the modified python code
00295 """
00296 lines = input.split("\n")
00297
00298 for line in lines:
00299 self.fsm.makeTransition(line)
00300
00301 if self.fsm.current_state == "DEFCLASS":
00302 self.__closeComment()
00303
00304 return "\n".join(self.output)
00305
00306 def loadFile(filename):
00307 """ Loads file "filename" and returns the content.
00308 @param filename The name of the file to load
00309 @returns the content of the file.
00310 """
00311 f = open(filename, 'r')
00312
00313 try:
00314 content = f.read()
00315 return content
00316 finally:
00317 f.close()
00318
00319 def optParse():
00320 """ Parses commandline options. """
00321 parser = OptionParser(prog=__applicationName__, version="%prog " + __version__)
00322
00323 parser.set_usage("%prog [options] filename")
00324 parser.add_option("--autobrief",
00325 action="store_true", dest="autobrief",
00326 help="Use the docstring summary line as \\brief description"
00327 )
00328
00329
00330 global options
00331 (options, filename) = parser.parse_args()
00332
00333 if not filename:
00334 print >>sys.stderr, "No filename given."
00335 sys.exit(-1)
00336
00337 return filename[0]
00338
00339 def main():
00340 """ Opens the file given as first commandline argument and processes it,
00341 then prints out the processed file.
00342 """
00343 filename = optParse()
00344
00345 try:
00346 input = loadFile(filename)
00347 except IOError, (errno, msg):
00348 print >>sys.stderr, msg
00349 sys.exit(-1)
00350
00351 fsm = Doxypy()
00352 output = fsm.parse(input)
00353 print output
00354
00355 if __name__ == "__main__":
00356 main()