#!/usr/bin/env python
#
# $Id: base.py,v 1.20 2006/12/05 13:10:45 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.20 2006/12/05 13:10:45 doughellmann Exp $',
'creator' : 'Doug Hellmann',
'project' : 'HappyDoc',
'created' : 'Sun, 17-Nov-2002 13:17:17 EST',
#
# Current Information
#
'author' : '$Author: doughellmann $',
'version' : '$Revision: 1.20 $',
'date' : '$Date: 2006/12/05 13:10:45 $',
}
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_<argument>=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()
full_name = packageTreeNode.getCanonicalName()
self.statusMessage('Skipping %s with unrecognized mimetype %s' % (
full_name,
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'),
('application/x-function' , 'noopHandler'),
]
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 noopHandler(self, packageTreeNode):
"""Handler that does nothing.
"""
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
|