#!/usr/bin/env python # # $Id: base.py,v 1.18 2003/03/16 20:17:30 doughellmann Exp $ # # Copyright 2002 Doug Hellmann. # # # All Rights Reserved # # Permission to use, copy, modify, and distribute this software and # its documentation for any purpose and without fee is hereby # granted, provided that the above copyright notice appear in all # copies and that both that copyright notice and this permission # notice appear in supporting documentation, and that the name of Doug # Hellmann not be used in advertising or publicity pertaining to # distribution of the software without specific, written prior # permission. # # DOUG HELLMANN DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, # INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN # NO EVENT SHALL DOUG HELLMANN BE LIABLE FOR ANY SPECIAL, INDIRECT OR # CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS # OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # """Base classes for documentation sets. """ __rcs_info__ = { # # Creation Information # 'module_name' : '$RCSfile: base.py,v $', 'rcs_id' : '$Id: base.py,v 1.18 2003/03/16 20:17:30 doughellmann Exp $', 'creator' : 'Doug Hellmann ', 'project' : 'HappyDoc', 'created' : 'Sun, 17-Nov-2002 13:17:17 EST', # # Current Information # 'author' : '$Author: doughellmann $', 'version' : '$Revision: 1.18 $', 'date' : '$Date: 2003/03/16 20:17:30 $', } try: __version__ = __rcs_info__['version'].split(' ')[1] except: __version__ = '0.0' # # Import system modules # import os import shutil # # Import Local modules # import happydoclib from happydoclib.utils import * from happydoclib.trace import trace from happydoclib.docstring import getConverterFactoryForFile, \ getConverterFactory # # Module # TRACE_LEVEL=2 class DocSetBase: """Base class for documentation sets. This is the base class for all documentation set classes. The methods defined here are required of all docset classes. Only the 'write' method is actually used by the main application. """ def __init__(self, scanner, title, outputDirectory, statusMessageFunc=None, extraParameters={}, ): """Basic Documentation Set Parameters scanner -- A directory tree scanner. title -- the title of the documentation set outputDirectory -- The base directory for writing the output files. statusMessageFunc -- function which will print a status message for the user extraParameters -- Dictionary containing parameters specified on the command line by the user. """ trace.into('DocSetBase', '__init__', scanner=scanner, title=title, outputDirectory=outputDirectory, statusMessageFunc=statusMessageFunc, extraParameters=extraParameters, outputLevel=TRACE_LEVEL, ) # # Store parameters # self.scanner = scanner self.title = title self.output_directory = outputDirectory self.status_message_func = statusMessageFunc self.statusMessage('Initializing documentation set %s' % title) self.statusMessage('NEED TO HANDLE extraParameters in DocSetBase') trace.outof(outputLevel=TRACE_LEVEL) return def statusMessage(self, message='', verboseLevel=1): "Print a status message for the user." if self.status_message_func: self.status_message_func(message, verboseLevel) return def warningMessage(self, message=''): self.statusMessage('WARNING: %s' % message, 0) return def write(self): """Called by the application to cause the docset to be written. """ raise NotImplementedError('%s.write' % self.__class__.__name__) class DocSet(DocSetBase): # # This class extends the DocSetBase with a few more convenience # methods. Most docsets will actually subclass from DocSet or one # of its descendants rather than from DocSet directly. # # The basic extension is that this class provides a 'write' method # which walks the scanner tree, determines the appropriate writer # method for each node, and calls the writer. Subclasses need only # provide writers and # """Docset Parameters Pass parameters to the docset using the syntax: docset_=value Common parameters for all documentation sets includeComments -- Boolean. False means to skip the comment parsing step in the parser. Default is True. includePrivateNames -- Boolean. False means to ignore names beginning with _. Default is True. """ # # Override this with a mapping from mime-type to the # method name to be called to handle writing a node # of that type. # mimetype_writer_mapping = {} def __init__(self, scanner, title, outputDirectory, includeComments=1, includePrivateNames=1, sortNames=0, statusMessageFunc=None, extraParameters={}, ): """Basic Documentation Set Parameters scanner -- A directory tree scanner. title -- the title of the documentation set outputDirectory -- The base directory for writing the output files. includeComments -- Boolean. False means to skip the comment parsing step in the parser. Default is True. includePrivateNames -- Boolean. False means to ignore names beginning with _. Default is True. sortNames=0 -- Boolean. True means to sort names before generating output. Default is False. statusMessageFunc -- function which will print a status message for the user extraParameters -- Dictionary containing parameters specified on the command line by the user. """ trace.into('DocSet', '__init__', scanner=scanner, title=title, outputDirectory=outputDirectory, includeComments=includeComments, includePrivateNames=includePrivateNames, sortNames=0, statusMessageFunc=statusMessageFunc, extraParameters=extraParameters, outputLevel=TRACE_LEVEL, ) DocSetBase.__init__( self, scanner=scanner, title=title, outputDirectory=outputDirectory, statusMessageFunc=statusMessageFunc, extraParameters=extraParameters, ) # # Store parameters # self.include_comments = includeComments self.include_private_names = includePrivateNames self.sort_names = sortNames self._initializeWriters() trace.outof(outputLevel=TRACE_LEVEL) return def _filterNames(self, nameList): """Remove names which should be ignored. Parameters nameList -- List of strings representing names of methods, classes, functions, etc. This method returns a list based on the contents of nameList. If private names are being ignored, they are removed before the list is returned. """ if not self.include_private_names: #nameList = filter(lambda x: ( (x[0] != '_') or (x[:2] == '__') ), # nameList) nameList = [ name for name in nameList if name and ((name[0] != '_') or (name[:2] == '__')) ] return nameList def _skipInputFile(self, packageTreeNode): """False writer method used to notify the user that a node is being skipped because the real writer is unknown. """ mimetype, encoding = packageTreeNode.getMimeType() filename = packageTreeNode.getInputFilename() self.statusMessage('Skiping unrecognized file %s with mimetype %s' % ( filename, mimetype, )) return def getWriterForNode(self, packageTreeNode): """Returns the writer to be used for the node. """ mimetype, encoding = packageTreeNode.getMimeType() writer_name = self.mimetype_writer_mapping.get(mimetype) if writer_name: writer = getattr(self, writer_name) else: # # Unrecognized file. # writer = self._skipInputFile return writer def writeCB(self, packageTreeNode): """Callback used when walking the scanned package tree. """ trace.into('MultiHTMLFileDocSet', 'writeCB', packageTreeNode=packageTreeNode, outputLevel=TRACE_LEVEL, ) writer = self.getWriterForNode(packageTreeNode) writer(packageTreeNode) trace.outof(outputLevel=TRACE_LEVEL) return def write(self): """Called by the application to cause the docset to be written. """ self.scanner.walk(self.writeCB) return def _initializeWriters(self): """Hook to allow subclasses to register writers without having to override __init__ with all of its arguments. """ return def registerWriter(self, mimetype, writerName): """Register a writer for the specified mimetype. """ #print '%s -> %s' % (mimetype, writerName) self.mimetype_writer_mapping[mimetype] = writerName return class MultiFileDocSet(DocSet): """Base class for documentation sets which write to multiple files. This class further extends the DocSet class by adding several convenience methods for handling files, as well as a few basic handlers. """ CONVERTER_HEADER_START_LEVEL = 4 mimetype_extension_mapping = { 'text/x-python' : { 'remove_existing':1,}, 'text/plain' : { 'remove_existing':1,}, 'text/x-structured' : { 'remove_existing':1,}, 'text/html' : { 'remove_existing':1,}, } def _initializeWriters(self): """Hook to allow subclasses to register writers without having to override __init__ with all of its arguments. """ DocSet._initializeWriters(self) mimetype_writers = [ ('application/x-directory' , 'processDirectory'), ('text/x-python' , 'processPythonFile'), ('application/x-class' , 'processPythonClass'), ('text/plain' , 'processPlainTextFile'), ('text/x-structured' , 'processPlainTextFile'), ('text/html' , 'copyInputFileToOutput'), ('image/gif' , 'copyInputFileToOutput'), ('image/jpeg' , 'copyInputFileToOutput'), ('image/png' , 'copyInputFileToOutput'), ] for mimetype, writer_name in mimetype_writers: self.registerWriter(mimetype, writer_name) return def getOutputFilenameForPackageTreeNode(self, packageTreeNode, includePath=1): """Returns a filename where documentation for packageTreeNode should be written. The filename will be in the output directory, possibly in a subdirectory based on the path from the input root to the input file. For example:: input_directory : /foo/input containing : /foo/input/bar.py output_directory : /foo/output results in : /foo/output/input/bar.py """ trace.into('MultiFileDocSet', 'getOutputFilenameForPackageTreeNode', packageTreeNode=packageTreeNode, includePath=includePath, outputLevel=TRACE_LEVEL, ) mimetype, encoding = packageTreeNode.getMimeType() trace.writeVar(mimetype=mimetype, outputLevel=TRACE_LEVEL) settings = self.mimetype_extension_mapping.get(mimetype, {}) trace.writeVar(settings=settings, outputLevel=TRACE_LEVEL) if includePath: # # Get the input filename, relative to the root of the input. # input_filename = packageTreeNode.getRelativeFilename() # # Add the output directory to the front of the input # filename. # output_filename = os.path.join(self.output_directory, input_filename) else: input_filename = packageTreeNode.getRelativeFilename() output_filename = os.path.basename(input_filename) if settings.get('remove_existing'): output_filename, ignore = os.path.splitext(output_filename) # # Normalize the path, in case it includes /./ and the like. # normalized_output_filename = os.path.normpath(output_filename) trace.outof(normalized_output_filename, outputLevel=TRACE_LEVEL) return normalized_output_filename def copyInputFileToOutput(self, packageTreeNode): """Copy the input file to the appropriate place in the output. """ input_filename = packageTreeNode.getInputFilename() output_filename = self.getOutputFilenameForPackageTreeNode(packageTreeNode) # # Make sure the directory exists. # output_dirname = os.path.dirname(output_filename) self.rmkdir(output_dirname) # # Copy the file # self.statusMessage('Copying: %s\n To: %s' % ( input_filename, output_filename, )) shutil.copyfile(input_filename, output_filename) return def rmkdir(self, path): "Create a directory and all of its children." if not path: return parts = os.path.split(path) if len(parts) > 1: parent, child = parts if not isSomethingThatLooksLikeDirectory(parent): self.rmkdir(parent) if not isSomethingThatLooksLikeDirectory(path): os.mkdir(path) return def processDirectory(self, packageTreeNode): """Handler for application/x-directory nodes. Creates the output directory and writes the table of contents file. """ trace.into('MultiFileDocSet', 'processDirectory', packageTreeNode=packageTreeNode, outputLevel=TRACE_LEVEL, ) canonical_path = packageTreeNode.getPath(1) canonical_filename = apply(os.path.join, canonical_path) output_filename = self.getOutputFilenameForPackageTreeNode(packageTreeNode) output_dirname = os.path.dirname(output_filename) self.statusMessage('Directory : "%s"\n to: "%s"' % ( canonical_filename, output_filename, )) if os.path.isdir(output_dirname): self.statusMessage('\tExists') else: self.rmkdir(output_dirname) self.statusMessage('\tCreated') self.writeTOCFile(packageTreeNode) trace.outof(outputLevel=TRACE_LEVEL) return def writeTOCFile(self, packageTreeNode): """Write the table of contents for a directory. Subclasses must implement this method. The packageTreeNode is a directory, and the table of contents for that directory should be written as appropriate. """ raise NotImplementedError('writeTOCFile') def writeFileHeader(self, output, packageTreeNode, title='', subtitle=''): """Given an open output stream, write a header using the title and subtitle. Subclasses must implement this method. """ raise NotImplementedError('writeFileHeader') def writeFileFooter(self, output): """Given an open output stream, write a footer using the title and subtitle. Subclasses must implement this method. """ raise NotImplementedError('writeFileFooter') def openOutput(self, name, packageTreeNode, title='', subtitle=''): """Open the output stream from the name. Opens the output stream and writes a header using title and subtitle. Returns the stream. """ directory, basename = os.path.split(name) if not os.path.exists(directory): self.rmkdir(directory) f = open(name, 'wt') self.writeFileHeader(f, packageTreeNode, title=title, subtitle=subtitle) return f def closeOutput(self, output): """Close the output stream. Writes a footer to the output stream and then closes it. """ self.writeFileFooter(output) output.close() return def _unquoteString(self, str): "Remove surrounding quotes from a string." str = str.strip() while ( str and (str[0] == str[-1]) and str[0] in ('"', "'") ): str = str[1:-1] return str def formatText(self, text, textFormat): """Returns text formatted appropriately for output by this docset. Arguments: 'text' -- String to be formatted. 'textFormat' -- String identifying the format of 'text' so the formatter can use a docstring converter to convert the body of 'text' to the appropriate output format. 'quote=1' -- Boolean option to control whether the text should be quoted to escape special characters. """ text = self._unquoteString(text) # # Get a text converter # converter_factory = getConverterFactory(textFormat) converter = converter_factory() # # Do we need to quote the text? # #if self._html_quote_text and quote: # text = converter.quote(text, 'html') # # Convert and write the text. # html = converter.convert(text, 'html', level=self.CONVERTER_HEADER_START_LEVEL) return html def writeText(self, output, text, textFormat): """Format and write the 'text' to the 'output'. Arguments: 'output' -- Stream to which 'text' should be written. 'text' -- String to be written. 'textFormat' -- String identifying the format of 'text' so the formatter can use a docstring converter to convert the body of 'text' to the appropriate output format. 'quote=1' -- Boolean option to control whether the text should be quoted to escape special characters. """ if not text: return html = self.formatText(text, textFormat) output.write(html) return def processPythonFile(self, packageTreeNode): """Handler for text/x-python nodes. """ raise NotImplementedError('processPythonFile') def processPlainTextFile(self, packageTreeNode): """Handler for text/x-structured and text/plain nodes. Converts the input file to the output file format and generates the output. The output directory is assumed to already exist. """ trace.into('MultiFileDocSet', 'processPlainTextFile', packageTreeNode=packageTreeNode, outputLevel=TRACE_LEVEL, ) canonical_path = packageTreeNode.getPath(1) canonical_filename = apply(os.path.join, canonical_path) output_filename = self.getOutputFilenameForPackageTreeNode(packageTreeNode) self.statusMessage('Translating: "%s"\n to: "%s"' % ( canonical_filename, output_filename, )) converter_factory = getConverterFactoryForFile(canonical_filename) converter = converter_factory() input_file = converter.getExternalDocumentationFile(canonical_filename) raw_body = str(input_file) # # FIXME - This needs to be handled more abstractly! # cooked_body = converter.convert(raw_body, 'html', level=3) output_file = self.openOutput( output_filename, packageTreeNode, title=self.title, subtitle=packageTreeNode.getRelativeFilename(), ) output_file.write(cooked_body) self.closeOutput(output_file) trace.outof(outputLevel=TRACE_LEVEL) return def _computeRelativeHREF(self, source, destination): """Compute the HREF to point from the output file of source to destination. """ trace.into('MultiHTMLFileDocSet', '_computeRelativeHREF', source=source.getName(), destination=destination.getName(), outputLevel=TRACE_LEVEL, ) relative_path = source.getPathToNode(destination) trace.writeVar(relative_path=relative_path, outputLevel=TRACE_LEVEL) if not relative_path: output_name = self.getOutputFilenameForPackageTreeNode( destination, includePath=0, ) trace.outof(output_name, outputLevel=TRACE_LEVEL) return output_name destination_mimetype = destination.getMimeType()[0] source_mimetype = source.getMimeType()[0] # # Pointing to a class defined by source module. # if ( (len(relative_path) == 1) and (destination_mimetype == 'application/x-class') and (source_mimetype == 'text/x-python') and (source.get(destination.getName()) is not None) ): trace.write('adding source to relative path', outputLevel=TRACE_LEVEL) relative_path = (source.getName(), relative_path[0]) destination_name = destination.getName() if relative_path[-1] == destination_name: # # Need to replace with output name. # output_name = self.getOutputFilenameForPackageTreeNode( destination, includePath=0, ) trace.write('Replacing %s with %s' % (relative_path[-1], output_name, ), outputLevel=TRACE_LEVEL, ) relative_path = relative_path[:-1] + (output_name,) # # If the destination is a directory, add 'index.html' to the end. # #print destination.getName(), destination.getMimeType() #if destination.getMimeType() == ('application/x-directory', None): # print 'adding index.html' # relative_path += ('index.html',) # print relative_path href = '/'.join(relative_path) trace.outof(href, outputLevel=TRACE_LEVEL) return href