Source code for pyquickhelper.helpgen._my_doxypy

"""
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)