#!/usr/bin/env python
#
# $Id: happydocset.py,v 1.11 2001/06/02 19:05:26 doughellmann Exp $
#
# Time-stamp: <01/05/05 14:22:25 dhellmann>
#
# Copyright 2001 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 for docsets.

"""

__rcs_info__ = {
    #
    #  Creation Information
    #
    'module_name'  : '$RCSfile: happydocset.py,v $',
    'rcs_id'       : '$Id: happydocset.py,v 1.11 2001/06/02 19:05:26 doughellmann Exp $',
    'creator'      : 'Doug Hellmann <DougHellmann@bigfoot.com>',
    'project'      : 'HappyDoc',
    'created'      : 'Sat, 10-Feb-2001 08:18:58 EST',

    #
    #  Current Information
    #
    'author'       : '$Author: doughellmann $',
    'version'      : '$Revision: 1.11 $',
    'date'         : '$Date: 2001/06/02 19:05:26 $',
}

#
# Import system modules
#
import UserList
import sys
import os
import string
import re
try:
    from cStringIO import StringIO
except:
    from StringIO import StringIO
import token
import symbol
import parser


#
# Import Local modules
#
import optiontools
import hdpath

#
# Module
#
class DocSet(UserList.UserList):
    """Basic DocSet Parameters
    
        Parameters

            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.

            usePackages -- Boolean.  True means to provide special
                           handling for Packages (directories
                           containing __init__.py files) from
                           non-package Modules.

            prewrittenFileBasenames -- Base names (no extensions) of
                                       StructuredText files which are
                                       to be converted to the output
                                       format and included in the
                                       docset.

            statusMessageFunc -- function which will print a status
                                 message for the user

            title -- the title of the documentation set

            useRecursion -- Recurse into subdirectories looking for
                            subdirectories and files within them.

    """

    def __init__(self,
                 
                 formatterFactory,
                 parserFunc,
                 inputModuleNames,
                 
                 author='',
                 outputBaseDirectory=None,
                 docsetBaseDirectory=None,
                 descriptionFilename=None,
                 formatterParameters={},
                 ignoreDirFunc=None,
                 includeComments=1,
                 includePrivateNames=1,
                 usePackages=1,
                 prewrittenFileBasenames=( 'ANNOUNCE',
                                           'CHANGES',
                                           'LICENSE',
                                           'README',
                                           'TODO',
                                           ),
                 statusMessageFunc=None,
                 title='HappyDoc Generated Documentation (use -t to specify a new title)',
                 useRecursion=1,

                 #
                 # Special package handling arguments
                 #
                 packageName='',
                 docsetRoot=None,

                 #
                 # DON'T FORGET TO UPDATE THE CLONE METHOD!!!
                 #
                 
                 **extraNamedParameters
                 ):
        """Initialize the documentation set.

        Parameters

            formatterFactory -- a callable object which creates the type
                                of formatter class to be used for formatting
                                the documentation set.  The object will be
                                called and passed the DocSet along with other
                                configuration values for the formatter.

            parserFunc -- Parser function which returns the info for a module.

            inputModuleNames -- List of modules or directories to be documented.
            
            outputBaseDirectory -- the name of the root directory for this and any
                                   parent docsets

            docsteBaseDirectory -- the name of the root directory for this docset

            descriptionFilename -- File describing the docset as a whole.

            formatterParameters -- other named configuration values to be passed
                                   to the formatter when it is created through the
                                   factory.  Any unrecognized values will be
                                   quietly ignored.

            ignoreDirFunc -- Function which returns true if the directory should
                             be ignored.

            packageName -- Name of the package being documented by
                           this docset.  This value should only be
                           specified when recursively creating a
                           docset (HappyDoc handles this case
                           automatically).

            docsetRoot=None -- The root DocSet object, when recursing.

            *For others, see class documentation.*
                                   
        """        
        #
        # Store parameters
        #
        self._formatter_factory = formatterFactory
        self._parser_func = parserFunc
        self._input_module_names = inputModuleNames
        self._contained_names = [] # list of inputModuleNames actually used
        self._title = title
        self._output_base_directory = outputBaseDirectory
        if docsetBaseDirectory:
            self._docset_base_directory = docsetBaseDirectory
        else:
            self._docset_base_directory = outputBaseDirectory
        self._status_message_func = statusMessageFunc
        self._description_filename = descriptionFilename
        self._formatter_configuration = formatterParameters
        self._include_private_names = optiontools.getBooleanArgumentValue(
            includePrivateNames)
        self._include_comments = optiontools.getBooleanArgumentValue(includeComments)
        self._use_packages = optiontools.getBooleanArgumentValue(usePackages)
        self._use_recursion = useRecursion
        self._ignore_dir_name = ignoreDirFunc
        self._prewritten_file_basenames = prewrittenFileBasenames
        self._prewritten_files = []
        self._package_name = packageName
        self._docset_root = docsetRoot
        
        #
        # Initialize this class
        #
        self._open_handles = []
        self._all_modules = {}
        self._all_classes = {}
        self._all_packages = {}
        
        #
        # Initialize base class
        #
        UserList.UserList.__init__(self)
        self.statusMessage()
        self.statusMessage('Initializing documentation set %s...' % \
                           self._title)
        #
        # Handle unrecognized named parameters.
        #
        for extra_param, extra_value in extraNamedParameters.items():
            self.statusMessage(
                'WARNING: Parameter "%s" (%s) unrecognized by docset %s.' % \
                (extra_param, extra_value, self.__class__.__name__)
                )
        #
        # Create the formatter
        #
        self._formatter = apply( self._formatter_factory,
                                 ( self, ),
                                 self._formatter_configuration )
        #
        # Process the modules specified.
        #
        self.processFiles(inputModuleNames)
        return

    def _foundReadme(self):
        """If a README file was found, return the name of the file.
        """
        for fname in self._prewritten_files:
            if hdpath.basename(fname)[:6] == 'README':
                return fname
        return None

    def getName(self):
        "Returns the name of the package."
        return self._package_name
    
    def getSummary(self):
        """Return one line summary of the documentation set.
        """
        one_liner = ''
        #
        # First check for __init__.py description.
        #
        if (not one_liner) and self._all_modules.has_key('__init__'):
            one_liner = self._all_modules['__init__'].getSummary()
        #
        # Then check for first line of README file.
        #
        if (not one_liner) and self._foundReadme():
            readme_filename = self._foundReadme()
            one_liner, body = self.getStructuredTextFile(readme_filename)
        #
        # Then check for specified title.
        #
        if (not one_liner) and self._title:
            one_liner = self._title
        
        return one_liner

    def getDocsetRoot(self):
        """Return the root node of the documentation set.
        """
        if self._docset_root:
            return self._docset_root
        else:
            return self

    def getPackageName(self):
        """Returns the package name for the docset.
        """
        #print '########\nNAME FOR DOCSET %s \n#########' % self._package_name
        return self._package_name

    def clone(self, packageName, docsetBaseDirectory, inputModuleNames):
        """Create a new object which is configured the same as the current.

        It is possible, by passing optional arguments, to
        create a clone with a different configuration.  This
        is useful for recursive work.

        Parameters

          packageName -- The name of the package being created.

          docsetBaseDirectory -- The base of output for the new docset.

          inputModuleNames -- Names of files to be included.

        """
        constructor_args = self.getCloneArguments(packageName,
                                                  docsetBaseDirectory,
                                                  inputModuleNames)
        new_obj = apply(self.__class__, (), constructor_args)
        return new_obj

    def getCloneArguments(self, packageName, docsetBaseDirectory, inputModuleNames):
        '''Return arguments to create a new docset based on the current one.

        Parameters

          packageName -- The name of the package being created.

          docsetBaseDirectory -- The base of output for the new docset.

          inputModuleNames -- Specify the file names to be included in
          the docset.

        Subclasses should override this method, but should also call
        the parent method using an algorithm such as::

          subclass_args = {}
          subclass_args.update( ParentClass.getCloneArguments(self,
                                                              packageName,
                                                              baseDirectory,
                                                              inputModuleNames) )
          subclass_args.update( { "subClassArgument":self._sub_class_argument,
                          })
          return subclass_args

        '''
        constructor_args = {
            'formatterFactory':self._formatter_factory,
            'parserFunc':self._parser_func,
            'inputModuleNames':inputModuleNames,

            'outputBaseDirectory':self._output_base_directory,
            'docsetBaseDirectory':docsetBaseDirectory,
            
            'descriptionFilename':self._description_filename,
            'formatterParameters':self._formatter_configuration,
            'ignoreDirFunc':self._ignore_dir_name,
            'includeComments':self._include_comments,
            'includePrivateNames':self._include_private_names,
            'usePackages':self._use_packages,
            'prewrittenFileBasenames':self._prewritten_file_basenames,

            'statusMessageFunc':self._status_message_func,
            'useRecursion':self._use_recursion,
            }
        #
        # Construct a reasonable title
        #
        if self._package_name:
            title = '%s.%s' % (self._title, packageName)
        else:
            title = '%s: %s' % (self._title, packageName)
        constructor_args['title'] = title
        #
        # Set up recursion into package with
        # a reference back to the root.
        #
        constructor_args.update( { 'packageName':packageName,
                                   'docsetRoot':self.getDocsetRoot(),
                                   }
                                 )
        return constructor_args

    def __del__(self):
        #
        # Attempt to reduce cycles, since the
        # formatter generally has a reference
        # to the docset as well.
        #
        self._formatter = None
        return

    def _requiredOfSubclass(self, name):
        "Convenient way to raise a consistent exception."
        raise AttributeError('%s is not implemented for this class.' % name,
                             self.__class__.__name__)
    
    def getFileInfo(self, fileName):
        "Parse the file and return the parseinfo instance for it."
        #self.statusMessage('Getting info for %s' % fileName)
        return self._parser_func(fileName, self._include_comments)

    def lookForPrewrittenFiles(self, dirName):
        """Look for prewritten StructuredText files in 'dirName'.
        """
        files = []
        for file_basename in self._prewritten_file_basenames:
            found = hdpath.findFilesInDir( dirName, '%s*' % file_basename )
            for f in found:
                #
                # Eliminate any backup files, etc. created by
                # text editors.
                #
                if f[-1] in '*~#':
                    continue
                #
                # Record this name
                #
                self.statusMessage(
                    '  Found external documentation file\n    %s' \
                    % f, 2)
                files.append(f)
        return files
    
    def processFiles(self,
                     fileNames,
                     moduleFileName=re.compile(r'^.*\.py$').match,
                     ):
        """Get information about a list of files.

        Parameters

          fileNames -- Sequence of names of files to be read.

        Each file in fileNames is parsed to extract information
        to be used in documenting the file.

        """
        for file_name in fileNames:

            if ( hdpath.isdir(file_name)
                 and
                 (not self._ignore_dir_name(file_name))
                 and
                 (self._use_recursion >= 0)
                 ):
                #
                # Record that we paid attention to this directory
                #
                self._contained_names.append(file_name)
                #
                # Find modules and directories within to
                # recurse.
                #
                dir_contents = hdpath.findFilesInDir(file_name)
                #
                # Adjust the recursion factor.
                #
                # -1 -- Do not recurse.
                #  0 -- Recurse one level.
                #  1 -- Always recurse.
                #
                if not self._use_recursion:
                    self._use_recursion = -1
                #
                # Check if the current dir is a Package
                #
                init_file = hdpath.join( file_name, '__init__.py' )
                if hdpath.exists( init_file ) and self._use_packages:
                    self.statusMessage('Detected package %s' % file_name, 2 )
                    #
                    # Special handling for Package directories
                    #
                    file_name = hdpath.removeRelativePrefix(file_name)
                    package_name = hdpath.basename(file_name)
                    docset_base = self.getDocsetBaseDirectory()
                    output_base = self.getOutputBaseDirectory()
                    new_base = hdpath.joinWithCommonMiddle(
                        output_base,
                        docset_base,
                        file_name)
                    
                    new_docset = self.clone( packageName=package_name,
                                             docsetBaseDirectory=new_base,
                                             inputModuleNames=dir_contents,
                                             )
                    new_docset._prewritten_files = new_docset.lookForPrewrittenFiles(
                        file_name)
                    self.append(new_docset)
                else:
                    self.statusMessage('Recursing into %s' % file_name)
                    #
                    # Find pre-written files within the regular directory
                    #
                    self._prewritten_files = self._prewritten_files + \
                                             self.lookForPrewrittenFiles(file_name)
                    self.processFiles(dir_contents)
                    
            elif moduleFileName(file_name):
                #
                # Record that we paid attention to this file
                #
                self._contained_names.append(file_name)
                #
                # Regular module file
                #
                try:
                    file_info = self.getFileInfo(file_name)
                except SyntaxError, msg:
                    self.statusMessage('\nERROR: SyntaxError: %s[%s] %s' % \
                                       (file_name, msg.lineno, msg.msg)
                                       )
                    self.statusMessage('\n%s' % msg.text)
                    self.statusMessage('Skipping %s' % file_name)
                else:
                    self.append(file_info)
        return

    def getDocsetBaseDirectory(self):
        "Returns the base directory for this documentation set."
        return self._docset_base_directory

    def getFileName(self):
        "Returns the \"filename\" of the documentation set."
        my_path = hdpath.removePrefix( self.getDocsetBaseDirectory(),
                                       self.getOutputBaseDirectory())
        return my_path

    def getOutputBaseDirectory(self):
        "Returns the base directory for all documentation sets."
        return self._output_base_directory

    def getClassInfo(self, className):
        "Returns class info if have it, None otherwise."
        return self._all_classes.get(className, None)

    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 write(self):
        """Write the documentation set to the output.

        Developers creating their own, new, docset types should
        override this method to cause the docset instance to
        generate its output.

        """
        self._requiredOfSubclass('write')
        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)
        return nameList

    def close(self):
        "Close the open documentation set."
        for f in self._open_handles:
            try:
                self.closeOutput(f)
            except:
                pass
        return

    def append(self, infoObject):
        """Add a module to the docset.
        """
        if infoObject.__class__ == self.__class__:
            #
            # Recursive package definition
            #
            self._all_packages[ infoObject.getPackageName() ] = infoObject
        else:
            #
            # Contained module definition
            #
            self._all_modules[ infoObject.getName() ] = infoObject
            for c in infoObject.getClassNames():
                self._all_classes[ c ] = infoObject.getClassInfo(c)
        #
        # Add to our list representation.
        #
        UserList.UserList.append(self, infoObject)
        return

    def openOutput(self, name, title, subtitle):
        """Open output for writing.
        
        Using this method to open output destinations
        means they will automatically be closed.

        Parameters

          name -- Name of output destination to open.

          title -- The main title to be given to the output (e.g.,
          HTML page title or other documentation title).

          subtitle -- A subtitle which should be applied to the
          output.

        See also 'closeOutput'.
        """
        self.statusMessage(
            '\tDocumenting : "%s"' % title, 2)
        self.statusMessage(
            '\t              "%s"' % subtitle, 3)
        self.statusMessage(
            '\t         to : %s' % name, 3)
        f = self._formatter.openOutput(name, title, subtitle)
        self._open_handles.append(f)
        return f
    
    def closeOutput(self, output):
        """Close the output handle.

        Parameters

          output -- A handle created by 'openOutput'.
        
        """
        self._formatter.closeOutput(output)
        return

    

    
