Source code for pyamp.documentation.rstGeneration

# Copyright 2012 Brett Ponsler
# This file is part of pyamp.
#
# pyamp 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 3 of the License, or
# (at your option) any later version.
#
# pyamp 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 pyamp.  If not, see <http://www.gnu.org/licenses/>.
'''The rstGeneration module contains classes and functions which provide
the ability to generate a single reStructured text formatted file for
each of the modules contained within a main Python project.

'''
from os import curdir
from datetime import date
from optparse import OptionParser
from os.path import abspath, basename, exists, join, split, splitext

from pyamp.logging import Loggable
from pyamp.util import getPythonContents, getClasses


[docs]class RstOptions: '''The RstOptions class encapsulates all the options that can be passed to the RST generation class to configure the generated output. ''' def __init__(self, inheritance=True, overwrite=False, extension='.rst', ignoredModules=None, modifiedDirectory="_modified"): ''' * inheritance -- True to include inheritance diagrams for each class * overwrite -- True to overwrite existing files * extension -- The extension to use for generated files * ingoreModules -- The list of modules to ignore (i.e., no RST files will be created for these modules) * modifiedDirectory -- The modified directory ''' self.inheritance = inheritance self.overwrite = overwrite self.extension = extension self.ignoredModules = [] if ignoredModules is None else ignoredModules self.modifiedDirectory = modifiedDirectory
[docs]class RstBase(Loggable): '''The RstBase class provides the base functionality used to create RstFiles based off of Python modules. It takes a module name and provides the ability to write the output in reStructured text format. ''' contentFunctions = [] '''The contentFunctions property should be set to a list of class function names. These functions will be called, in order, to return a list of lines to add to the final generated file. Each content function takes an :class:`RstOptions` object as its only parameter, and should return a list of strings. ''' def __init__(self, moduleName, logData=None): ''' * moduleName -- The name of the module which contains this module * logData -- The LogData object ''' Loggable.__init__(self, self.__class__.__name__, logData) self._moduleName = moduleName self._currentModuleName = '.'.join(moduleName.split('.')[0:-1]) self._projectName = moduleName.split('.')[0] self._baseName = moduleName.split('.')[-1]
[docs] def write(self, directory, options): '''Generate the reStructured text formatted file for this Python module. * directory -- The output directory in which to write the file * options -- The RstOptions to use when generating the file ''' filename = join(directory, self._getFilename(options)) # Only create the RST file if: the file does not exist, or we are # allowed to overwrite existing files, and this module is not expected # to be ignored if (not exists(filename) or options.overwrite) and \ self._baseName not in options.ignoredModules: content = self._toRst(options) if exists(filename): self.warn("Overwriting existing file [%s]" % self._baseName) # Write the file contents fd = file(filename, 'w') fd.write(content) fd.close() else: filename = None return filename
def _getFilename(self, _options): '''Return the name of the RST file to write. .. note:: This function **must** be overridden by subclasses. * _options -- The :class:`RstOptions` object ''' # @todo: use an error class raise Exception("This function must be implemented by subclasses!") def _toRst(self, options): '''Convert the Python module to a reStructured text file. .. warning:: This function will raise an :class:`AttributError` in the event that the class does not contain one of the function names listed in the :attr:`contentFunctions` property. * options -- The RstOptions to use ''' lines = [] # Create lines for all the different parts of content needed to # create this RST file for name in self.contentFunctions: # Attempt to locate the function # Note: This will throw an AttributeError if the function name # could not be found function = getattr(self, name) lines.extend(function(options)) # Add an empty line to all but the last function if function != self.contentFunctions[-1]: lines.append("") return '\n'.join(lines)
[docs]class RstModuleDirectory(RstBase): '''The RstModuleDirectory encapsulates the functionality of writing a file in reStructured text format for a given Python module which is a directory containing one or more Python sub-modules. ''' ModulePrefix = "mod_" contentFunctions = [ "_getTitle", "_getTocTree", "_getSubModules" ] def __init__(self, moduleName, subFiles, subDirectories, logData=None): ''' * moduleName -- The name of the module which contains this module * subFiles -- The list of sub-file modules for this module * subDirectories -- The list of sub-directory module for this module * -- The LogData object ''' RstBase.__init__(self, moduleName, logData) self.__files = subFiles self.__directories = subDirectories def _getFilename(self, options): '''Return the name of the RST file to write. * options -- The :class:`RstOptions` object ''' # Replace periods with underscores in the module name for # use in the filename moduleName = self._moduleName.replace(".", "_") # Prefix the filename to indicate that this is a module directory entry return "%s%s%s" % (self.ModulePrefix, moduleName, options.extension) def _getTitle(self, _options): '''Return reStructured text lines needed to create the title for this Python module * options -- The :class:`RstOptions` object ''' title = "The %s module" % self._baseName titleLine = "=" * 80 return [titleLine, title, titleLine] def _getTocTree(self, _options): '''Return the reStructured text lines needed to create the table of contents tree for this Python module. * options -- The :class:`RstOptions` object ''' lines = [ ".. toctree::", " :maxdepth: 3" ] return lines def _getSubModules(self, options): '''Return the reStructured text lines needed to create the list of sub-modules for this Python module. * options -- The :class:`RstOptions` object ''' dirs = [self.__getDir(name, options) for name in self.__directories] files = [self.__getFile(name, options) for name in self.__files] # Place the directory entries before the file entries return dirs + files def __getFile(self, moduleName, options): '''Return the reStructured text lines needed to create a single table of contents entry for this Python module corresponding to a single Python file. * options -- The :class:`RstOptions` object ''' spacing = " " * 3 # Grab the base module name for this file baseName = moduleName.split(".")[-1] # Replace periods with underscores to correlate to the actual RST # file name moduleName = moduleName.replace(".", "_") # Add the modified directory location if this module is found # in the list of modules to ignore ignoredModules = options.ignoredModules if baseName in ignoredModules or moduleName in ignoredModules: moduleName = "../%s/%s" % (options.modifiedDirectory, moduleName) # Add spacing to the front of the module name, return "%s%s" % (spacing, moduleName) def __getDir(self, moduleName, options): '''Return the reStructured text lines needed to create a single table of contents entry for this Python module corresponding to a directory containing one or more Python file. * options -- The :class:`RstOptions` object ''' spacing = " " * 3 # Grab the base module name for this file baseName = moduleName.split(".")[-1] # Replace periods with underscores to correlate to the actual RST # module name moduleName = moduleName.replace(".", "_") # Add the modified directory location if this module is found # in the list of modules to ignore ignoredModules = options.ignoredModules if baseName in ignoredModules or moduleName in ignoredModules: moduleName = "../%s/%s" % (options.modifiedDirectory, moduleName) # Add spacing to the front of the module name, and replace periods with # underscores to correlate to the actual RST file name return "%s%s%s" % (spacing, self.ModulePrefix, moduleName)
[docs]class RstModule(RstBase): '''The RstModule class encapsulates the functionality of writing a file in reStructured text format for a given Python module which is a single Python file. ''' contentFunctions = [ "_getComment", "_getTitle", "_getAutoModule", "_getAutoClasses", ] def __init__(self, moduleName, logData=None): ''' * moduleName -- The name of the module which contains this module * logData -- The LogData object ''' RstBase.__init__(self, moduleName, logData) # Find all of the classes contained within this module self.__classes = self.__findClasses() def _getFilename(self, options): '''Return the name of the RST file to write. * options -- The :class:`RstOptions` object ''' # Convert periods to underscores in the module name for # use in the filename moduleName = self._moduleName.replace(".", "_") # Create a filename with the module name as a prefix return "%s%s" % (moduleName, options.extension) def _getComment(self, _options): '''Get the reStructured text lines for the auto generated comment. * _options -- The RstOptions to use ''' timestamp = date.today().strftime("%B %d, %Y") lines = [ ".. %s documentation file, created by" % self._projectName, " generateRsts for %s on %s" % (self._moduleName, timestamp), " This file should not be modified." ] return lines def _getTitle(self, _options): '''Get the reStructured text lines for the title for this module. * _options -- The RstOptions to use ''' title = "The %s module" % self._baseName titleLine = "=" * (len(title) + 1) return [titleLine, title, titleLine] def _getAutoModule(self, _options): '''Get the reStructured text lines for the automodule directive. * _options -- The RstOptions to use ''' return [".. automodule:: %s" % self._moduleName] def _getAutoClasses(self, options): '''Get the reStructured text lines for all the autoclass directives for this module. * options -- The RstOptions to use ''' lines = [] index = 1 for className in self.__classes: lines.extend(self.__getAutoClass(className, options)) # Add an empty line to all but the last class if index < len(self.__classes): lines.append("") index += 1 return lines def __getAutoClass(self, className, options): '''Get the reStructured text lines for a single autoclass directive. * className -- The name of the class * options -- The RstOptions to use ''' # Get the full module name of the class fullClassName = '.'.join([self._moduleName, className]) title = "The %s class" % className titleLine = "-" * (len(title) + 1) # Add the title to the lines lines = [titleLine, title, titleLine, ""] # Create the line to add the inheritance diagram if options.inheritance: inheritance = ".. inheritance-diagram:: %s" % fullClassName lines.extend([inheritance, ""]) # Create the lines to include the class and its members autoClass = ".. autoclass:: %s" % fullClassName members = " :members:" # Add the auto class directive lines.extend([autoClass, members]) return lines def __findClasses(self): '''Find the list of classes for this Python module''' try: classes = getClasses(self._moduleName) except Exception, e: self.error("Failed to locate classes for [%s]" % self._moduleName) classes = [] # Only keep classes that are actually part of the current project classes = [t[0] for t in classes.items() if self.__isProjectClass(t)] return classes def __isProjectClass(self, classTuple): '''Determine if the given class tuple (where the first element in the tuple is the class name and the second element in the tuple is the class object) is part of the current module. * classTuple -- The class tuple ''' className = str(classTuple[1]) searchFor = "%s." % self._currentModuleName return className.startswith(self._currentModuleName) or \ searchFor in str(classTuple[1])
def locateRsts(moduleName, directory): '''Return the list of :class:`RstModule`s contained within the given directory for the given project module name. * moduleName -- The module name for the Python project * directory -- The directory containing the Python modules ''' rsts = [] # Grab the contents of the current directory files, subDirectories = getPythonContents(directory, absPath=True) # Determine if this is a proper Python module initFile = "__init__.py" fileBases = dict([(basename(f), f) for f in files]) # Only continue for valid Python module directories (i.e., those containing # the __init__.py file) if initFile in fileBases: # Remove the init file from the list of files because we do not want # to generate an RST file for this file files.remove(fileBases[initFile]) # Keep track of the list of sub-modules (files and directories) # for the current module subModuleFiles = [] subModuleDirs = [] # Generate RST classes for all of the files in this directory for filename in files: baseName = splitext(basename(filename))[0] # Ignore any files starting with an underscore if not baseName.startswith("_"): fullModuleName = '.'.join([moduleName, baseName]) # Only process the module once if fullModuleName not in subModuleFiles: rsts.append(RstModule(fullModuleName)) # This file is a sub-module subModuleFiles.append(fullModuleName) # Generate RST classes for all of the sub directories for subDirectory in subDirectories: newModuleName = '.'.join([moduleName, basename(subDirectory)]) # Recurse to find the RST files for this sub-directory subRsts = locateRsts(newModuleName, subDirectory) # If the directory contained modules then we should keep # track of it if len(subRsts) > 0 and newModuleName not in subModuleDirs: rsts.extend(subRsts) # This directory is a sub-module subModuleDirs.append(newModuleName) # Add an entry for the module directory rsts.append(RstModuleDirectory(moduleName, subModuleFiles, subModuleDirs)) return rsts def writeRsts(rsts, outputDirectory, options): '''Generate a single reStructured text file for all of the given :class:`.RstModule` objects. These files will be generated in the output directory. Use the given RstOptions when generating the files. * rsts -- The list of :class:`.RstModule` objects * outputDirectory -- The output directory where the files will be saved * options -- The RstOptions to use ''' generated = [] if exists(outputDirectory): # Write all of the RST files to the output directory for rst in rsts: generated.append(rst.write(outputDirectory, options)) else: # @todo: use amp error raise Exception("Output directory [%s] does not exist!" % \ outputDirectory) # Return the list of files that were generated (filter out None which # indicates that a file was not created) return [filename for filename in generated if filename is not None] def generateAllRsts(projectName, ignoredModules): '''Locate and generate reStructured text files for all of the Python modules located within the given project name and also being sure to ignore any modules that are in the list of modules to ignore. .. note:: This assumes a standard doctools Python project setup structure and that this function will be called from a script located within the 'doc' directory. ''' parser = OptionParser() # Create command line options which provide the ability to configure the # RST creation from the command line parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False, help="True for verbose mode.") parser.add_option("-f", "--force", dest="force", action="store_true", default=False, help="Overwrite existing files.") parser.add_option("--noInheritance", dest="noInheritance", default=False, action="store_true", help="Do not include inheritance graphs for " \ "all of the classes.") parser.add_option("--extension", dest="extension", default='.rst', help="The extension used to save the RST files " \ "default is '.rst'.") parser.add_option("-d", "--dest", dest="destination", default='source/_generated', help="The destination directory for the generated RST " \ "files. Default is source/_generated.") parser.add_option("--modified", dest="modifiedDirectory", default='_modified', help="The name of the directory where modified RST " \ "files pertaining to modules within this project " \ "are stored. Default is source/_modified. Note: " \ "The modified directory must be at the same level " \ "as the generated directory!") (options, _) = parser.parse_args() # Pop up one directory to get the project trunk directory, and then # underneath that should be a directory with the same name as the project # which should contain all of the source files cwd = abspath(curdir) projectTrunkDir = split(cwd)[0] projectSourceDir = join(projectTrunkDir, projectName) # Create the RST options rstOptions = RstOptions(inheritance=not options.noInheritance, overwrite=options.force, extension=options.extension, ignoredModules=ignoredModules, modifiedDirectory=options.modifiedDirectory) # Locate all of the RST files for the project rsts = locateRsts(projectName, projectSourceDir) # Generate all of the reStructured text files for this project generated = writeRsts(rsts, options.destination, rstOptions) # Display output to the user print "Generated [%d] files..." % len(generated) # Print out all of the generated files for verbose mode if options.verbose: for gen in generated: print " ", gen return generated