"""
Modification to doxypy.py
(which you can found here: http://code.foosel.org/doxypy).
The main tweaks are:
- the documentation is not moved before the function or class definition
- it uses a function to modify every line of documentation to use rst syntax.
:githublink:`%|py|11`
"""
from __future__ import print_function
import sys
import re
from argparse import ArgumentParser as OptionParser
_allowed = re.compile("^([a-zA-Z]:)?[^:*?\"<>|]+$")
[docs]def is_file_string(s):
"""
Says if the string s could be a filename.
:param s: string
:return: boolean
:githublink:`%|py|26`
"""
if len(s) >= 3000:
return False
global _allowed
if not _allowed.search(s):
return False
for c in s:
if ord(c) < 32:
return False
return True
__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__ = "14th October 2009"
__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 `GNU <http://www.gnu.org/licenses/>`_.
"""
[docs]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.
.. list-table::
:widths: auto
:header-rows: 1
* - attribute
- meaning
* - transitions
- holds the transitions
* - current_state
- holds the current state
* - current_input
- holds the current input
* - current_transition
- hold the currently active transition
:githublink:`%|py|93`
"""
[docs] def __init__(self, start_state=None, transitions=None):
if transitions is 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])
[docs] def makeTransition(self, input):
"""
Makes a transition based on the given input.
:param input: input to parse by the FSM
:githublink:`%|py|114`
"""
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 options.debug: # pragma: no cover
sys.stderr.write(
"# FSM: executing ({} -> {}) for line '{}'\n".format(from_state, to_state, input))
callback(match)
return
class Doxypy(object):
def __init__(self, print_output, process_comment, information):
"""
Constructor for Doxypy.
:param print_output: function which will receive the output
:param process_comment: function applied to the help to modify it
:param information: a dictionary with additional information such
as filename, first_row
:githublink:`%|py|140`
"""
string_prefixes = "[uU]?[rR]?"
self.start_single_comment_re = re.compile(
"^\\s*%s(''')" % string_prefixes)
self.end_single_comment_re = re.compile("(''')\\s*$")
self.start_double_comment_re = re.compile(
"^\\s*%s(\"\"\")" % string_prefixes)
self.end_double_comment_re = re.compile("(\"\"\")\\s*$")
self.single_comment_re = re.compile(
"^\\s*%s(''').*(''')\\s*$" % string_prefixes)
self.double_comment_re = re.compile(
"^\\s*%s(\"\"\").*(\"\"\")\\s*$" % string_prefixes)
self.defclass_re = re.compile(
"^(\\s*)(def .+:|class .+:)\\s*([#].*?)?$")
self.empty_re = re.compile("^\\s*$")
self.hashline_re = re.compile("^\\s*#.*$")
self.importline_re = re.compile("^\\s*(import |from .+ import)")
self.multiline_defclass_start_re = re.compile(
"^(\\s*)(def|class)(\\s.*)?$")
self.multiline_defclass_end_re = re.compile(":\\s*([#].*?)?$")
self.print_output = print_output
self.process_comment = process_comment
self.information = information
# 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.
:githublink:`%|py|268`
"""
if options.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.defclass:
self.output.extend(self.defclass)
if self.comment:
block = self.makeCommentBlock()
self.output.extend(block)
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.
:githublink:`%|py|289`
"""
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.
:githublink:`%|py|299`
"""
if self.output:
if options.debug: # pragma: no cover
sys.stderr.write("# OUTPUT: {0}\n".format(self.output))
self.print_output("\n".join(self.output), file=self.outstream)
self.outstream.flush()
self.output = []
def catchall(self, input):
"""
The catchall-condition, always returns true.
:githublink:`%|py|310`
"""
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.
:githublink:`%|py|318`
"""
if options.debug: # pragma: no cover
sys.stderr.write("# CALLBACK: resetCommentSearch")
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.
:githublink:`%|py|330`
"""
if options.debug: # pragma: no cover
sys.stderr.write("# CALLBACK: startCommentSearch")
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.
:githublink:`%|py|343`
"""
if options.debug: # pragma: no cover
sys.stderr.write("# CALLBACK: stopCommentSearch")
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.
:githublink:`%|py|356`
"""
if options.debug: # pragma: no cover
sys.stderr.write("# CALLBACK: appendFileheadLine")
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.
:githublink:`%|py|369`
"""
if options.debug: # pragma: no cover
sys.stderr.write("# CALLBACK: appendCommentLine")
from_state, to_state, condition, callback = self.fsm.current_transition # pylint: disable=W0612
# 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 in ("DEFCLASS", "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 in ("DEFCLASS_BODY", "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.
:githublink:`%|py|410`
"""
if options.debug: # pragma: no cover
self.print_output("# CALLBACK: appendNormalLine", file=sys.stderr)
self.output.append(self.fsm.current_input)
def appendDefclassLine(self, match):
"""
Appends a line to the triggering block.
:githublink:`%|py|418`
"""
if options.debug: # pragma: no cover
self.print_output(
"# 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.
:return:s a list of indented comment lines
:githublink:`%|py|430`
"""
if options.debug: # pragma: no cover
self.print_output("# makeCommentBlock", file=sys.stderr)
indent4 = " " if len(self.defclass) > 0 else ""
doxyStart = "%s\"\"\"" % indent4
doxyEnd = "%s\"\"\"" % indent4
commentLines = self.comment
full_indent = self.indent + indent4
if full_indent:
if commentLines and commentLines[0].lstrip() == commentLines[0]:
commentLines[0] = full_indent + commentLines[0]
# commentLines = ["%s%s%s" % (self.indent, indent4, x) for x in commentLines]
commentLines = self.process_comment(commentLines,
self.information.get(
"first_row", 0) + self._index_row + 1,
self.information.get("filename", "filename is not present"))
# We remove the indentation.
while commentLines and commentLines[0].strip() == '':
del commentLines[0]
while commentLines and commentLines[-1].strip() == '':
del commentLines[-1]
# Back to doxypy.
li = [self.indent + doxyStart]
li.extend(commentLines)
li.append(self.indent + doxyEnd)
return li
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
:return:s the modified python code
:githublink:`%|py|469`
"""
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 :epkg:`python` file given as input string and returns`
the :epkg:`doxygen` compatible representation.
:param filename: the :epkg:`python` code to parse (filename)
:githublink:`%|py|486`
"""
self._index_row = 0
if isinstance(filename, list):
for line in filename:
self.parseLine(line.rstrip('\r\n'))
self._index_row += 1
else:
import os
if is_file_string(filename) and os.path.exists(filename):
with open(filename, 'r') as fh:
for line in fh:
self.parseLine(line.rstrip('\r\n'))
self._index_row += 1
else:
for line in filename.split("\n"):
self.parseLine(line.rstrip('\r\n'))
self._index_row += 1
if self.fsm.current_state == "DEFCLASS":
self.__closeComment()
self.__flushBuffer()
def parseLine(self, line):
"""
Parses one line of python and flush the resulting output to the
outstream.
:param line: the python code line to parse
:githublink:`%|py|514`
"""
self.fsm.makeTransition(line)
self.__flushBuffer()
[docs]def optParse():
"""
Parses commandline options.
:githublink:`%|py|522`
"""
parser = OptionParser(prog=__applicationName__)
# parser.set_usage("%prog [options] filename")
parser.add_argument("--autobrief",
action="store_true", dest="autobrief",
help="use the docstring summary line as @brief description",
version="%prog " + __version__
)
parser.add_argument("--debug",
action="store_true", dest="debug",
help="enable debug output on stderr",
version="%prog " + __version__
)
# parse options
global options
(options, filename) = parser.parse_args()
if not filename:
sys.stderr.write("No filename given.")
sys.exit(-1)
return filename[0]
[docs]def main(file=None, print_output=None):
"""
Starts the parser on the file given by the filename as the first
argument on the commandline.
:param file: if equal to None, take this one on the command line
:param print_output: every string is sent to that funtion
:githublink:`%|py|555`
"""
filename = file if file is not None else optParse()
fsm = Doxypy(print_output, lambda a, b, c: a, {})
fsm.parseFile(filename)
class Opt:
def __init__(self):
self.debug = False
self.autobrief = True
options = Opt()
[docs]def process_string(content, print_output, process_comment, filename, first_row, debug=False):
"""
Applies the doxypy like process to a string.
:param content: string
:param print_output: every string is sent to that funtion
:param process_comment: function applied to the help to modifies it
:param filename: or None if there is no file
:param first_row: True: to display error messages
:param debug: if True, display more information
:githublink:`%|py|581`
"""
options.debug = debug
fsm = Doxypy(print_output, process_comment,
{"filename": filename, "first_row": first_row})
fsm.parseFile(content)
if __name__ == "__main__":
with open(__file__, "r") as f:
local_content = f.read()
# main(__file__, print_output = print)
def pprint(*la, **kw):
print(*la, **kw)
process_string(local_content, pprint, lambda a, b, c: a, "", 0)