| Index: doxypypy/doxypypy/doxypypy.py
|
| diff --git a/doxypypy/doxypypy/doxypypy.py b/doxypypy/doxypypy/doxypypy.py
|
| new file mode 100755
|
| index 0000000000000000000000000000000000000000..25e9ae77939c53dea57f82fc724a53d5fc8c58f7
|
| --- /dev/null
|
| +++ b/doxypypy/doxypypy/doxypypy.py
|
| @@ -0,0 +1,849 @@
|
| +#!/usr/bin/env python
|
| +# -*- coding: utf-8 -*-
|
| +"""
|
| +Filters Python code for use with Doxygen, using a syntax-aware approach.
|
| +
|
| +Rather than implementing a partial Python parser with regular expressions, this
|
| +script uses Python's own abstract syntax tree walker to isolate meaningful
|
| +constructs. It passes along namespace information so Doxygen can construct a
|
| +proper tree for nested functions, classes, and methods. It understands bed lump
|
| +variables are by convention private. It groks Zope-style Python interfaces.
|
| +It can automatically turn PEP 257 compliant that follow the more restrictive
|
| +Google style guide into appropriate Doxygen tags, and is even aware of
|
| +doctests.
|
| +"""
|
| +
|
| +from ast import NodeVisitor, parse, iter_fields, AST, Name, get_docstring
|
| +from re import compile as regexpCompile, IGNORECASE, MULTILINE
|
| +from types import GeneratorType
|
| +from sys import stderr
|
| +from os import linesep
|
| +from string import whitespace
|
| +from codeop import compile_command
|
| +
|
| +
|
| +def coroutine(func):
|
| + """Basic decorator to implement the coroutine pattern."""
|
| + def __start(*args, **kwargs):
|
| + """Automatically calls next() on the internal generator function."""
|
| + __cr = func(*args, **kwargs)
|
| + next(__cr)
|
| + return __cr
|
| + return __start
|
| +
|
| +
|
| +class AstWalker(NodeVisitor):
|
| + """
|
| + A walker that'll recursively progress through an AST.
|
| +
|
| + Given an abstract syntax tree for Python code, walk through all the
|
| + nodes looking for significant types (for our purposes we only care
|
| + about module starts, class definitions, function definitions, variable
|
| + assignments, and function calls, as all the information we want to pass
|
| + to Doxygen is found within these constructs). If the autobrief option
|
| + is set, it further attempts to parse docstrings to create appropriate
|
| + Doxygen tags.
|
| + """
|
| +
|
| + # We have a number of regular expressions that we use. They don't
|
| + # vary across instances and so are compiled directly in the class
|
| + # definition.
|
| + __indentRE = regexpCompile(r'^(\s*)\S')
|
| + __newlineRE = regexpCompile(r'^#', MULTILINE)
|
| + __blanklineRE = regexpCompile(r'^\s*$')
|
| + __docstrMarkerRE = regexpCompile(r"\s*([uUbB]*[rR]?(['\"]{3}))")
|
| + __docstrOneLineRE = regexpCompile(r"\s*[uUbB]*[rR]?(['\"]{3})(.+)\1")
|
| +
|
| + __implementsRE = regexpCompile(r"^(\s*)(?:zope\.)?(?:interface\.)?"
|
| + r"(?:module|class|directly)?"
|
| + r"(?:Provides|Implements)\(\s*(.+)\s*\)",
|
| + IGNORECASE)
|
| + __interfaceRE = regexpCompile(r"^\s*class\s+(\S+)\s*\(\s*(?:zope\.)?"
|
| + r"(?:interface\.)?"
|
| + r"Interface\s*\)\s*:", IGNORECASE)
|
| + __attributeRE = regexpCompile(r"^(\s*)(\S+)\s*=\s*(?:zope\.)?"
|
| + r"(?:interface\.)?"
|
| + r"Attribute\s*\(['\"]{1,3}(.*)['\"]{1,3}\)",
|
| + IGNORECASE)
|
| +
|
| + __singleLineREs = {
|
| + ' @author: ': regexpCompile(r"^(\s*Authors?:\s*)(.*)$", IGNORECASE),
|
| + ' @copyright ': regexpCompile(r"^(\s*Copyright:\s*)(.*)$", IGNORECASE),
|
| + ' @date ': regexpCompile(r"^(\s*Date:\s*)(.*)$", IGNORECASE),
|
| + ' @file ': regexpCompile(r"^(\s*File:\s*)(.*)$", IGNORECASE),
|
| + ' @version: ': regexpCompile(r"^(\s*Version:\s*)(.*)$", IGNORECASE),
|
| + ' @note ': regexpCompile(r"^(\s*Note:\s*)(.*)$", IGNORECASE),
|
| + ' @warning ': regexpCompile(r"^(\s*Warning:\s*)(.*)$", IGNORECASE)
|
| + }
|
| + __argsStartRE = regexpCompile(r"^(\s*(?:(?:Keyword\s+)?"
|
| + r"(?:A|Kwa)rg(?:ument)?|Attribute)s?"
|
| + r"\s*:\s*)$", IGNORECASE)
|
| + __argsRE = regexpCompile(r"^\s*(?P<name>\w+)\s*(?P<type>\(?\S*\)?)?\s*"
|
| + r"(?:-|:)+\s+(?P<desc>.+)$")
|
| + __returnsStartRE = regexpCompile(r"^\s*(?:Return|Yield)s:\s*$", IGNORECASE)
|
| + __raisesStartRE = regexpCompile(r"^\s*(Raises|Exceptions|See Also):\s*$",
|
| + IGNORECASE)
|
| + __listRE = regexpCompile(r"^\s*(([\w\.]+),\s*)+(&|and)?\s*([\w\.]+)$")
|
| + __singleListItemRE = regexpCompile(r'^\s*([\w\.]+)\s*$')
|
| + __listItemRE = regexpCompile(r'([\w\.]+),?\s*')
|
| + __examplesStartRE = regexpCompile(r"^\s*(?:Example|Doctest)s?:\s*$",
|
| + IGNORECASE)
|
| + __sectionStartRE = regexpCompile(r"^\s*(([A-Z]\w* ?){1,2}):\s*$")
|
| + # The error line should match traceback lines, error exception lines, and
|
| + # (due to a weird behavior of codeop) single word lines.
|
| + __errorLineRE = regexpCompile(r"^\s*((?:\S+Error|Traceback.*):?\s*(.*)|@?[\w.]+)\s*$",
|
| + IGNORECASE)
|
| +
|
| + def __init__(self, lines, options, inFilename):
|
| + """Initialize a few class variables in preparation for our walk."""
|
| + self.lines = lines
|
| + self.options = options
|
| + self.inFilename = inFilename
|
| + self.docLines = []
|
| +
|
| + @staticmethod
|
| + def _stripOutAnds(inStr):
|
| + """Takes a string and returns the same without ands or ampersands."""
|
| + assert isinstance(inStr, str)
|
| + return inStr.replace(' and ', ' ').replace(' & ', ' ')
|
| +
|
| + @staticmethod
|
| + def _endCodeIfNeeded(line, inCodeBlock):
|
| + """Simple routine to append end code marker if needed."""
|
| + assert isinstance(line, str)
|
| + if inCodeBlock:
|
| + line = '# @endcode{0}{1}'.format(linesep, line.rstrip())
|
| + inCodeBlock = False
|
| + return line, inCodeBlock
|
| +
|
| + @coroutine
|
| + def _checkIfCode(self, inCodeBlock):
|
| + """Checks whether or not a given line appears to be Python code."""
|
| + while True:
|
| + line, lines, lineNum = (yield)
|
| + testLineNum = 1
|
| + currentLineNum = 0
|
| + testLine = line.strip()
|
| + lineOfCode = None
|
| + while lineOfCode is None:
|
| + match = AstWalker.__errorLineRE.match(testLine)
|
| + if not testLine or testLine == '...' or match:
|
| + # These are ambiguous.
|
| + line, lines, lineNum = (yield)
|
| + testLine = line.strip()
|
| + #testLineNum = 1
|
| + elif testLine.startswith('>>> '):
|
| + # This is definitely code.
|
| + lineOfCode = True
|
| + else:
|
| + try:
|
| + compLine = compile_command(testLine)
|
| + if compLine and lines[currentLineNum].strip().startswith('#'):
|
| + lineOfCode = True
|
| + else:
|
| + line, lines, lineNum = (yield)
|
| + line = line.strip()
|
| + if line.startswith('>>> '):
|
| + # Definitely code, don't compile further.
|
| + lineOfCode = True
|
| + else:
|
| + testLine += linesep + line
|
| + testLine = testLine.strip()
|
| + testLineNum += 1
|
| + except (SyntaxError, RuntimeError):
|
| + # This is definitely not code.
|
| + lineOfCode = False
|
| + except Exception:
|
| + # Other errors are ambiguous.
|
| + line, lines, lineNum = (yield)
|
| + testLine = line.strip()
|
| + #testLineNum = 1
|
| + currentLineNum = lineNum - testLineNum
|
| + if not inCodeBlock and lineOfCode:
|
| + inCodeBlock = True
|
| + lines[currentLineNum] = '{0}{1}# @code{1}'.format(
|
| + lines[currentLineNum],
|
| + linesep
|
| + )
|
| + elif inCodeBlock and lineOfCode is False:
|
| + # None is ambiguous, so strict checking
|
| + # against False is necessary.
|
| + inCodeBlock = False
|
| + lines[currentLineNum] = '{0}{1}# @endcode{1}'.format(
|
| + lines[currentLineNum],
|
| + linesep
|
| + )
|
| +
|
| + @coroutine
|
| + def __alterDocstring(self, tail='', writer=None):
|
| + """
|
| + Runs eternally, processing docstring lines.
|
| +
|
| + Parses docstring lines as they get fed in via send, applies appropriate
|
| + Doxygen tags, and passes them along in batches for writing.
|
| + """
|
| + assert isinstance(tail, str) and isinstance(writer, GeneratorType)
|
| +
|
| + lines = []
|
| + timeToSend = False
|
| + inCodeBlock = False
|
| + inSection = False
|
| + prefix = ''
|
| + firstLineNum = -1
|
| + sectionHeadingIndent = 0
|
| + codeChecker = self._checkIfCode(False)
|
| + proseChecker = self._checkIfCode(True)
|
| + while True:
|
| + lineNum, line = (yield)
|
| + if firstLineNum < 0:
|
| + firstLineNum = lineNum
|
| + # Don't bother doing extra work if it's a sentinel.
|
| + if line is not None:
|
| + # Also limit work if we're not parsing the docstring.
|
| + if self.options.autobrief:
|
| + for doxyTag, tagRE in AstWalker.__singleLineREs.items():
|
| + match = tagRE.search(line)
|
| + if match:
|
| + # We've got a simple one-line Doxygen command
|
| + lines[-1], inCodeBlock = self._endCodeIfNeeded(
|
| + lines[-1], inCodeBlock)
|
| + writer.send((firstLineNum, lineNum - 1, lines))
|
| + lines = []
|
| + firstLineNum = lineNum
|
| + line = line.replace(match.group(1), doxyTag)
|
| + timeToSend = True
|
| +
|
| + if inSection:
|
| + # The last line belonged to a section.
|
| + # Does this one too? (Ignoring empty lines.)
|
| + match = AstWalker.__blanklineRE.match(line)
|
| + if not match:
|
| + indent = len(line.expandtabs(self.options.tablength)) - \
|
| + len(line.expandtabs(self.options.tablength).lstrip())
|
| + if indent <= sectionHeadingIndent:
|
| + inSection = False
|
| + else:
|
| + if lines[-1] == '#':
|
| + # If the last line was empty, but we're still in a section
|
| + # then we need to start a new paragraph.
|
| + lines[-1] = '# @par'
|
| +
|
| + match = AstWalker.__returnsStartRE.match(line)
|
| + if match:
|
| + # We've got a "returns" section
|
| + line = line.replace(match.group(0), ' @return\t').rstrip()
|
| + prefix = '@return\t'
|
| + else:
|
| + match = AstWalker.__argsStartRE.match(line)
|
| + if match:
|
| + # We've got an "arguments" section
|
| + line = line.replace(match.group(0), '').rstrip()
|
| + if 'attr' in match.group(0).lower():
|
| + prefix = '@property\t'
|
| + else:
|
| + prefix = '@param\t'
|
| + lines[-1], inCodeBlock = self._endCodeIfNeeded(
|
| + lines[-1], inCodeBlock)
|
| + lines.append('#' + line)
|
| + continue
|
| + else:
|
| + match = AstWalker.__argsRE.match(line)
|
| + if match and not inCodeBlock:
|
| + # We've got something that looks like an item /
|
| + # description pair.
|
| + if 'property' in prefix:
|
| + line = '# {0}\t{1[name]}{2}# {1[desc]}'.format(
|
| + prefix, match.groupdict(), linesep)
|
| + else:
|
| + line = ' {0}\t{1[name]}\t{1[desc]}'.format(
|
| + prefix, match.groupdict())
|
| + else:
|
| + match = AstWalker.__raisesStartRE.match(line)
|
| + if match:
|
| + line = line.replace(match.group(0), '').rstrip()
|
| + if 'see' in match.group(1).lower():
|
| + # We've got a "see also" section
|
| + prefix = '@sa\t'
|
| + else:
|
| + # We've got an "exceptions" section
|
| + prefix = '@exception\t'
|
| + lines[-1], inCodeBlock = self._endCodeIfNeeded(
|
| + lines[-1], inCodeBlock)
|
| + lines.append('#' + line)
|
| + continue
|
| + else:
|
| + match = AstWalker.__listRE.match(line)
|
| + if match and not inCodeBlock:
|
| + # We've got a list of something or another
|
| + itemList = []
|
| + for itemMatch in AstWalker.__listItemRE.findall(self._stripOutAnds(
|
| + match.group(0))):
|
| + itemList.append('# {0}\t{1}{2}'.format(
|
| + prefix, itemMatch, linesep))
|
| + line = ''.join(itemList)[1:]
|
| + else:
|
| + match = AstWalker.__examplesStartRE.match(line)
|
| + if match and lines[-1].strip() == '#' \
|
| + and self.options.autocode:
|
| + # We've got an "example" section
|
| + inCodeBlock = True
|
| + line = line.replace(match.group(0),
|
| + ' @b Examples{0}# @code'.format(linesep))
|
| + else:
|
| + match = AstWalker.__sectionStartRE.match(line)
|
| + if match:
|
| + # We've got an arbitrary section
|
| + prefix = ''
|
| + inSection = True
|
| + # What's the indentation of the section heading?
|
| + sectionHeadingIndent = len(line.expandtabs(self.options.tablength)) \
|
| + - len(line.expandtabs(self.options.tablength).lstrip())
|
| + line = line.replace(
|
| + match.group(0),
|
| + ' @par {0}'.format(match.group(1))
|
| + )
|
| + if lines[-1] == '# @par':
|
| + lines[-1] = '#'
|
| + lines[-1], inCodeBlock = self._endCodeIfNeeded(
|
| + lines[-1], inCodeBlock)
|
| + lines.append('#' + line)
|
| + continue
|
| + elif prefix:
|
| + match = AstWalker.__singleListItemRE.match(line)
|
| + if match and not inCodeBlock:
|
| + # Probably a single list item
|
| + line = ' {0}\t{1}'.format(
|
| + prefix, match.group(0))
|
| + elif self.options.autocode and inCodeBlock:
|
| + proseChecker.send(
|
| + (
|
| + line, lines,
|
| + lineNum - firstLineNum
|
| + )
|
| + )
|
| + elif self.options.autocode:
|
| + codeChecker.send(
|
| + (
|
| + line, lines,
|
| + lineNum - firstLineNum
|
| + )
|
| + )
|
| +
|
| + # If we were passed a tail, append it to the docstring.
|
| + # Note that this means that we need a docstring for this
|
| + # item to get documented.
|
| + if tail and lineNum == len(self.docLines) - 1:
|
| + line = '{0}{1}# {2}'.format(line.rstrip(), linesep, tail)
|
| +
|
| + # Add comment marker for every line.
|
| + line = '#{0}'.format(line.rstrip())
|
| + # Ensure the first line has the Doxygen double comment.
|
| + if lineNum == 0:
|
| + line = '#' + line
|
| +
|
| + lines.append(line.replace(' ' + linesep, linesep))
|
| + else:
|
| + # If we get our sentinel value, send out what we've got.
|
| + timeToSend = True
|
| +
|
| + if timeToSend:
|
| + lines[-1], inCodeBlock = self._endCodeIfNeeded(lines[-1],
|
| + inCodeBlock)
|
| + writer.send((firstLineNum, lineNum, lines))
|
| + lines = []
|
| + firstLineNum = -1
|
| + timeToSend = False
|
| +
|
| + @coroutine
|
| + def __writeDocstring(self):
|
| + """
|
| + Runs eternally, dumping out docstring line batches as they get fed in.
|
| +
|
| + Replaces original batches of docstring lines with modified versions
|
| + fed in via send.
|
| + """
|
| + while True:
|
| + firstLineNum, lastLineNum, lines = (yield)
|
| + newDocstringLen = lastLineNum - firstLineNum + 1
|
| + while len(lines) < newDocstringLen:
|
| + lines.append('')
|
| + # Substitute the new block of lines for the original block of lines.
|
| + self.docLines[firstLineNum: lastLineNum + 1] = lines
|
| +
|
| + def _processDocstring(self, node, tail='', **kwargs):
|
| + """
|
| + Handles a docstring for functions, classes, and modules.
|
| +
|
| + Basically just figures out the bounds of the docstring and sends it
|
| + off to the parser to do the actual work.
|
| + """
|
| + typeName = type(node).__name__
|
| + # Modules don't have lineno defined, but it's always 0 for them.
|
| + curLineNum = startLineNum = 0
|
| + if typeName != 'Module':
|
| + startLineNum = curLineNum = node.lineno - 1
|
| + # Figure out where both our enclosing object and our docstring start.
|
| + line = ''
|
| + while curLineNum < len(self.lines):
|
| + line = self.lines[curLineNum]
|
| + match = AstWalker.__docstrMarkerRE.match(line)
|
| + if match:
|
| + break
|
| + curLineNum += 1
|
| + docstringStart = curLineNum
|
| + # Figure out where our docstring ends.
|
| + if not AstWalker.__docstrOneLineRE.match(line):
|
| + # Skip for the special case of a single-line docstring.
|
| + curLineNum += 1
|
| + while curLineNum < len(self.lines):
|
| + line = self.lines[curLineNum]
|
| + if line.find(match.group(2)) >= 0:
|
| + break
|
| + curLineNum += 1
|
| + endLineNum = curLineNum + 1
|
| +
|
| + # Isolate our enclosing object's declaration.
|
| + defLines = self.lines[startLineNum: docstringStart]
|
| + # Isolate our docstring.
|
| + self.docLines = self.lines[docstringStart: endLineNum]
|
| +
|
| + # If we have a docstring, extract information from it.
|
| + if self.docLines:
|
| + # Get rid of the docstring delineators.
|
| + self.docLines[0] = AstWalker.__docstrMarkerRE.sub('',
|
| + self.docLines[0])
|
| + self.docLines[-1] = AstWalker.__docstrMarkerRE.sub('',
|
| + self.docLines[-1])
|
| + # Handle special strings within the docstring.
|
| + docstringConverter = self.__alterDocstring(
|
| + tail, self.__writeDocstring())
|
| + for lineInfo in enumerate(self.docLines):
|
| + docstringConverter.send(lineInfo)
|
| + docstringConverter.send((len(self.docLines) - 1, None))
|
| +
|
| + # Add a Doxygen @brief tag to any single-line description.
|
| + if self.options.autobrief:
|
| + safetyCounter = 0
|
| + while len(self.docLines) > 0 and self.docLines[0].lstrip('#').strip() == '':
|
| + del self.docLines[0]
|
| + self.docLines.append('')
|
| + safetyCounter += 1
|
| + if safetyCounter >= len(self.docLines):
|
| + # Escape the effectively empty docstring.
|
| + break
|
| + if len(self.docLines) == 1 or (len(self.docLines) >= 2 and (
|
| + self.docLines[1].strip(whitespace + '#') == '' or
|
| + self.docLines[1].strip(whitespace + '#').startswith('@'))):
|
| + self.docLines[0] = "## @brief {0}".format(self.docLines[0].lstrip('#'))
|
| + if len(self.docLines) > 1 and self.docLines[1] == '# @par':
|
| + self.docLines[1] = '#'
|
| +
|
| + if defLines:
|
| + match = AstWalker.__indentRE.match(defLines[0])
|
| + indentStr = match and match.group(1) or ''
|
| + self.docLines = [AstWalker.__newlineRE.sub(indentStr + '#', docLine)
|
| + for docLine in self.docLines]
|
| +
|
| + # Taking away a docstring from an interface method definition sometimes
|
| + # leaves broken code as the docstring may be the only code in it.
|
| + # Here we manually insert a pass statement to rectify this problem.
|
| + if typeName != 'Module':
|
| + if docstringStart < len(self.lines):
|
| + match = AstWalker.__indentRE.match(self.lines[docstringStart])
|
| + indentStr = match and match.group(1) or ''
|
| + else:
|
| + indentStr = ''
|
| + containingNodes = kwargs.get('containingNodes', []) or []
|
| + fullPathNamespace = self._getFullPathName(containingNodes)
|
| + parentType = fullPathNamespace[-2][1]
|
| + if parentType == 'interface' and typeName == 'FunctionDef' \
|
| + or fullPathNamespace[-1][1] == 'interface':
|
| + defLines[-1] = '{0}{1}{2}pass'.format(defLines[-1],
|
| + linesep, indentStr)
|
| + elif self.options.autobrief and typeName == 'ClassDef':
|
| + # If we're parsing docstrings separate out class attribute
|
| + # definitions to get better Doxygen output.
|
| + for firstVarLineNum, firstVarLine in enumerate(self.docLines):
|
| + if '@property\t' in firstVarLine:
|
| + break
|
| + lastVarLineNum = len(self.docLines)
|
| + if '@property\t' in firstVarLine:
|
| + while lastVarLineNum > firstVarLineNum:
|
| + lastVarLineNum -= 1
|
| + if '@property\t' in self.docLines[lastVarLineNum]:
|
| + break
|
| + lastVarLineNum += 1
|
| + if firstVarLineNum < len(self.docLines):
|
| + indentLineNum = endLineNum
|
| + indentStr = ''
|
| + while not indentStr and indentLineNum < len(self.lines):
|
| + match = AstWalker.__indentRE.match(self.lines[indentLineNum])
|
| + indentStr = match and match.group(1) or ''
|
| + indentLineNum += 1
|
| + varLines = ['{0}{1}'.format(linesep, docLine).replace(
|
| + linesep, linesep + indentStr)
|
| + for docLine in self.docLines[
|
| + firstVarLineNum: lastVarLineNum]]
|
| + defLines.extend(varLines)
|
| + self.docLines[firstVarLineNum: lastVarLineNum] = []
|
| + # After the property shuffling we will need to relocate
|
| + # any existing namespace information.
|
| + namespaceLoc = defLines[-1].find('\n# @namespace')
|
| + if namespaceLoc >= 0:
|
| + self.docLines[-1] += defLines[-1][namespaceLoc:]
|
| + defLines[-1] = defLines[-1][:namespaceLoc]
|
| +
|
| + # For classes and functions, apply our changes and reverse the
|
| + # order of the declaration and docstring, and for modules just
|
| + # apply our changes.
|
| + if typeName != 'Module':
|
| + self.lines[startLineNum: endLineNum] = self.docLines + defLines
|
| + else:
|
| + self.lines[startLineNum: endLineNum] = defLines + self.docLines
|
| +
|
| + @staticmethod
|
| + def _checkMemberName(name):
|
| + """
|
| + See if a member name indicates that it should be private.
|
| +
|
| + Private variables in Python (starting with a double underscore but
|
| + not ending in a double underscore) and bed lumps (variables that
|
| + are not really private but are by common convention treated as
|
| + protected because they begin with a single underscore) get Doxygen
|
| + tags labeling them appropriately.
|
| + """
|
| + assert isinstance(name, str)
|
| + restrictionLevel = None
|
| + if not name.endswith('__'):
|
| + if name.startswith('__'):
|
| + restrictionLevel = 'private'
|
| + elif name.startswith('_'):
|
| + restrictionLevel = 'protected'
|
| + return restrictionLevel
|
| +
|
| + def _processMembers(self, node, contextTag):
|
| + """
|
| + Mark up members if they should be private.
|
| +
|
| + If the name indicates it should be private or protected, apply
|
| + the appropriate Doxygen tags.
|
| + """
|
| + restrictionLevel = self._checkMemberName(node.name)
|
| + if restrictionLevel:
|
| + workTag = '{0}{1}# @{2}'.format(contextTag,
|
| + linesep,
|
| + restrictionLevel)
|
| + else:
|
| + workTag = contextTag
|
| + return workTag
|
| +
|
| + def generic_visit(self, node, **kwargs):
|
| + """
|
| + Extract useful information from relevant nodes including docstrings.
|
| +
|
| + This is virtually identical to the standard version contained in
|
| + NodeVisitor. It is only overridden because we're tracking extra
|
| + information (the hierarchy of containing nodes) not preserved in
|
| + the original.
|
| + """
|
| + for field, value in iter_fields(node):
|
| + if isinstance(value, list):
|
| + for item in value:
|
| + if isinstance(item, AST):
|
| + self.visit(item, containingNodes=kwargs['containingNodes'])
|
| + elif isinstance(value, AST):
|
| + self.visit(value, containingNodes=kwargs['containingNodes'])
|
| +
|
| + def visit(self, node, **kwargs):
|
| + """
|
| + Visit a node and extract useful information from it.
|
| +
|
| + This is virtually identical to the standard version contained in
|
| + NodeVisitor. It is only overridden because we're tracking extra
|
| + information (the hierarchy of containing nodes) not preserved in
|
| + the original.
|
| + """
|
| + containingNodes = kwargs.get('containingNodes', [])
|
| + method = 'visit_' + node.__class__.__name__
|
| + visitor = getattr(self, method, self.generic_visit)
|
| + return visitor(node, containingNodes=containingNodes)
|
| +
|
| + def _getFullPathName(self, containingNodes):
|
| + """
|
| + Returns the full node hierarchy rooted at module name.
|
| +
|
| + The list representing the full path through containing nodes
|
| + (starting with the module itself) is returned.
|
| + """
|
| + assert isinstance(containingNodes, list)
|
| + return [(self.options.fullPathNamespace, 'module')] + containingNodes
|
| +
|
| + def visit_Module(self, node, **kwargs):
|
| + """
|
| + Handles the module-level docstring.
|
| +
|
| + Process the module-level docstring and create appropriate Doxygen tags
|
| + if autobrief option is set.
|
| + """
|
| + containingNodes=kwargs.get('containingNodes', [])
|
| + if self.options.debug:
|
| + stderr.write("# Module {0}{1}".format(self.options.fullPathNamespace,
|
| + linesep))
|
| + if get_docstring(node):
|
| + if self.options.topLevelNamespace:
|
| + fullPathNamespace = self._getFullPathName(containingNodes)
|
| + contextTag = '.'.join(pathTuple[0] for pathTuple in fullPathNamespace)
|
| + tail = '@namespace {0}'.format(contextTag)
|
| + else:
|
| + tail = ''
|
| + self._processDocstring(node, tail)
|
| + # Visit any contained nodes (in this case pretty much everything).
|
| + self.generic_visit(node, containingNodes=containingNodes)
|
| +
|
| + def visit_Assign(self, node, **kwargs):
|
| + """
|
| + Handles assignments within code.
|
| +
|
| + Variable assignments in Python are used to represent interface
|
| + attributes in addition to basic variables. If an assignment appears
|
| + to be an attribute, it gets labeled as such for Doxygen. If a variable
|
| + name uses Python mangling or is just a bed lump, it is labeled as
|
| + private for Doxygen.
|
| + """
|
| + lineNum = node.lineno - 1
|
| + # Assignments have one Doxygen-significant special case:
|
| + # interface attributes.
|
| + match = AstWalker.__attributeRE.match(self.lines[lineNum])
|
| + if match:
|
| + self.lines[lineNum] = '{0}## @property {1}{2}{0}# {3}{2}' \
|
| + '{0}# @hideinitializer{2}{4}{2}'.format(
|
| + match.group(1),
|
| + match.group(2),
|
| + linesep,
|
| + match.group(3),
|
| + self.lines[lineNum].rstrip()
|
| + )
|
| + if self.options.debug:
|
| + stderr.write("# Attribute {0.id}{1}".format(node.targets[0],
|
| + linesep))
|
| + if isinstance(node.targets[0], Name):
|
| + match = AstWalker.__indentRE.match(self.lines[lineNum])
|
| + indentStr = match and match.group(1) or ''
|
| + restrictionLevel = self._checkMemberName(node.targets[0].id)
|
| + if restrictionLevel:
|
| + self.lines[lineNum] = '{0}## @var {1}{2}{0}' \
|
| + '# @hideinitializer{2}{0}# @{3}{2}{4}{2}'.format(
|
| + indentStr,
|
| + node.targets[0].id,
|
| + linesep,
|
| + restrictionLevel,
|
| + self.lines[lineNum].rstrip()
|
| + )
|
| + # Visit any contained nodes.
|
| + self.generic_visit(node, containingNodes=kwargs['containingNodes'])
|
| +
|
| + def visit_Call(self, node, **kwargs):
|
| + """
|
| + Handles function calls within code.
|
| +
|
| + Function calls in Python are used to represent interface implementations
|
| + in addition to their normal use. If a call appears to mark an
|
| + implementation, it gets labeled as such for Doxygen.
|
| + """
|
| + lineNum = node.lineno - 1
|
| + # Function calls have one Doxygen-significant special case: interface
|
| + # implementations.
|
| + match = AstWalker.__implementsRE.match(self.lines[lineNum])
|
| + if match:
|
| + self.lines[lineNum] = '{0}## @implements {1}{2}{0}{3}{2}'.format(
|
| + match.group(1), match.group(2), linesep,
|
| + self.lines[lineNum].rstrip())
|
| + if self.options.debug:
|
| + stderr.write("# Implements {0}{1}".format(match.group(1),
|
| + linesep))
|
| + # Visit any contained nodes.
|
| + self.generic_visit(node, containingNodes=kwargs['containingNodes'])
|
| +
|
| + def visit_FunctionDef(self, node, **kwargs):
|
| + """
|
| + Handles function definitions within code.
|
| +
|
| + Process a function's docstring, keeping well aware of the function's
|
| + context and whether or not it's part of an interface definition.
|
| + """
|
| + if self.options.debug:
|
| + stderr.write("# Function {0.name}{1}".format(node, linesep))
|
| + # Push either 'interface' or 'class' onto our containing nodes
|
| + # hierarchy so we can keep track of context. This will let us tell
|
| + # if a function is nested within another function or even if a class
|
| + # is nested within a function.
|
| + containingNodes = kwargs.get('containingNodes', []) or []
|
| + containingNodes.append((node.name, 'function'))
|
| + if self.options.topLevelNamespace:
|
| + fullPathNamespace = self._getFullPathName(containingNodes)
|
| + contextTag = '.'.join(pathTuple[0] for pathTuple in fullPathNamespace)
|
| + modifiedContextTag = self._processMembers(node, contextTag)
|
| + tail = '@namespace {0}'.format(modifiedContextTag)
|
| + else:
|
| + tail = self._processMembers(node, '')
|
| + if get_docstring(node):
|
| + self._processDocstring(node, tail,
|
| + containingNodes=containingNodes)
|
| + # Visit any contained nodes.
|
| + self.generic_visit(node, containingNodes=containingNodes)
|
| + # Remove the item we pushed onto the containing nodes hierarchy.
|
| + containingNodes.pop()
|
| +
|
| + def visit_ClassDef(self, node, **kwargs):
|
| + """
|
| + Handles class definitions within code.
|
| +
|
| + Process the docstring. Note though that in Python Class definitions
|
| + are used to define interfaces in addition to classes.
|
| + If a class definition appears to be an interface definition tag it as an
|
| + interface definition for Doxygen. Otherwise tag it as a class
|
| + definition for Doxygen.
|
| + """
|
| + lineNum = node.lineno - 1
|
| + # Push either 'interface' or 'class' onto our containing nodes
|
| + # hierarchy so we can keep track of context. This will let us tell
|
| + # if a function is a method or an interface method definition or if
|
| + # a class is fully contained within another class.
|
| + containingNodes = kwargs.get('containingNodes', []) or []
|
| + match = AstWalker.__interfaceRE.match(self.lines[lineNum])
|
| + if match:
|
| + if self.options.debug:
|
| + stderr.write("# Interface {0.name}{1}".format(node, linesep))
|
| + containingNodes.append((node.name, 'interface'))
|
| + else:
|
| + if self.options.debug:
|
| + stderr.write("# Class {0.name}{1}".format(node, linesep))
|
| + containingNodes.append((node.name, 'class'))
|
| + if self.options.topLevelNamespace:
|
| + fullPathNamespace = self._getFullPathName(containingNodes)
|
| + contextTag = '.'.join(pathTuple[0] for pathTuple in fullPathNamespace)
|
| + tail = '@namespace {0}'.format(contextTag)
|
| + else:
|
| + tail = ''
|
| + # Class definitions have one Doxygen-significant special case:
|
| + # interface definitions.
|
| + if match:
|
| + contextTag = '{0}{1}# @interface {2}'.format(tail,
|
| + linesep,
|
| + match.group(1))
|
| + else:
|
| + contextTag = tail
|
| + contextTag = self._processMembers(node, contextTag)
|
| + if get_docstring(node):
|
| + self._processDocstring(node, contextTag,
|
| + containingNodes=containingNodes)
|
| + # Visit any contained nodes.
|
| + self.generic_visit(node, containingNodes=containingNodes)
|
| + # Remove the item we pushed onto the containing nodes hierarchy.
|
| + containingNodes.pop()
|
| +
|
| + def parseLines(self):
|
| + """Form an AST for the code and produce a new version of the source."""
|
| + inAst = parse(''.join(self.lines), self.inFilename)
|
| + # Visit all the nodes in our tree and apply Doxygen tags to the source.
|
| + self.visit(inAst)
|
| +
|
| + def getLines(self):
|
| + """Return the modified file once processing has been completed."""
|
| + return linesep.join(line.rstrip() for line in self.lines)
|
| +
|
| +
|
| +def main():
|
| + """
|
| + Starts the parser on the file given by the filename as the first
|
| + argument on the command line.
|
| + """
|
| + from optparse import OptionParser, OptionGroup
|
| + from os import sep
|
| + from os.path import basename
|
| + from sys import argv, exit as sysExit
|
| +
|
| + def optParse():
|
| + """
|
| + Parses command line options.
|
| +
|
| + Generally we're supporting all the command line options that doxypy.py
|
| + supports in an analogous way to make it easy to switch back and forth.
|
| + We additionally support a top-level namespace argument that is used
|
| + to trim away excess path information.
|
| + """
|
| +
|
| + parser = OptionParser(prog=basename(argv[0]))
|
| +
|
| + parser.set_usage("%prog [options] filename")
|
| + parser.add_option(
|
| + "-a", "--autobrief",
|
| + action="store_true", dest="autobrief",
|
| + help="parse the docstring for @brief description and other information"
|
| + )
|
| + parser.add_option(
|
| + "-c", "--autocode",
|
| + action="store_true", dest="autocode",
|
| + help="parse the docstring for code samples"
|
| + )
|
| + parser.add_option(
|
| + "-n", "--ns",
|
| + action="store", type="string", dest="topLevelNamespace",
|
| + help="specify a top-level namespace that will be used to trim paths"
|
| + )
|
| + parser.add_option(
|
| + "-t", "--tablength",
|
| + action="store", type="int", dest="tablength", default=4,
|
| + help="specify a tab length in spaces; only needed if tabs are used"
|
| + )
|
| + parser.add_option(
|
| + "-s", "--stripinit",
|
| + action="store_true", dest="stripinit",
|
| + help="strip __init__ from namespace"
|
| + )
|
| + group = OptionGroup(parser, "Debug Options")
|
| + group.add_option(
|
| + "-d", "--debug",
|
| + action="store_true", dest="debug",
|
| + help="enable debug output on stderr"
|
| + )
|
| + parser.add_option_group(group)
|
| +
|
| + ## Parse options based on our definition.
|
| + (options, filename) = parser.parse_args()
|
| +
|
| + # Just abort immediately if we are don't have an input file.
|
| + if not filename:
|
| + stderr.write("No filename given." + linesep)
|
| + sysExit(-1)
|
| +
|
| + # Turn the full path filename into a full path module location.
|
| + fullPathNamespace = filename[0].replace(sep, '.')[:-3]
|
| + # Use any provided top-level namespace argument to trim off excess.
|
| + realNamespace = fullPathNamespace
|
| + if options.topLevelNamespace:
|
| + namespaceStart = fullPathNamespace.find(options.topLevelNamespace)
|
| + if namespaceStart >= 0:
|
| + realNamespace = fullPathNamespace[namespaceStart:]
|
| + if options.stripinit:
|
| + realNamespace = realNamespace.replace('.__init__', '')
|
| + options.fullPathNamespace = realNamespace
|
| +
|
| + return options, filename[0]
|
| +
|
| + # Figure out what is being requested.
|
| + (options, inFilename) = optParse()
|
| +
|
| + # Read contents of input file.
|
| + inFile = open(inFilename)
|
| + lines = inFile.readlines()
|
| + inFile.close()
|
| + # Create the abstract syntax tree for the input file.
|
| + astWalker = AstWalker(lines, options, inFilename)
|
| + astWalker.parseLines()
|
| + # Output the modified source.
|
| + print(astWalker.getLines())
|
| +
|
| +# See if we're running as a script.
|
| +if __name__ == "__main__":
|
| + main()
|
|
|