#! /usr/bin/env python
#
# FILE            $Id: gendoc.py,v 1.13 1998/05/20 17:21:44 dlarsson Exp $
#
# DESCRIPTION     Generate documentation from python files.
#
# AUTHOR          SEISY/LKSB Daniel Larsson
#
# 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 ABB Industrial Systems
# not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior permission.
#
# ABB INDUSTRIAL SYSTEMS DISCLAIMS ALL WARRANTIES WITH REGARD TO
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
# FITNESS, IN NO EVENT SHALL ABB INDUSTRIAL SYSTEMS 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.
#
# Copyright (C) ABB Industrial Systems AB, 1996
# Unpublished work.  All Rights Reserved.
#
# HISTORY:
# $Log: /Gendoc/gendoc.py $
#
# 3     98-05-25 22:18 Daniel
# Fixed formats list parsing.
# Revision 1.13  1998/05/20 17:21:44  dlarsson
# Fixed a few bugs.
#
# Revision 1.12  1998/05/19 19:41:16  dlarsson
# Merged in changes from home.
#
#
# 2     98-04-01 14:12 Daniel
# Added function calling interface. Removed "import ni".
# Revision 1.11  1998/02/04 17:55:13  dlarsson
# Changes for Python 1.5 (the ni module is obsolete now)
#
# Revision 1.10  1998/01/19 11:42:14  dlarsson
# Cosmetic changes
#
# Revision 1.9  1996/09/06 10:06:27  omfadmin
# Added -? (help) option. Prints the formatters found in the 'formatters'
# directory.
#
# Revision 1.8  1996/08/26  20:38:58  omfadmin
# Added -u option to include private names (Robin Friedrich).
#
# Revision 1.7  1996/07/12  16:13:30  omfadmin
# Small change in printout if the parser module is not present.
# Maybe I should revert to import mode automatically?
#
# Revision 1.6  1996/07/10  03:12:46  omfadmin
# - Added support for "keyword arguments" on the command line.
# - Improved exception handling.
# - Added skip_modules, a list of modules never imported.
#   Only contains the importall module for now.
#
# Revision 1.5  1996/07/08  05:27:37  omfadmin
# - Fixed usage documentation.
# - Added a -d switch to select output directory.
# - The generated index is now always called index.html.
# - Setup links between pages, so the formatter can generate
#   navigation buttons (or whatever).
#
# Revision 1.4  1996/06/24  09:54:16  omfadmin
# Moved index_page function to separate file.
# Changed the way formatter selection works. Used to be different
# one-character options (-m, -h, etc), but is now -f <formatter name>.
# Formatter modules moved to separate package to easy installation
# of new formatters.
#
# Revision 1.3  1996/06/21  00:16:53  omfadmin
# Moved packing of manual pages into a (module-manpage, [class-manpages])
# structure out of the ParseCollector class to this module.
#
# Revision 1.2  1996/06/13  18:11:13  omfadmin
# Added parse support.
#
#
#

"""
SYNOPSIS

 Usage: ~gendoc~ [-v] [-u] [-p|-i] [-h head] [-f format] [key=value ...] files ...

  **-d** *dir*     Save generated files in the *dir* directory.
  **-f** *format*  Generate files using the formatter *format*.
  **-h** *head*    Generate a head page with references to all generated pages (HTML only)
  **-p**         Parse source files for docstrings (default)
  **-i**         Import files for docstrings
  **-u**         Include private methods starting with single '_'
  **-v**         Verbose mode
  *key=value*  Pass formatter options.

DESCRIPTION

 *gendoc* scans one or several python modules for documentation strings.
 From the documentation strings, manual pages are generated in any of
 FrameMaker MIF, MML, HTML and ASCII formats.

 Note that you can give multiple format options at once. It is a lot
 faster, since the the source files are only parsed/imported and traversed
 once.

 The *key=value* options are used to send arguments to formatters. These
 definitions can be put in the environment as well. Currently only one
 such option is supported:

 FRAME=yes -- Generate top page as an HTML framed document.

FORMATTERS

 Currently the following formatters are supported:

 ASCII -- Produce simple ascii text
 HTML -- Produce a set of HTML files
 HTMLg -- Same as above using Robin Friedrich's HTMLgen module
 MIF -- FrameMaker's Maker Interchange Format
 MML -- FrameMaker's Maker Markup Language
"""

import sys, os, string

__author__  = "Daniel Larsson"
__version__ = "0.73"
version_string = "gendoc %s" % __version__
__usage__   = """
Usage: %s [-v] [-p|-i] [-h head] [-f format] [key=value ...] files ...

 -d dir     Save generated files in the dir directory.
 -f format  Generate files using the formatter format.
 -h head    Generate a head page with references to all
            generated pages (HTML only)
 -p         Parse source files for docstrings (default)
 -i         Import files for docstrings
 -u         Include private methods starting with single '_'
 -v         Verbose mode

""" % os.path.basename(sys.argv[0])


VERBOSE = 0

# Default directory for generated files
directory = ''

from formatters import Formatters

# Flag indicating if you want to do docs for private methods
include_private_methods = 0

# Some modules that I know are problematic to import:
skip_modules = ('importall', )

# Exceptions raised by gendoc
import exceptions
class GendocException(Exception): pass

def main_parseopts():
    import getopt, sys

    program = os.path.basename(sys.argv[0])

    global VERBOSE, directory
    global include_private_methods

    # Process command line arguments
    try:
	optlist, args = getopt.getopt(sys.argv[1:], '?ipuvd:h:f:')
    except getopt.error, str:
	sys.stderr.write('gendoc: %s\n' % str)
	sys.stderr.write(__usage__)
	sys.exit(-1)

    formats = []
    head = None
    parse = 0
    for opt in optlist:
	if opt[0] == '-?':
	    print version_string
	    print
	    print __doc__
	    frmtrs = reduce(lambda c, i: c+', '+i, Formatters.formatters.keys())
	    print "Valid formatters are:", frmtrs
	    sys.exit(0)
	elif opt[0] == '-v':
	    VERBOSE = VERBOSE+1
	elif opt[0] == '-i':
	    parse = 0
	elif opt[0] == '-p':
	    parse = 1
	elif opt[0] == '-d':
	    directory = opt[1]
	elif opt[0] == '-h':
	    head = opt[1]
	elif opt[0] == '-u':
	    include_private_methods = 1
	elif opt[0] == '-f':
	    try:
		#formats.append(Formatters.formatters[opt[1]]())
		formats.append(opt[1])
	    except KeyError, key:
		print "Couldn't find a %s formatter (is it in the formatters subdir?)" % key
		frmtrs = reduce(lambda c, i: c+', '+i, Formatters.formatters.keys())
		print "Valid formatters are:", frmtrs
		sys.exit(0)


    # Arguments in the '<key>=<value>' style are stripped, and stored in
    # os.environ. This is the way to pass arguments to formatters.
    envs = filter(lambda arg: string.find(arg, '=') != -1, args)
    args = filter(lambda arg: string.find(arg, '=') == -1, args)

    def add_dict(dict, str):
	[key, value] = string.splitfields(str, '=')
	dict[key] = value
	return dict

    try:
	# Believe it or not, this will put definitions into
	# os.environ (I *like* filter, map and reduce!).
	reduce(lambda d, s, f=add_dict: f(d, s), envs, os.environ)
    except ValueError:
	sys.stderr.write(program + ': error in variable definition\n')
	sys.stderr.write('(in one of ' + str(envs) + ')\n')
	sys.stderr.write('Syntax is "key=name"\n')


    if not args:
	sys.stderr.write(program + ': You must pass at least one python file\n')
	sys.exit(1)

    try:
	main(args, formats, head, parse, VERBOSE)
    except GendocException, x:
	import sys
	print x
	sys.stderr.write(x)
	sys.exit(0)


def main(modules, formats=None, head=None, parse=0, verbose=0):
    """Creates manual pages.

    Given a sequence of module names, this function generates
    a set of manual pages.

    *modules* -- A sequence of module names (as strings)

    *formats* -- A sequence of output formats (defaults to ['HTML'])

    *head* -- The title of the index page

    *parse* -- Boolean controlling whether parsing or importing mode
               will be deployed (defaults to false, i.e. importing mode)

    *verbose* -- Controls debugging printouts. 0 is silent, higher numbers
                 leads to more verbose runs.
    """

    if verbose:
	import ManualPage, doc_collect
	ManualPage.VERBOSE = verbose - 1
	doc_collect.VERBOSE = verbose

    if parse:
	collect = parse_collect
    else:
	collect = import_collect

    if not formats:
	formats = ('HTML',)

    _formats = []
    for format in formats:
	_formats.append(Formatters.formatters[format]())

    manpages = collect(modules, _formats, head)
    render_manpages(_formats, manpages, head)


def import_collect(files, formats, head):
    import doc_collect
    from string import splitfields

    if VERBOSE:
	doc_collect.VERBOSE = VERBOSE - 1
    doc_collect.include_private_methods = include_private_methods

    modules=[]
    for file in files:
	dir = os.path.dirname(file)
	if dir and dir not in sys.path:
	    sys.path.insert(0, dir)

	module = splitfields(os.path.basename(file), '.')[0]
	dict = {}

	# Skip certain modules that I know causes problems
	if module in skip_modules:
	    if VERBOSE:
		print "*** Skipping %s (In skip list)" % file
	    continue

	if VERBOSE:
	    print '*** Importing', module

	try:
	    exec 'import '+module in dict
	    modules.append(dict[module])
	except SyntaxError:
	    sys.stderr.write('*** Syntax errors in ' + file + '\n');
	except:
	    sys.stderr.write('*** Failed to import %s (%s, %s)\n' % (file, sys.exc_type, sys.exc_value) );



    if VERBOSE:
	print 'Collecting information'
    manpages = doc_collect.gendoc(doc_collect.doc_collect(modules))
    return manpages


def parse_collect(files, formats, head):
    import os
    from string import splitfields
    import ParseCollect
    from ParseCollect import ParseCollector

    if VERBOSE:
	ParseCollect.VERBOSE = VERBOSE - 1

    try:
	import parser
    except:
	import sys
	sys.stderr.write('You must include the parser module in your Setup\n')
	sys.stderr.write('to run in parser mode!\n')
	sys.stderr.write('You can run in import mode instead (-i)\n')
	sys.exit(1)


    if VERBOSE:
	print 'Parsing files'

    manpages = []
    for file in files:
	text = open(file).read()
	try:
	    ast_tuple = parser.ast2tuple(parser.suite(text))
	except:
	    import sys
	    sys.stderr.write("Sorry, got a syntax error when parsing %s\n" % file)

	module = splitfields(os.path.basename(file), '.')[0]
	collector = ParseCollector(module)
	pages = collector.walk(ast_tuple)
	manpages.append((pages[0], pages[1:]))
    return manpages


def render_manpages(formats, manpages, head):
    for formatter in formats:
	if VERBOSE:
	    print 'Generating documents for %s' % formatter.__class__.__name__
	files = render_pages(manpages, formatter)
	if head and formatter.file_ext in ['.html', '.htm']:
	    import os
	    print 'Generating index in %s' % os.path.join(directory, 'index' + formatter.file_ext)
	    from HTMLindex import index_page
	    modules = map(lambda page: page[0].name(), manpages)
	    index_page(directory, head, modules, files)

def render_pages(manpages, formatter):
    filenames = []
    for ix in range(len(manpages)):
	page = manpages[ix][0]
	if ix > 0:
	    prev = manpages[ix-1][0]
	else:
	    prev = None
	if ix < len(manpages)-1:
	    next = manpages[ix+1][0]
	else:
	    next = None

	filename = render_page(page, formatter, prev, next)

	# If this entry's length is more than one, it means it is a
	# module man page with some children man pages.
	if len(manpages[ix]) > 1:
	    child_files = []
	    child_pages = manpages[ix][1]
	    for chix in range(len(child_pages)):
		page = child_pages[chix]
		if chix > 0:
		    prev = child_pages[chix-1]
		else:
		    prev = None
		if chix < len(child_pages)-1:
		    next = child_pages[chix+1]
		else:
		    next = None
		child_files.append(render_page(page, formatter, prev, next))
	    filename = (filename, child_files)
	filenames.append(filename)

    return filenames

def render_page(manpage, formatter, prev, next):
    import os

    manpage.previous(prev)
    manpage.next(next)
    mantext = manpage.render(formatter)
    filename = manpage.name()+formatter.file_ext
    try:
	file = open(os.path.join(directory, filename), 'w')
    except IOError, str:
	import sys
	sys.stderr.write("%s: %s" % (os.path.join(directory, filename), str))
	sys.exit(1)

    file.write(mantext)
    file.close()
    return filename


if __name__ == '__main__':
	main()
