kopia lustrzana https://github.com/cariboulabs/cariboulite
447 wiersze
16 KiB
Python
447 wiersze
16 KiB
Python
#!/usr/bin/env python
|
|
|
|
|
|
__applicationName__ = "doxypy"
|
|
__blurb__ = """
|
|
doxypy is an input filter for Doxygen. It preprocesses python
|
|
files so that docstrings of classes and functions are reformatted
|
|
into Doxygen-conform documentation blocks.
|
|
"""
|
|
|
|
__doc__ = __blurb__ + \
|
|
"""
|
|
In order to make Doxygen preprocess files through doxypy, simply
|
|
add the following lines to your Doxyfile:
|
|
FILTER_SOURCE_FILES = YES
|
|
INPUT_FILTER = "python /path/to/doxypy.py"
|
|
"""
|
|
|
|
__version__ = "0.4.2"
|
|
__date__ = "5th December 2008"
|
|
__website__ = "http://code.foosel.org/doxypy"
|
|
|
|
__author__ = (
|
|
"Philippe 'demod' Neumann (doxypy at demod dot org)",
|
|
"Gina 'foosel' Haeussge (gina at foosel dot net)"
|
|
)
|
|
|
|
__licenseName__ = "GPL v2"
|
|
__license__ = """This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
import sys
|
|
import re
|
|
|
|
from argparse import ArgumentParser
|
|
|
|
|
|
class FSM(object):
|
|
"""Implements a finite state machine.
|
|
|
|
Transitions are given as 4-tuples, consisting of an origin state, a target
|
|
state, a condition for the transition (given as a reference to a function
|
|
which gets called with a given piece of input) and a pointer to a function
|
|
to be called upon the execution of the given transition.
|
|
"""
|
|
|
|
"""
|
|
@var transitions holds the transitions
|
|
@var current_state holds the current state
|
|
@var current_input holds the current input
|
|
@var current_transition hold the currently active transition
|
|
"""
|
|
|
|
def __init__(self, start_state=None, transitions=[]):
|
|
self.transitions = transitions
|
|
self.current_state = start_state
|
|
self.current_input = None
|
|
self.current_transition = None
|
|
|
|
def setStartState(self, state):
|
|
self.current_state = state
|
|
|
|
def addTransition(self, from_state, to_state, condition, callback):
|
|
self.transitions.append([from_state, to_state, condition, callback])
|
|
|
|
def makeTransition(self, input):
|
|
""" Makes a transition based on the given input.
|
|
|
|
@param input input to parse by the FSM
|
|
"""
|
|
for transition in self.transitions:
|
|
[from_state, to_state, condition, callback] = transition
|
|
if from_state == self.current_state:
|
|
match = condition(input)
|
|
if match:
|
|
self.current_state = to_state
|
|
self.current_input = input
|
|
self.current_transition = transition
|
|
if args.debug:
|
|
print("# FSM: executing (%s -> %s) for line '%s'" %
|
|
(from_state, to_state, input), file=sys.stderr)
|
|
callback(match)
|
|
return
|
|
|
|
|
|
class Doxypy(object):
|
|
def __init__(self):
|
|
string_prefixes = "[uU]?[rR]?"
|
|
|
|
self.start_single_comment_re = re.compile(
|
|
r"^\s*%s(''')" % string_prefixes)
|
|
self.end_single_comment_re = re.compile(r"(''')\s*$")
|
|
|
|
self.start_double_comment_re = re.compile(
|
|
r'^\s*%s(""")' % string_prefixes)
|
|
self.end_double_comment_re = re.compile(r'(""")\s*$')
|
|
|
|
self.single_comment_re = re.compile(
|
|
r"^\s*%s(''').*(''')\s*$" % string_prefixes)
|
|
self.double_comment_re = re.compile(
|
|
r'^\s*%s(""").*(""")\s*$' % string_prefixes)
|
|
|
|
self.defclass_re = re.compile(r"^(\s*)(def .+:|class .+:)")
|
|
self.empty_re = re.compile(r"^\s*$")
|
|
self.hashline_re = re.compile(r"^\s*#.*$")
|
|
self.importline_re = re.compile(r"^\s*(import |from .+ import)")
|
|
|
|
self.multiline_defclass_start_re = re.compile(
|
|
r"^(\s*)(def|class)(\s.*)?$")
|
|
self.multiline_defclass_end_re = re.compile(r":\s*$")
|
|
|
|
# Transition list format
|
|
# ["FROM", "TO", condition, action]
|
|
transitions = [
|
|
# FILEHEAD
|
|
|
|
# single line comments
|
|
["FILEHEAD", "FILEHEAD", self.single_comment_re.search,
|
|
self.appendCommentLine],
|
|
["FILEHEAD", "FILEHEAD", self.double_comment_re.search,
|
|
self.appendCommentLine],
|
|
|
|
# multiline comments
|
|
["FILEHEAD", "FILEHEAD_COMMENT_SINGLE",
|
|
self.start_single_comment_re.search, self.appendCommentLine],
|
|
["FILEHEAD_COMMENT_SINGLE", "FILEHEAD",
|
|
self.end_single_comment_re.search, self.appendCommentLine],
|
|
["FILEHEAD_COMMENT_SINGLE", "FILEHEAD_COMMENT_SINGLE",
|
|
self.catchall, self.appendCommentLine],
|
|
["FILEHEAD", "FILEHEAD_COMMENT_DOUBLE",
|
|
self.start_double_comment_re.search, self.appendCommentLine],
|
|
["FILEHEAD_COMMENT_DOUBLE", "FILEHEAD",
|
|
self.end_double_comment_re.search, self.appendCommentLine],
|
|
["FILEHEAD_COMMENT_DOUBLE", "FILEHEAD_COMMENT_DOUBLE",
|
|
self.catchall, self.appendCommentLine],
|
|
|
|
# other lines
|
|
["FILEHEAD", "FILEHEAD", self.empty_re.search, self.appendFileheadLine],
|
|
["FILEHEAD", "FILEHEAD", self.hashline_re.search, self.appendFileheadLine],
|
|
["FILEHEAD", "FILEHEAD", self.importline_re.search,
|
|
self.appendFileheadLine],
|
|
["FILEHEAD", "DEFCLASS", self.defclass_re.search, self.resetCommentSearch],
|
|
["FILEHEAD", "DEFCLASS_MULTI",
|
|
self.multiline_defclass_start_re.search, self.resetCommentSearch],
|
|
["FILEHEAD", "DEFCLASS_BODY", self.catchall, self.appendFileheadLine],
|
|
|
|
# DEFCLASS
|
|
|
|
# single line comments
|
|
["DEFCLASS", "DEFCLASS_BODY",
|
|
self.single_comment_re.search, self.appendCommentLine],
|
|
["DEFCLASS", "DEFCLASS_BODY",
|
|
self.double_comment_re.search, self.appendCommentLine],
|
|
|
|
# multiline comments
|
|
["DEFCLASS", "COMMENT_SINGLE",
|
|
self.start_single_comment_re.search, self.appendCommentLine],
|
|
["COMMENT_SINGLE", "DEFCLASS_BODY",
|
|
self.end_single_comment_re.search, self.appendCommentLine],
|
|
["COMMENT_SINGLE", "COMMENT_SINGLE",
|
|
self.catchall, self.appendCommentLine],
|
|
["DEFCLASS", "COMMENT_DOUBLE",
|
|
self.start_double_comment_re.search, self.appendCommentLine],
|
|
["COMMENT_DOUBLE", "DEFCLASS_BODY",
|
|
self.end_double_comment_re.search, self.appendCommentLine],
|
|
["COMMENT_DOUBLE", "COMMENT_DOUBLE",
|
|
self.catchall, self.appendCommentLine],
|
|
|
|
# other lines
|
|
["DEFCLASS", "DEFCLASS", self.empty_re.search, self.appendDefclassLine],
|
|
["DEFCLASS", "DEFCLASS", self.defclass_re.search, self.resetCommentSearch],
|
|
["DEFCLASS", "DEFCLASS_MULTI",
|
|
self.multiline_defclass_start_re.search, self.resetCommentSearch],
|
|
["DEFCLASS", "DEFCLASS_BODY", self.catchall, self.stopCommentSearch],
|
|
|
|
# DEFCLASS_BODY
|
|
|
|
["DEFCLASS_BODY", "DEFCLASS",
|
|
self.defclass_re.search, self.startCommentSearch],
|
|
["DEFCLASS_BODY", "DEFCLASS_MULTI",
|
|
self.multiline_defclass_start_re.search, self.startCommentSearch],
|
|
["DEFCLASS_BODY", "DEFCLASS_BODY", self.catchall, self.appendNormalLine],
|
|
|
|
# DEFCLASS_MULTI
|
|
["DEFCLASS_MULTI", "DEFCLASS",
|
|
self.multiline_defclass_end_re.search, self.appendDefclassLine],
|
|
["DEFCLASS_MULTI", "DEFCLASS_MULTI",
|
|
self.catchall, self.appendDefclassLine],
|
|
]
|
|
|
|
self.fsm = FSM("FILEHEAD", transitions)
|
|
self.outstream = sys.stdout
|
|
|
|
self.output = []
|
|
self.comment = []
|
|
self.filehead = []
|
|
self.defclass = []
|
|
self.indent = ""
|
|
|
|
def __closeComment(self):
|
|
"""Appends any open comment block and triggering block to the output."""
|
|
|
|
if args.autobrief:
|
|
if len(self.comment) == 1 \
|
|
or (len(self.comment) > 2 and self.comment[1].strip() == ''):
|
|
self.comment[0] = self.__docstringSummaryToBrief(
|
|
self.comment[0])
|
|
|
|
if self.comment:
|
|
block = self.makeCommentBlock()
|
|
self.output.extend(block)
|
|
|
|
if self.defclass:
|
|
self.output.extend(self.defclass)
|
|
|
|
def __docstringSummaryToBrief(self, line):
|
|
"""Adds \\brief to the docstrings summary line.
|
|
|
|
A \\brief is prepended, provided no other doxygen command is at the
|
|
start of the line.
|
|
"""
|
|
stripped = line.strip()
|
|
if stripped and not stripped[0] in ('@', '\\'):
|
|
return "\\brief " + line
|
|
else:
|
|
return line
|
|
|
|
def __flushBuffer(self):
|
|
"""Flushes the current outputbuffer to the outstream."""
|
|
if self.output:
|
|
try:
|
|
if args.debug:
|
|
print("# OUTPUT: ", self.output, file=sys.stderr)
|
|
print("\n".join(self.output), file=self.outstream)
|
|
self.outstream.flush()
|
|
except IOError:
|
|
# Fix for FS#33. Catches "broken pipe" when doxygen closes
|
|
# stdout prematurely upon usage of INPUT_FILTER, INLINE_SOURCES
|
|
# and FILTER_SOURCE_FILES.
|
|
pass
|
|
self.output = []
|
|
|
|
def catchall(self, input):
|
|
"""The catchall-condition, always returns true."""
|
|
return True
|
|
|
|
def resetCommentSearch(self, match):
|
|
"""Restarts a new comment search for a different triggering line.
|
|
|
|
Closes the current commentblock and starts a new comment search.
|
|
"""
|
|
if args.debug:
|
|
print("# CALLBACK: resetCommentSearch", file=sys.stderr)
|
|
self.__closeComment()
|
|
self.startCommentSearch(match)
|
|
|
|
def startCommentSearch(self, match):
|
|
"""Starts a new comment search.
|
|
|
|
Saves the triggering line, resets the current comment and saves
|
|
the current indentation.
|
|
"""
|
|
if args.debug:
|
|
print("# CALLBACK: startCommentSearch", file=sys.stderr)
|
|
self.defclass = [self.fsm.current_input]
|
|
self.comment = []
|
|
self.indent = match.group(1)
|
|
|
|
def stopCommentSearch(self, match):
|
|
"""Stops a comment search.
|
|
|
|
Closes the current commentblock, resets the triggering line and
|
|
appends the current line to the output.
|
|
"""
|
|
if args.debug:
|
|
print("# CALLBACK: stopCommentSearch", file=sys.stderr)
|
|
self.__closeComment()
|
|
|
|
self.defclass = []
|
|
self.output.append(self.fsm.current_input)
|
|
|
|
def appendFileheadLine(self, match):
|
|
"""Appends a line in the FILEHEAD state.
|
|
|
|
Closes the open comment block, resets it and appends the current line.
|
|
"""
|
|
if args.debug:
|
|
print("# CALLBACK: appendFileheadLine", file=sys.stderr)
|
|
self.__closeComment()
|
|
self.comment = []
|
|
self.output.append(self.fsm.current_input)
|
|
|
|
def appendCommentLine(self, match):
|
|
"""Appends a comment line.
|
|
|
|
The comment delimiter is removed from multiline start and ends as
|
|
well as singleline comments.
|
|
"""
|
|
if args.debug:
|
|
print("# CALLBACK: appendCommentLine", file=sys.stderr)
|
|
(from_state, to_state, condition, callback) = self.fsm.current_transition
|
|
|
|
# single line comment
|
|
if (from_state == "DEFCLASS" and to_state == "DEFCLASS_BODY") \
|
|
or (from_state == "FILEHEAD" and to_state == "FILEHEAD"):
|
|
# remove comment delimiter from begin and end of the line
|
|
activeCommentDelim = match.group(1)
|
|
line = self.fsm.current_input
|
|
self.comment.append(line[line.find(
|
|
activeCommentDelim) + len(activeCommentDelim):line.rfind(activeCommentDelim)])
|
|
|
|
if (to_state == "DEFCLASS_BODY"):
|
|
self.__closeComment()
|
|
self.defclass = []
|
|
# multiline start
|
|
elif from_state == "DEFCLASS" or from_state == "FILEHEAD":
|
|
# remove comment delimiter from begin of the line
|
|
activeCommentDelim = match.group(1)
|
|
line = self.fsm.current_input
|
|
self.comment.append(
|
|
line[line.find(activeCommentDelim) + len(activeCommentDelim):])
|
|
# multiline end
|
|
elif to_state == "DEFCLASS_BODY" or to_state == "FILEHEAD":
|
|
# remove comment delimiter from end of the line
|
|
activeCommentDelim = match.group(1)
|
|
line = self.fsm.current_input
|
|
self.comment.append(line[0:line.rfind(activeCommentDelim)])
|
|
if (to_state == "DEFCLASS_BODY"):
|
|
self.__closeComment()
|
|
self.defclass = []
|
|
# in multiline comment
|
|
else:
|
|
# just append the comment line
|
|
self.comment.append(self.fsm.current_input)
|
|
|
|
def appendNormalLine(self, match):
|
|
"""Appends a line to the output."""
|
|
if args.debug:
|
|
print("# CALLBACK: appendNormalLine", file=sys.stderr)
|
|
self.output.append(self.fsm.current_input)
|
|
|
|
def appendDefclassLine(self, match):
|
|
"""Appends a line to the triggering block."""
|
|
if args.debug:
|
|
print("# CALLBACK: appendDefclassLine", file=sys.stderr)
|
|
self.defclass.append(self.fsm.current_input)
|
|
|
|
def makeCommentBlock(self):
|
|
"""Indents the current comment block with respect to the current
|
|
indentation level.
|
|
|
|
@returns a list of indented comment lines
|
|
"""
|
|
doxyStart = "##"
|
|
commentLines = self.comment
|
|
|
|
commentLines = ["%s# %s" % (self.indent, x) for x in commentLines]
|
|
l = [self.indent + doxyStart]
|
|
l.extend(commentLines)
|
|
|
|
return l
|
|
|
|
def parse(self, input):
|
|
"""Parses a python file given as input string and returns the doxygen-
|
|
compatible representation.
|
|
|
|
@param input the python code to parse
|
|
@returns the modified python code
|
|
"""
|
|
lines = input.split("\n")
|
|
|
|
for line in lines:
|
|
self.fsm.makeTransition(line)
|
|
|
|
if self.fsm.current_state == "DEFCLASS":
|
|
self.__closeComment()
|
|
|
|
return "\n".join(self.output)
|
|
|
|
def parseFile(self, filename):
|
|
"""Parses a python file given as input string and returns the doxygen-
|
|
compatible representation.
|
|
|
|
@param input the python code to parse
|
|
@returns the modified python code
|
|
"""
|
|
f = open(filename, 'r')
|
|
|
|
for line in f:
|
|
self.parseLine(line.rstrip('\r\n'))
|
|
if self.fsm.current_state == "DEFCLASS":
|
|
self.__closeComment()
|
|
self.__flushBuffer()
|
|
f.close()
|
|
|
|
def parseLine(self, line):
|
|
"""Parse one line of python and flush the resulting output to the
|
|
outstream.
|
|
|
|
@param line the python code line to parse
|
|
"""
|
|
self.fsm.makeTransition(line)
|
|
self.__flushBuffer()
|
|
|
|
|
|
def argParse():
|
|
"""Parses commandline args."""
|
|
parser = ArgumentParser(prog=__applicationName__)
|
|
|
|
parser.add_argument("--version", action="version",
|
|
version="%(prog)s " + __version__
|
|
)
|
|
parser.add_argument("--autobrief", action="store_true",
|
|
help="use the docstring summary line as \\brief description"
|
|
)
|
|
parser.add_argument("--debug", action="store_true",
|
|
help="enable debug output on stderr"
|
|
)
|
|
parser.add_argument("filename", metavar="FILENAME")
|
|
|
|
return parser.parse_args()
|
|
|
|
|
|
def main():
|
|
"""Starts the parser on the file given by the filename as the first
|
|
argument on the commandline.
|
|
"""
|
|
global args
|
|
args = argParse()
|
|
fsm = Doxypy()
|
|
fsm.parseFile(args.filename)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|