epydoc.cli

Package epydoc :: Module cli |
[hide private]
[frames] | no frames]

# Source Code for Module epydoc.cli

       1  # epydoc -- Command line interface
       2  #
       3  # Copyright (C) 2005 Edward Loper
       4  # Author: Edward Loper <edloper@loper.org>
       5  # URL: <http://epydoc.sf.net>
       6  #
       7  # $Id: cli.py 1678 2008-01-29 17:21:29Z edloper $
       8
       9  """
      10  Command-line interface for epydoc.  Abbreviated Usage::
      11
      12   epydoc [options] NAMES...
      13
      14       NAMES...                  The Python modules to document.
      15       --html                    Generate HTML output (default).
      16       --latex                   Generate LaTeX output.
      17       --pdf                     Generate pdf output, via LaTeX.
      18       -o DIR, --output DIR      The output directory.
      19       --inheritance STYLE       The format for showing inherited objects.
      20       -V, --version             Print the version of epydoc.
      21       -h, --help                Display a usage message.
      22
      23  Run \"epydoc --help\" for a complete option list.  See the epydoc(1)
      24  man page for more information.
      25
      26  Config Files
      27  ============
      28  Configuration files can be specified with the C{--config} option.
      29  These files are read using U{ConfigParser
      30  <http://docs.python.org/lib/module-ConfigParser.html>}.  Configuration
      31  files may set options or add names of modules to document.  Option
      32  names are (usually) identical to the long names of command line
      33  options.  To specify names to document, use any of the following
      34  option names::
      35
      36    module modules value values object objects
      37
      38  A simple example of a config file is::
      39
      40    [epydoc]
      41    modules: sys, os, os.path, re, %(MYSANDBOXPATH)/utilities.py
      42    name: Example
      43    graph: classtree
      44    introspect: no
      45
      46  All ConfigParser interpolations are done using local values and the
      47  environment variables.
      48
      49
      50  Verbosity Levels
      51  ================
      52  The C{-v} and C{-q} options increase and decrease verbosity,
      53  respectively.  The default verbosity level is zero.  The verbosity
      54  levels are currently defined as follows::
      55
      56                  Progress    Markup warnings   Warnings   Errors
      57   -3               none            no             no        no
      58   -2               none            no             no        yes
      59   -1               none            no             yes       yes
      60    0 (default)     bar             no             yes       yes
      61    1               bar             yes            yes       yes
      62    2               list            yes            yes       yes
      63  """
      64  __docformat__ = 'epytext en'
      65
      66  import sys, os, time, re, pickle, textwrap
      67  from glob import glob
      68  from optparse import OptionParser, OptionGroup, SUPPRESS_HELP
      69  import optparse
      70  import epydoc
      71  from epydoc import log
      72  from epydoc.util import wordwrap, run_subprocess, RunSubprocessError
      73  from epydoc.util import plaintext_to_html
      74  from epydoc.apidoc import UNKNOWN
      75  from epydoc.compat import *
      76  import ConfigParser
      77  from epydoc.docwriter.html_css import STYLESHEETS as CSS_STYLESHEETS
      78
      79  # This module is only available if Docutils are in the system
      80  try:
      81      from epydoc.docwriter import xlink
      82  except:
      83      xlink = None
      84
      85  INHERITANCE_STYLES = ('grouped', 'listed', 'included')
      86  GRAPH_TYPES = ('classtree', 'callgraph', 'umlclasstree')
      87  ACTIONS = ('html', 'text', 'latex', 'dvi', 'ps', 'pdf', 'check')
      88  DEFAULT_DOCFORMAT = 'epytext'
      89  PROFILER = 'profile' #: Which profiler to use: 'hotshot' or 'profile'
      90
      91  ######################################################################
      92  #{ Help Topics
      93  ######################################################################
      94
      95  DOCFORMATS = ('epytext', 'plaintext', 'restructuredtext', 'javadoc')
      96  HELP_TOPICS = {
      97      'docformat': textwrap.dedent('''\
      98          __docformat__ is a module variable that specifies the markup
      99          language for the docstrings in a module.  Its value is a
     100          string, consisting the name of a markup language, optionally
     101          followed by a language code (such as "en" for English).  Epydoc
     102          currently recognizes the following markup language names:
     103          ''' + ', '.join(DOCFORMATS)),
     104      'inheritance': textwrap.dedent('''\
     105          The following inheritance formats are currently supported:
     106              - grouped: inherited objects are gathered into groups,
     107                based on what class they were inherited from.
     108              - listed: inherited objects are listed in a short list
     109                at the end of their section.
     110              - included: inherited objects are mixed in with
     111                non-inherited objects.'''),
     112      'css': textwrap.dedent(
     113          'The following built-in CSS stylesheets are available:\n' +
     114          '\n'.join(['  %10s: %s' % (key, descr)
     115                     for (key, (sheet, descr))
     116                     in CSS_STYLESHEETS.items()])),
     117      #'checks': textwrap.dedent('''\
     118      #
     119      #    '''),
     120      }
     121
     122
     123  HELP_TOPICS['topics'] = wordwrap(
     124      'Epydoc can provide additional help for the following topics: ' +
     125      ', '.join(['%r' % topic for topic in HELP_TOPICS.keys()]))
     126
     127  ######################################################################
     128  #{ Argument & Config File Parsing
     129  ######################################################################
     130
     131  OPTION_DEFAULTS = dict(
     132      action="html", show_frames=True, docformat=DEFAULT_DOCFORMAT,
     133      show_private=True, show_imports=False, inheritance="listed",
     134      verbose=0, quiet=0, load_pickle=False, parse=True, introspect=True,
     135      debug=epydoc.DEBUG, profile=False, graphs=[],
     136      list_classes_separately=False, graph_font=None, graph_font_size=None,
     137      include_source_code=True, pstat_files=[], simple_term=False, fail_on=None,
     138      exclude=[], exclude_parse=[], exclude_introspect=[],
     139      external_api=[], external_api_file=[], external_api_root=[],
     140      redundant_details=False, src_code_tab_width=8)
     141
     142 -def parse_arguments():
     143      # Construct the option parser.
     144      usage = '%prog [ACTION] [options] NAMES...'
     145      version = "Epydoc, version %s" % epydoc.__version__
     146      optparser = OptionParser(usage=usage, add_help_option=False)
     147
     148      optparser.add_option('--config',
     149          action='append', dest="configfiles", metavar='FILE',
     150          help=("A configuration file, specifying additional OPTIONS "
     151                "and/or NAMES.  This option may be repeated."))
     152
     153      optparser.add_option("--output", "-o",
     154          dest="target", metavar="PATH",
     155          help="The output directory.  If PATH does not exist, then "
     156          "it will be created.")
     157
     158      optparser.add_option("--quiet", "-q",
     159          action="count", dest="quiet",
     160          help="Decrease the verbosity.")
     161
     162      optparser.add_option("--verbose", "-v",
     163          action="count", dest="verbose",
     164          help="Increase the verbosity.")
     165
     166      optparser.add_option("--debug",
     167          action="store_true", dest="debug",
     168          help="Show full tracebacks for internal errors.")
     169
     170      optparser.add_option("--simple-term",
     171          action="store_true", dest="simple_term",
     172          help="Do not try to use color or cursor control when displaying "
     173          "the progress bar, warnings, or errors.")
     174
     175
     176      action_group = OptionGroup(optparser, 'Actions')
     177      optparser.add_option_group(action_group)
     178
     179      action_group.add_option("--html",
     180          action="store_const", dest="action", const="html",
     181          help="Write HTML output.")
     182
     183      action_group.add_option("--text",
     184          action="store_const", dest="action", const="text",
     185          help="Write plaintext output. (not implemented yet)")
     186
     187      action_group.add_option("--latex",
     188          action="store_const", dest="action", const="latex",
     189          help="Write LaTeX output.")
     190
     191      action_group.add_option("--dvi",
     192          action="store_const", dest="action", const="dvi",
     193          help="Write DVI output.")
     194
     195      action_group.add_option("--ps",
     196          action="store_const", dest="action", const="ps",
     197          help="Write Postscript output.")
     198
     199      action_group.add_option("--pdf",
     200          action="store_const", dest="action", const="pdf",
     201          help="Write PDF output.")
     202
     203      action_group.add_option("--check",
     204          action="store_const", dest="action", const="check",
     205          help="Check completeness of docs.")
     206
     207      action_group.add_option("--pickle",
     208          action="store_const", dest="action", const="pickle",
     209          help="Write the documentation to a pickle file.")
     210
     211      # Provide our own --help and --version options.
     212      action_group.add_option("--version",
     213          action="store_const", dest="action", const="version",
     214          help="Show epydoc's version number and exit.")
     215
     216      action_group.add_option("-h", "--help",
     217          action="store_const", dest="action", const="help",
     218          help="Show this message and exit.  For help on specific "
     219          "topics, use \"--help TOPIC\".  Use \"--help topics\" for a "
     220          "list of available help topics")
     221
     222
     223      generation_group = OptionGroup(optparser, 'Generation Options')
     224      optparser.add_option_group(generation_group)
     225
     226      generation_group.add_option("--docformat",
     227          dest="docformat", metavar="NAME",
     228          help="The default markup language for docstrings.  Defaults "
     229          "to \"%s\"." % DEFAULT_DOCFORMAT)
     230
     231      generation_group.add_option("--parse-only",
     232          action="store_false", dest="introspect",
     233          help="Get all information from parsing (don't introspect)")
     234
     235      generation_group.add_option("--introspect-only",
     236          action="store_false", dest="parse",
     237          help="Get all information from introspecting (don't parse)")
     238
     239      generation_group.add_option("--exclude",
     240          dest="exclude", metavar="PATTERN", action="append",
     241          help="Exclude modules whose dotted name matches "
     242               "the regular expression PATTERN")
     243
     244      generation_group.add_option("--exclude-introspect",
     245          dest="exclude_introspect", metavar="PATTERN", action="append",
     246          help="Exclude introspection of modules whose dotted name matches "
     247               "the regular expression PATTERN")
     248
     249      generation_group.add_option("--exclude-parse",
     250          dest="exclude_parse", metavar="PATTERN", action="append",
     251          help="Exclude parsing of modules whose dotted name matches "
     252               "the regular expression PATTERN")
     253
     254      generation_group.add_option("--inheritance",
     255          dest="inheritance", metavar="STYLE",
     256          help="The format for showing inheritance objects.  STYLE "
     257          "should be one of: %s." % ', '.join(INHERITANCE_STYLES))
     258
     259      generation_group.add_option("--show-private",
     260          action="store_true", dest="show_private",
     261          help="Include private variables in the output. (default)")
     262
     263      generation_group.add_option("--no-private",
     264          action="store_false", dest="show_private",
     265          help="Do not include private variables in the output.")
     266
     267      generation_group.add_option("--show-imports",
     268          action="store_true", dest="show_imports",
     269          help="List each module's imports.")
     270
     271      generation_group.add_option("--no-imports",
     272          action="store_false", dest="show_imports",
     273          help="Do not list each module's imports. (default)")
     274
     275      generation_group.add_option('--show-sourcecode',
     276          action='store_true', dest='include_source_code',
     277          help=("Include source code with syntax highlighting in the "
     278                "HTML output. (default)"))
     279
     280      generation_group.add_option('--no-sourcecode',
     281          action='store_false', dest='include_source_code',
     282          help=("Do not include source code with syntax highlighting in the "
     283                "HTML output."))
     284
     285      generation_group.add_option('--include-log',
     286          action='store_true', dest='include_log',
     287          help=("Include a page with the process log (epydoc-log.html)"))
     288
     289      generation_group.add_option(
     290          '--redundant-details',
     291          action='store_true', dest='redundant_details',
     292          help=("Include values in the details lists even if all info "
     293                "about them is already provided by the summary table."))
     294
     295      output_group = OptionGroup(optparser, 'Output Options')
     296      optparser.add_option_group(output_group)
     297
     298      output_group.add_option("--name", "-n",
     299          dest="prj_name", metavar="NAME",
     300          help="The documented project's name (for the navigation bar).")
     301
     302      output_group.add_option("--css", "-c",
     303          dest="css", metavar="STYLESHEET",
     304          help="The CSS stylesheet.  STYLESHEET can be either a "
     305          "builtin stylesheet or the name of a CSS file.")
     306
     307      output_group.add_option("--url", "-u",
     308          dest="prj_url", metavar="URL",
     309          help="The documented project's URL (for the navigation bar).")
     310
     311      output_group.add_option("--navlink",
     312          dest="prj_link", metavar="HTML",
     313          help="HTML code for a navigation link to place in the "
     314          "navigation bar.")
     315
     316      output_group.add_option("--top",
     317          dest="top_page", metavar="PAGE",
     318          help="The \"top\" page for the HTML documentation.  PAGE can "
     319          "be a URL, the name of a module or class, or one of the "
     320          "special names \"trees.html\", \"indices.html\", or \"help.html\"")
     321
     322      output_group.add_option("--help-file",
     323          dest="help_file", metavar="FILE",
     324          help="An alternate help file.  FILE should contain the body "
     325          "of an HTML file -- navigation bars will be added to it.")
     326
     327      output_group.add_option("--show-frames",
     328          action="store_true", dest="show_frames",
     329          help="Include frames in the HTML output. (default)")
     330
     331      output_group.add_option("--no-frames",
     332          action="store_false", dest="show_frames",
     333          help="Do not include frames in the HTML output.")
     334
     335      output_group.add_option('--separate-classes',
     336          action='store_true', dest='list_classes_separately',
     337          help=("When generating LaTeX or PDF output, list each class in "
     338                "its own section, instead of listing them under their "
     339                "containing module."))
     340
     341      output_group.add_option('--src-code-tab-width',
     342          action='store', type='int', dest='src_code_tab_width',
     343          help=("When generating HTML output, sets the number of spaces "
     344                "each tab in source code listings is replaced with."))
     345
     346      # The group of external API options.
     347      # Skip if the module couldn't be imported (usually missing docutils)
     348      if xlink is not None:
     349          link_group = OptionGroup(optparser,
     350                                   xlink.ApiLinkReader.settings_spec[0])
     351          optparser.add_option_group(link_group)
     352
     353          for help, names, opts in xlink.ApiLinkReader.settings_spec[2]:
     354              opts = opts.copy()
     355              opts['help'] = help
     356              link_group.add_option(*names, **opts)
     357
     358      graph_group = OptionGroup(optparser, 'Graph Options')
     359      optparser.add_option_group(graph_group)
     360
     361      graph_group.add_option('--graph',
     362          action='append', dest='graphs', metavar='GRAPHTYPE',
     363          help=("Include graphs of type GRAPHTYPE in the generated output.  "
     364                "Graphs are generated using the Graphviz dot executable.  "
     365                "If this executable is not on the path, then use --dotpath "
     366                "to specify its location.  This option may be repeated to "
     367                "include multiple graph types in the output.  GRAPHTYPE "
     368                "should be one of: all, %s." % ', '.join(GRAPH_TYPES)))
     369
     370      graph_group.add_option("--dotpath",
     371          dest="dotpath", metavar='PATH',
     372          help="The path to the Graphviz 'dot' executable.")
     373
     374      graph_group.add_option('--graph-font',
     375          dest='graph_font', metavar='FONT',
     376          help=("Specify the font used to generate Graphviz graphs.  (e.g., "
     377                "helvetica or times)."))
     378
     379      graph_group.add_option('--graph-font-size',
     380          dest='graph_font_size', metavar='SIZE',
     381          help=("Specify the font size used to generate Graphviz graphs, "
     382                "in points."))
     383
     384      graph_group.add_option('--pstat',
     385          action='append', dest='pstat_files', metavar='FILE',
     386          help="A pstat output file, to be used in generating call graphs.")
     387
     388      # this option is for developers, not users.
     389      graph_group.add_option("--profile-epydoc",
     390          action="store_true", dest="profile",
     391          help=SUPPRESS_HELP or
     392               ("Run the hotshot profiler on epydoc itself.  Output "
     393                "will be written to profile.out."))
     394
     395
     396      return_group = OptionGroup(optparser, 'Return Value Options')
     397      optparser.add_option_group(return_group)
     398
     399      return_group.add_option("--fail-on-error",
     400          action="store_const", dest="fail_on", const=log.ERROR,
     401          help="Return a non-zero exit status, indicating failure, if any "
     402          "errors are encountered.")
     403
     404      return_group.add_option("--fail-on-warning",
     405          action="store_const", dest="fail_on", const=log.WARNING,
     406          help="Return a non-zero exit status, indicating failure, if any "
     407          "errors or warnings are encountered (not including docstring "
     408          "warnings).")
     409
     410      return_group.add_option("--fail-on-docstring-warning",
     411          action="store_const", dest="fail_on", const=log.DOCSTRING_WARNING,
     412          help="Return a non-zero exit status, indicating failure, if any "
     413          "errors or warnings are encountered (including docstring "
     414          "warnings).")
     415
     416      # Set the option parser's defaults.
     417      optparser.set_defaults(**OPTION_DEFAULTS)
     418
     419      # Parse the arguments.
     420      options, names = optparser.parse_args()
     421
     422      # Print help message, if requested.  We also provide support for
     423      # --help [topic]
     424      if options.action == 'help':
     425          names = set([n.lower() for n in names])
     426          for (topic, msg) in HELP_TOPICS.items():
     427              if topic.lower() in names:
     428                  print '\n' + msg.rstrip() + '\n'
     429                  sys.exit(0)
     430          optparser.print_help()
     431          sys.exit(0)
     432
     433      # Print version message, if requested.
     434      if options.action == 'version':
     435          print version
     436          sys.exit(0)
     437
     438      # Process any config files.
     439      if options.configfiles:
     440          try:
     441              parse_configfiles(options.configfiles, options, names)
     442          except (KeyboardInterrupt,SystemExit): raise
     443          except Exception, e:
     444              if len(options.configfiles) == 1:
     445                  cf_name = 'config file %s' % options.configfiles[0]
     446              else:
     447                  cf_name = 'config files %s' % ', '.join(options.configfiles)
     448              optparser.error('Error reading %s:\n    %s' % (cf_name, e))
     449
     450      # Check if the input file is a pickle file.
     451      for name in names:
     452          if name.endswith('.pickle'):
     453              if len(names) != 1:
     454                  optparser.error("When a pickle file is specified, no other "
     455                                 "input files may be specified.")
     456              options.load_pickle = True
     457
     458      # Check to make sure all options are valid.
     459      if len(names) == 0:
     460          optparser.error("No names specified.")
     461
     462      # perform shell expansion.
     463      for i, name in reversed(list(enumerate(names[:]))):
     464          if '?' in name or '*' in name:
     465              names[i:i+1] = glob(name)
     466
     467      if options.inheritance not in INHERITANCE_STYLES:
     468          optparser.error("Bad inheritance style.  Valid options are " +
     469                          ",".join(INHERITANCE_STYLES))
     470      if not options.parse and not options.introspect:
     471          optparser.error("Invalid option combination: --parse-only "
     472                          "and --introspect-only.")
     473      if options.action == 'text' and len(names) > 1:
     474          optparser.error("--text option takes only one name.")
     475
     476      # Check the list of requested graph types to make sure they're
     477      # acceptable.
     478      options.graphs = [graph_type.lower() for graph_type in options.graphs]
     479      for graph_type in options.graphs:
     480          if graph_type == 'callgraph' and not options.pstat_files:
     481              optparser.error('"callgraph" graph type may only be used if '
     482                              'one or more pstat files are specified.')
     483          # If it's 'all', then add everything (but don't add callgraph if
     484          # we don't have any profiling info to base them on).
     485          if graph_type == 'all':
     486              if options.pstat_files:
     487                  options.graphs = GRAPH_TYPES
     488              else:
     489                  options.graphs = [g for g in GRAPH_TYPES if g != 'callgraph']
     490              break
     491          elif graph_type not in GRAPH_TYPES:
     492              optparser.error("Invalid graph type %s." % graph_type)
     493
     494      # Calculate verbosity.
     495      verbosity = getattr(options, 'verbosity', 0)
     496      options.verbosity = verbosity + options.verbose - options.quiet
     497
     498      # The target default depends on the action.
     499      if options.target is None:
     500          options.target = options.action
     501
     502      # Return parsed args.
     503      options.names = names
     504      return options, names
     505
     506 -def parse_configfiles(configfiles, options, names):
     507      configparser = ConfigParser.ConfigParser()
     508      # ConfigParser.read() silently ignores errors, so open the files
     509      # manually (since we want to notify the user of any errors).
     510      for configfile in configfiles:
     511          fp = open(configfile, 'r') # may raise IOError.
     512          configparser.readfp(fp, configfile)
     513          fp.close()
     514      for optname in configparser.options('epydoc'):
     515          val = configparser.get('epydoc', optname, vars=os.environ).strip()
     516          optname = optname.lower().strip()
     517
     518          if optname in ('modules', 'objects', 'values',
     519                         'module', 'object', 'value'):
     520              names.extend(_str_to_list(val))
     521          elif optname == 'target':
     522              options.target = val
     523          elif optname == 'output':
     524              if val.lower() not in ACTIONS:
     525                  raise ValueError('"%s" expected one of: %s' %
     526                                   (optname, ', '.join(ACTIONS)))
     527              options.action = val.lower()
     528          elif optname == 'verbosity':
     529              options.verbosity = _str_to_int(val, optname)
     530          elif optname == 'debug':
     531              options.debug = _str_to_bool(val, optname)
     532          elif optname in ('simple-term', 'simple_term'):
     533              options.simple_term = _str_to_bool(val, optname)
     534
     535          # Generation options
     536          elif optname == 'docformat':
     537              options.docformat = val
     538          elif optname == 'parse':
     539              options.parse = _str_to_bool(val, optname)
     540          elif optname == 'introspect':
     541              options.introspect = _str_to_bool(val, optname)
     542          elif optname == 'exclude':
     543              options.exclude.extend(_str_to_list(val))
     544          elif optname in ('exclude-parse', 'exclude_parse'):
     545              options.exclude_parse.extend(_str_to_list(val))
     546          elif optname in ('exclude-introspect', 'exclude_introspect'):
     547              options.exclude_introspect.extend(_str_to_list(val))
     548          elif optname == 'inheritance':
     549              if val.lower() not in INHERITANCE_STYLES:
     550                  raise ValueError('"%s" expected one of: %s.' %
     551                                   (optname, ', '.join(INHERITANCE_STYLES)))
     552              options.inheritance = val.lower()
     553          elif optname =='private':
     554              options.show_private = _str_to_bool(val, optname)
     555          elif optname =='imports':
     556              options.show_imports = _str_to_bool(val, optname)
     557          elif optname == 'sourcecode':
     558              options.include_source_code = _str_to_bool(val, optname)
     559          elif optname in ('include-log', 'include_log'):
     560              options.include_log = _str_to_bool(val, optname)
     561          elif optname in ('redundant-details', 'redundant_details'):
     562              options.redundant_details = _str_to_bool(val, optname)
     563
     564          # Output options
     565          elif optname == 'name':
     566              options.prj_name = val
     567          elif optname == 'css':
     568              options.css = val
     569          elif optname == 'url':
     570              options.prj_url = val
     571          elif optname == 'link':
     572              options.prj_link = val
     573          elif optname == 'top':
     574              options.top_page = val
     575          elif optname == 'help':
     576              options.help_file = val
     577          elif optname =='frames':
     578              options.show_frames = _str_to_bool(val, optname)
     579          elif optname in ('separate-classes', 'separate_classes'):
     580              options.list_classes_separately = _str_to_bool(val, optname)
     581          elif optname in ('src-code-tab-width', 'src_code_tab_width'):
     582              options.src_code_tab_width = _str_to_int(val, optname)
     583
     584          # External API
     585          elif optname in ('external-api', 'external_api'):
     586              options.external_api.extend(_str_to_list(val))
     587          elif optname in ('external-api-file', 'external_api_file'):
     588              options.external_api_file.extend(_str_to_list(val))
     589          elif optname in ('external-api-root', 'external_api_root'):
     590              options.external_api_root.extend(_str_to_list(val))
     591
     592          # Graph options
     593          elif optname == 'graph':
     594              graphtypes = _str_to_list(val)
     595              for graphtype in graphtypes:
     596                  if graphtype not in GRAPH_TYPES + ('all',):
     597                      raise ValueError('"%s" expected one of: all, %s.' %
     598                                       (optname, ', '.join(GRAPH_TYPES)))
     599              options.graphs.extend(graphtypes)
     600          elif optname == 'dotpath':
     601              options.dotpath = val
     602          elif optname in ('graph-font', 'graph_font'):
     603              options.graph_font = val
     604          elif optname in ('graph-font-size', 'graph_font_size'):
     605              options.graph_font_size = _str_to_int(val, optname)
     606          elif optname == 'pstat':
     607              options.pstat_files.extend(_str_to_list(val))
     608
     609          # Return value options
     610          elif optname in ('failon', 'fail-on', 'fail_on'):
     611              if val.lower().strip() in ('error', 'errors'):
     612                  options.fail_on = log.ERROR
     613              elif val.lower().strip() in ('warning', 'warnings'):
     614                  options.fail_on = log.WARNING
     615              elif val.lower().strip() in ('docstring_warning',
     616                                           'docstring_warnings'):
     617                  options.fail_on = log.DOCSTRING_WARNING
     618              else:
     619                  raise ValueError("%r expected one of: error, warning, "
     620                                   "docstring_warning" % optname)
     621          else:
     622              raise ValueError('Unknown option %s' % optname)
     623
     624 -def _str_to_bool(val, optname):
     625      if val.lower() in ('0', 'no', 'false', 'n', 'f', 'hide'):
     626          return False
     627      elif val.lower() in ('1', 'yes', 'true', 'y', 't', 'show'):
     628          return True
     629      else:
     630          raise ValueError('"%s" option expected a boolean' % optname)
     631
     632 -def _str_to_int(val, optname):
     633      try:
     634          return int(val)
     635      except ValueError:
     636          raise ValueError('"%s" option expected an int' % optname)
     637
     638 -def _str_to_list(val):
     639      return val.replace(',', ' ').split()
     640
     641  ######################################################################
     642  #{ Interface
     643  ######################################################################
     644
     645 -def main(options, names):
     646      # Set the debug flag, if '--debug' was specified.
     647      if options.debug:
     648          epydoc.DEBUG = True
     649
     650      ## [XX] Did this serve a purpose?  Commenting out for now:
     651      #if options.action == 'text':
     652      #    if options.parse and options.introspect:
     653      #        options.parse = False
     654
     655      # Set up the logger
     656      if options.simple_term:
     657          TerminalController.FORCE_SIMPLE_TERM = True
     658      if options.action == 'text':
     659          logger = None # no logger for text output.
     660      elif options.verbosity > 1:
     661          logger = ConsoleLogger(options.verbosity)
     662          log.register_logger(logger)
     663      else:
     664          # Each number is a rough approximation of how long we spend on
     665          # that task, used to divide up the unified progress bar.
     666          stages = [40,  # Building documentation
     667                    7,   # Merging parsed & introspected information
     668                    1,   # Linking imported variables
     669                    3,   # Indexing documentation
     670                    1,   # Checking for overridden methods
     671                    30,  # Parsing Docstrings
     672                    1,   # Inheriting documentation
     673                    2]   # Sorting & Grouping
     674          if options.load_pickle:
     675              stages = [30] # Loading pickled documentation
     676          if options.action == 'html': stages += [100]
     677          elif options.action == 'text': stages += [30]
     678          elif options.action == 'latex': stages += [60]
     679          elif options.action == 'dvi': stages += [60,30]
     680          elif options.action == 'ps': stages += [60,40]
     681          elif options.action == 'pdf': stages += [60,50]
     682          elif options.action == 'check': stages += [10]
     683          elif options.action == 'pickle': stages += [10]
     684          else: raise ValueError, '%r not supported' % options.action
     685          if options.parse and not options.introspect:
     686              del stages[1] # no merging
     687          if options.introspect and not options.parse:
     688              del stages[1:3] # no merging or linking
     689          logger = UnifiedProgressConsoleLogger(options.verbosity, stages)
     690          log.register_logger(logger)
     691
     692      # check the output directory.
     693      if options.action not in ('text', 'check', 'pickle'):
     694          if os.path.exists(options.target):
     695              if not os.path.isdir(options.target):
     696                  log.error("%s is not a directory" % options.target)
     697                  sys.exit(1)
     698
     699      if options.include_log:
     700          if options.action == 'html':
     701              if not os.path.exists(options.target):
     702                  os.mkdir(options.target)
     703              log.register_logger(HTMLLogger(options.target, options))
     704          else:
     705              log.warning("--include-log requires --html")
     706
     707      # Set the default docformat
     708      from epydoc import docstringparser
     709      docstringparser.DEFAULT_DOCFORMAT = options.docformat
     710
     711      # Configure the external API linking
     712      if xlink is not None:
     713          try:
     714              xlink.ApiLinkReader.read_configuration(options, problematic=False)
     715          except Exception, exc:
     716              log.error("Error while configuring external API linking: %s: %s"
     717                  % (exc.__class__.__name__, exc))
     718
     719      # Set the dot path
     720      if options.dotpath:
     721          from epydoc.docwriter import dotgraph
     722          dotgraph.DOT_COMMAND = options.dotpath
     723
     724      # Set the default graph font & size
     725      if options.graph_font:
     726          from epydoc.docwriter import dotgraph
     727          fontname = options.graph_font
     728          dotgraph.DotGraph.DEFAULT_NODE_DEFAULTS['fontname'] = fontname
     729          dotgraph.DotGraph.DEFAULT_EDGE_DEFAULTS['fontname'] = fontname
     730      if options.graph_font_size:
     731          from epydoc.docwriter import dotgraph
     732          fontsize = options.graph_font_size
     733          dotgraph.DotGraph.DEFAULT_NODE_DEFAULTS['fontsize'] = fontsize
     734          dotgraph.DotGraph.DEFAULT_EDGE_DEFAULTS['fontsize'] = fontsize
     735
     736      # If the input name is a pickle file, then read the docindex that
     737      # it contains.  Otherwise, build the docs for the input names.
     738      if options.load_pickle:
     739          assert len(names) == 1
     740          log.start_progress('Deserializing')
     741          log.progress(0.1, 'Loading %r' % names[0])
     742          t0 = time.time()
     743          unpickler = pickle.Unpickler(open(names[0], 'rb'))
     744          unpickler.persistent_load = pickle_persistent_load
     745          docindex = unpickler.load()
     746          log.debug('deserialization time: %.1f sec' % (time.time()-t0))
     747          log.end_progress()
     748      else:
     749          # Build docs for the named values.
     750          from epydoc.docbuilder import build_doc_index
     751          exclude_parse = '|'.join(options.exclude_parse+options.exclude)
     752          exclude_introspect = '|'.join(options.exclude_introspect+
     753                                        options.exclude)
     754          docindex = build_doc_index(names, options.introspect, options.parse,
     755                                     add_submodules=(options.action!='text'),
     756                                     exclude_introspect=exclude_introspect,
     757                                     exclude_parse=exclude_parse)
     758
     759      if docindex is None:
     760          if log.ERROR in logger.reported_message_levels:
     761              sys.exit(1)
     762          else:
     763              return # docbuilder already logged an error.
     764
     765      # Load profile information, if it was given.
     766      if options.pstat_files:
     767          try: import pstats
     768          except ImportError:
     769              log.error("Could not import pstats -- ignoring pstat files.")
     770          try:
     771              profile_stats = pstats.Stats(options.pstat_files[0])
     772              for filename in options.pstat_files[1:]:
     773                  profile_stats.add(filename)
     774          except KeyboardInterrupt: raise
     775          except Exception, e:
     776              log.error("Error reading pstat file: %s" % e)
     777              profile_stats = None
     778          if profile_stats is not None:
     779              docindex.read_profiling_info(profile_stats)
     780
     781      # Perform the specified action.
     782      if options.action == 'html':
     783          write_html(docindex, options)
     784      elif options.action in ('latex', 'dvi', 'ps', 'pdf'):
     785          write_latex(docindex, options, options.action)
     786      elif options.action == 'text':
     787          write_text(docindex, options)
     788      elif options.action == 'check':
     789          check_docs(docindex, options)
     790      elif options.action == 'pickle':
     791          write_pickle(docindex, options)
     792      else:
     793          print >>sys.stderr, '\nUnsupported action %s!' % options.action
     794
     795      # If we suppressed docstring warnings, then let the user know.
     796      if logger is not None and logger.suppressed_docstring_warning:
     797          if logger.suppressed_docstring_warning == 1:
     798              prefix = '1 markup error was found'
     799          else:
     800              prefix = ('%d markup errors were found' %
     801                        logger.suppressed_docstring_warning)
     802          log.warning("%s while processing docstrings.  Use the verbose "
     803                      "switch (-v) to display markup errors." % prefix)
     804
     805      # Basic timing breakdown:
     806      if options.verbosity >= 2 and logger is not None:
     807          logger.print_times()
     808
     809      # If we encountered any message types that we were requested to
     810      # fail on, then exit with status 2.
     811      if options.fail_on is not None:
     812          max_reported_message_level = max(logger.reported_message_levels)
     813          if max_reported_message_level >= options.fail_on:
     814              sys.exit(2)
     815
     816 -def write_html(docindex, options):
     817      from epydoc.docwriter.html import HTMLWriter
     818      html_writer = HTMLWriter(docindex, **options.__dict__)
     819      if options.verbose > 0:
     820          log.start_progress('Writing HTML docs to %r' % options.target)
     821      else:
     822          log.start_progress('Writing HTML docs')
     823      html_writer.write(options.target)
     824      log.end_progress()
     825
     826 -def write_pickle(docindex, options):
     827      """Helper for writing output to a pickle file, which can then be
     828      read in at a later time.  But loading the pickle is only marginally
     829      faster than building the docs from scratch, so this has pretty
     830      limited application."""
     831      if options.target == 'pickle':
     832          options.target = 'api.pickle'
     833      elif not options.target.endswith('.pickle'):
     834          options.target += '.pickle'
     835
     836      log.start_progress('Serializing output')
     837      log.progress(0.2, 'Writing %r' % options.target)
     838      outfile = open(options.target, 'wb')
     839      pickler = pickle.Pickler(outfile, protocol=0)
     840      pickler.persistent_id = pickle_persistent_id
     841      pickler.dump(docindex)
     842      outfile.close()
     843      log.end_progress()
     844
     845 -def pickle_persistent_id(obj):
     846      """Helper for pickling, which allows us to save and restore UNKNOWN,
     847      which is required to be identical to apidoc.UNKNOWN."""
     848      if obj is UNKNOWN: return 'UNKNOWN'
     849      else: return None
     850
     851 -def pickle_persistent_load(identifier):
     852      """Helper for pickling, which allows us to save and restore UNKNOWN,
     853      which is required to be identical to apidoc.UNKNOWN."""
     854      if identifier == 'UNKNOWN': return UNKNOWN
     855      else: raise pickle.UnpicklingError, 'Invalid persistent id'
     856
     857  _RERUN_LATEX_RE = re.compile(r'(?im)^LaTeX\s+Warning:\s+Label\(s\)\s+may'
     858                               r'\s+have\s+changed.\s+Rerun')
     859
     860 -def write_latex(docindex, options, format):
     861      from epydoc.docwriter.latex import LatexWriter
     862      latex_writer = LatexWriter(docindex, **options.__dict__)
     863      log.start_progress('Writing LaTeX docs')
     864      latex_writer.write(options.target)
     865      log.end_progress()
     866      # If we're just generating the latex, and not any output format,
     867      # then we're done.
     868      if format == 'latex': return
     869
     870      if format == 'dvi': steps = 4
     871      elif format == 'ps': steps = 5
     872      elif format == 'pdf': steps = 6
     873
     874      log.start_progress('Processing LaTeX docs')
     875      oldpath = os.path.abspath(os.curdir)
     876      running = None # keep track of what we're doing.
     877      try:
     878          try:
     879              os.chdir(options.target)
     880
     881              # Clear any old files out of the way.
     882              for ext in 'tex aux log out idx ilg toc ind'.split():
     883                  if os.path.exists('apidoc.%s' % ext):
     884                      os.remove('apidoc.%s' % ext)
     885
     886              # The first pass generates index files.
     887              running = 'latex'
     888              log.progress(0./steps, 'LaTeX: First pass')
     889              run_subprocess('latex api.tex')
     890
     891              # Build the index.
     892              running = 'makeindex'
     893              log.progress(1./steps, 'LaTeX: Build index')
     894              run_subprocess('makeindex api.idx')
     895
     896              # The second pass generates our output.
     897              running = 'latex'
     898              log.progress(2./steps, 'LaTeX: Second pass')
     899              out, err = run_subprocess('latex api.tex')
     900
     901              # The third pass is only necessary if the second pass
     902              # changed what page some things are on.
     903              running = 'latex'
     904              if _RERUN_LATEX_RE.match(out):
     905                  log.progress(3./steps, 'LaTeX: Third pass')
     906                  out, err = run_subprocess('latex api.tex')
     907
     908              # A fourth path should (almost?) never be necessary.
     909              running = 'latex'
     910              if _RERUN_LATEX_RE.match(out):
     911                  log.progress(3./steps, 'LaTeX: Fourth pass')
     912                  run_subprocess('latex api.tex')
     913
     914              # If requested, convert to postscript.
     915              if format in ('ps', 'pdf'):
     916                  running = 'dvips'
     917                  log.progress(4./steps, 'dvips')
     918                  run_subprocess('dvips api.dvi -o api.ps -G0 -Ppdf')
     919
     920              # If requested, convert to pdf.
     921              if format in ('pdf'):
     922                  running = 'ps2pdf'
     923                  log.progress(5./steps, 'ps2pdf')
     924                  run_subprocess(
     925                      'ps2pdf -sPAPERSIZE#letter -dMaxSubsetPct#100 '
     926                      '-dSubsetFonts#true -dCompatibilityLevel#1.2 '
     927                      '-dEmbedAllFonts#true api.ps api.pdf')
     928          except RunSubprocessError, e:
     929              if running == 'latex':
     930                  e.out = re.sub(r'(?sm)\A.*?!( LaTeX Error:)?', r'', e.out)
     931                  e.out = re.sub(r'(?sm)\s*Type X to quit.*', '', e.out)
     932                  e.out = re.sub(r'(?sm)^! Emergency stop.*', '', e.out)
     933              log.error("%s failed: %s" % (running, (e.out+e.err).lstrip()))
     934          except OSError, e:
     935              log.error("%s failed: %s" % (running, e))
     936      finally:
     937          os.chdir(oldpath)
     938          log.end_progress()
     939
     940 -def write_text(docindex, options):
     941      log.start_progress('Writing output')
     942      from epydoc.docwriter.plaintext import PlaintextWriter
     943      plaintext_writer = PlaintextWriter()
     944      s = ''
     945      for apidoc in docindex.root:
     946          s += plaintext_writer.write(apidoc)
     947      log.end_progress()
     948      if isinstance(s, unicode):
     949          s = s.encode('ascii', 'backslashreplace')
     950      print s
     951
     952 -def check_docs(docindex, options):
     953      from epydoc.checker import DocChecker
     954      DocChecker(docindex).check()
     955
     956 -def cli():
     957      # Parse command-line arguments.
     958      options, names = parse_arguments()
     959
     960      try:
     961          try:
     962              if options.profile:
     963                  _profile()
     964              else:
     965                  main(options, names)
     966          finally:
     967              log.close()
     968      except SystemExit:
     969          raise
     970      except KeyboardInterrupt:
     971          print '\n\n'
     972          print >>sys.stderr, 'Keyboard interrupt.'
     973      except:
     974          if options.debug: raise
     975          print '\n\n'
     976          exc_info = sys.exc_info()
     977          if isinstance(exc_info[0], basestring): e = exc_info[0]
     978          else: e = exc_info[1]
     979          print >>sys.stderr, ('\nUNEXPECTED ERROR:\n'
     980                               '%s\n' % (str(e) or e.__class__.__name__))
     981          print >>sys.stderr, 'Use --debug to see trace information.'
     982          sys.exit(3)
     983
     984 -def _profile():
     985      # Hotshot profiler.
     986      if PROFILER == 'hotshot':
     987          try: import hotshot, hotshot.stats
     988          except ImportError:
     989              print >>sys.stderr, "Could not import profile module!"
     990              return
     991          try:
     992              prof = hotshot.Profile('hotshot.out')
     993              prof = prof.runctx('main(*parse_arguments())', globals(), {})
     994          except SystemExit:
     995              pass
     996          prof.close()
     997          # Convert profile.hotshot -> profile.out
     998          print 'Consolidating hotshot profiling info...'
     999          hotshot.stats.load('hotshot.out').dump_stats('profile.out')
    1000
    1001      # Standard 'profile' profiler.
    1002      elif PROFILER == 'profile':
    1003          # cProfile module was added in Python 2.5 -- use it if its'
    1004          # available, since it's faster.
    1005          try: from cProfile import Profile
    1006          except ImportError:
    1007              try: from profile import Profile
    1008              except ImportError:
    1009                  print >>sys.stderr, "Could not import profile module!"
    1010                  return
    1011
    1012          # There was a bug in Python 2.4's profiler.  Check if it's
    1013          # present, and if so, fix it.  (Bug was fixed in 2.4maint:
    1014          # <http://mail.python.org/pipermail/python-checkins/
    1015          #                         2005-September/047099.html>)
    1016          if (hasattr(Profile, 'dispatch') and
    1017              Profile.dispatch['c_exception'] is
    1018              Profile.trace_dispatch_exception.im_func):
    1019              trace_dispatch_return = Profile.trace_dispatch_return.im_func
    1020              Profile.dispatch['c_exception'] = trace_dispatch_return
    1021          try:
    1022              prof = Profile()
    1023              prof = prof.runctx('main(*parse_arguments())', globals(), {})
    1024          except SystemExit:
    1025              pass
    1026          prof.dump_stats('profile.out')
    1027
    1028      else:
    1029          print >>sys.stderr, 'Unknown profiler %s' % PROFILER
    1030          return
    1031
    1032  ######################################################################
    1033  #{ Logging
    1034  ######################################################################
    1035
    1036 -class TerminalController:
    1037      """
    1038      A class that can be used to portably generate formatted output to
    1039      a terminal.  See
    1040      U{http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/475116}
    1041      for documentation.  (This is a somewhat stripped-down version.)
    1042      """
    1043      BOL = ''             #: Move the cursor to the beginning of the line
    1044      UP = ''              #: Move the cursor up one line
    1045      DOWN = ''            #: Move the cursor down one line
    1046      LEFT = ''            #: Move the cursor left one char
    1047      RIGHT = ''           #: Move the cursor right one char
    1048      CLEAR_EOL = ''       #: Clear to the end of the line.
    1049      CLEAR_LINE = ''      #: Clear the current line; cursor to BOL.
    1050      BOLD = ''            #: Turn on bold mode
    1051      NORMAL = ''          #: Turn off all modes
    1052      COLS = 75            #: Width of the terminal (default to 75)
    1053      BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = ''
    1054
    1055      _STRING_CAPABILITIES = """
    1056      BOL=cr UP=cuu1 DOWN=cud1 LEFT=cub1 RIGHT=cuf1
    1057      CLEAR_EOL=el BOLD=bold UNDERLINE=smul NORMAL=sgr0""".split()
    1058      _COLORS = """BLACK BLUE GREEN CYAN RED MAGENTA YELLOW WHITE""".split()
    1059      _ANSICOLORS = "BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE".split()
    1060
    1061      #: If this is set to true, then new TerminalControllers will
    1062      #: assume that the terminal is not capable of doing manipulation
    1063      #: of any kind.
    1064      FORCE_SIMPLE_TERM = False
    1065
    1066 -    def __init__(self, term_stream=sys.stdout):
    1067          # If the stream isn't a tty, then assume it has no capabilities.
    1068          if not term_stream.isatty(): return
    1069          if self.FORCE_SIMPLE_TERM: return
    1070
    1071          # Curses isn't available on all platforms
    1072          try: import curses
    1073          except:
    1074              # If it's not available, then try faking enough to get a
    1075              # simple progress bar.
    1076              self.BOL = '\r'
    1077              self.CLEAR_LINE = '\r' + ' '*self.COLS + '\r'
    1078
    1079          # Check the terminal type.  If we fail, then assume that the
    1080          # terminal has no capabilities.
    1081          try: curses.setupterm()
    1082          except: return
    1083
    1084          # Look up numeric capabilities.
    1085          self.COLS = curses.tigetnum('cols')
    1086
    1087          # Look up string capabilities.
    1088          for capability in self._STRING_CAPABILITIES:
    1089              (attrib, cap_name) = capability.split('=')
    1090              setattr(self, attrib, self._tigetstr(cap_name) or '')
    1091          if self.BOL and self.CLEAR_EOL:
    1092              self.CLEAR_LINE = self.BOL+self.CLEAR_EOL
    1093
    1094          # Colors
    1095          set_fg = self._tigetstr('setf')
    1096          if set_fg:
    1097              for i,color in zip(range(len(self._COLORS)), self._COLORS):
    1098                  setattr(self, color, curses.tparm(set_fg, i) or '')
    1099          set_fg_ansi = self._tigetstr('setaf')
    1100          if set_fg_ansi:
    1101              for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS):
    1102                  setattr(self, color, curses.tparm(set_fg_ansi, i) or '')
    1103
    1104 -    def _tigetstr(self, cap_name):
    1105          # String capabilities can include "delays" of the form "$<2>".
    1106          # For any modern terminal, we should be able to just ignore
    1107          # these, so strip them out.
    1108          import curses
    1109          cap = curses.tigetstr(cap_name) or ''
    1110          return re.sub(r'\$<\d+>[/*]?', '', cap)
    1111
    1112 -class ConsoleLogger(log.Logger):
    1113 -    def __init__(self, verbosity, progress_mode=None):
    1114          self._verbosity = verbosity
    1115          self._progress = None
    1116          self._message_blocks = []
    1117          # For ETA display:
    1118          self._progress_start_time = None
    1119          # For per-task times:
    1120          self._task_times = []
    1121          self._progress_header = None
    1122
    1123          self.reported_message_levels = set()
    1124          """This set contains all the message levels (WARNING, ERROR,
    1125          etc) that have been reported.  It is used by the options
    1126          --fail-on-warning etc to determine the return value."""
    1127
    1128          self.suppressed_docstring_warning = 0
    1129          """This variable will be incremented once every time a
    1130          docstring warning is reported tothe logger, but the verbosity
    1131          level is too low for it to be displayed."""
    1132
    1133          self.term = TerminalController()
    1134
    1135          # Set the progress bar mode.
    1136          if verbosity >= 2: self._progress_mode = 'list'
    1137          elif verbosity >= 0:
    1138              if progress_mode is not None:
    1139                  self._progress_mode = progress_mode
    1140              elif self.term.COLS < 15:
    1141                  self._progress_mode = 'simple-bar'
    1142              elif self.term.BOL and self.term.CLEAR_EOL and self.term.UP:
    1143                  self._progress_mode = 'multiline-bar'
    1144              elif self.term.BOL and self.term.CLEAR_LINE:
    1145                  self._progress_mode = 'bar'
    1146              else:
    1147                  self._progress_mode = 'simple-bar'
    1148          else: self._progress_mode = 'hide'
    1149
    1150 -    def start_block(self, header):
    1151          self._message_blocks.append( (header, []) )
    1152
    1153 -    def end_block(self):
    1154          header, messages = self._message_blocks.pop()
    1155          if messages:
    1156              width = self.term.COLS - 5 - 2*len(self._message_blocks)
    1157              prefix = self.term.CYAN+self.term.BOLD+'| '+self.term.NORMAL
    1158              divider = (self.term.CYAN+self.term.BOLD+'+'+'-'*(width-1)+
    1159                         self.term.NORMAL)
    1160              # Mark up the header:
    1161              header = wordwrap(header, right=width-2, splitchars='\\/').rstrip()
    1162              header = '\n'.join([prefix+self.term.CYAN+l+self.term.NORMAL
    1163                                  for l in header.split('\n')])
    1164              # Construct the body:
    1165              body = ''
    1166              for message in messages:
    1167                  if message.endswith('\n'): body += message
    1168                  else: body += message+'\n'
    1169              # Indent the body:
    1170              body = '\n'.join([prefix+'  '+l for l in body.split('\n')])
    1171              # Put it all together:
    1172              message = divider + '\n' + header + '\n' + body + '\n'
    1173              self._report(message)
    1174
    1175 -    def _format(self, prefix, message, color):
    1176          """
    1177          Rewrap the message; but preserve newlines, and don't touch any
    1178          lines that begin with spaces.
    1179          """
    1180          lines = message.split('\n')
    1181          startindex = indent = len(prefix)
    1182          for i in range(len(lines)):
    1183              if lines[i].startswith(' '):
    1184                  lines[i] = ' '*(indent-startindex) + lines[i] + '\n'
    1185              else:
    1186                  width = self.term.COLS - 5 - 4*len(self._message_blocks)
    1187                  lines[i] = wordwrap(lines[i], indent, width, startindex, '\\/')
    1188              startindex = 0
    1189          return color+prefix+self.term.NORMAL+''.join(lines)
    1190
    1191 -    def log(self, level, message):
    1192          self.reported_message_levels.add(level)
    1193          if self._verbosity >= -2 and level >= log.ERROR:
    1194              message = self._format('  Error: ', message, self.term.RED)
    1195          elif self._verbosity >= -1 and level >= log.WARNING:
    1196              message = self._format('Warning: ', message, self.term.YELLOW)
    1197          elif self._verbosity >= 1 and level >= log.DOCSTRING_WARNING:
    1198              message = self._format('Warning: ', message, self.term.YELLOW)
    1199          elif self._verbosity >= 3 and level >= log.INFO:
    1200              message = self._format('   Info: ', message, self.term.NORMAL)
    1201          elif epydoc.DEBUG and level == log.DEBUG:
    1202              message = self._format('  Debug: ', message, self.term.CYAN)
    1203          else:
    1204              if level >= log.DOCSTRING_WARNING:
    1205                  self.suppressed_docstring_warning += 1
    1206              return
    1207
    1208          self._report(message)
    1209
    1210 -    def _report(self, message):
    1211          if not message.endswith('\n'): message += '\n'
    1212
    1213          if self._message_blocks:
    1214              self._message_blocks[-1][-1].append(message)
    1215          else:
    1216              # If we're in the middle of displaying a progress bar,
    1217              # then make room for the message.
    1218              if self._progress_mode == 'simple-bar':
    1219                  if self._progress is not None:
    1220                      print
    1221                      self._progress = None
    1222              if self._progress_mode == 'bar':
    1223                  sys.stdout.write(self.term.CLEAR_LINE)
    1224              if self._progress_mode == 'multiline-bar':
    1225                  sys.stdout.write((self.term.CLEAR_EOL + '\n')*2 +
    1226                                   self.term.CLEAR_EOL + self.term.UP*2)
    1227
    1228              # Display the message message.
    1229              sys.stdout.write(message)
    1230              sys.stdout.flush()
    1231
    1232 -    def progress(self, percent, message=''):
    1233          percent = min(1.0, percent)
    1234          message = '%s' % message
    1235
    1236          if self._progress_mode == 'list':
    1237              if message:
    1238                  print '[%3d%%] %s' % (100*percent, message)
    1239                  sys.stdout.flush()
    1240
    1241          elif self._progress_mode == 'bar':
    1242              dots = int((self.term.COLS/2-8)*percent)
    1243              background = '-'*(self.term.COLS/2-8)
    1244              if len(message) > self.term.COLS/2:
    1245                  message = message[:self.term.COLS/2-3]+'...'
    1246              sys.stdout.write(self.term.CLEAR_LINE + '%3d%% '%(100*percent) +
    1247                               self.term.GREEN + '[' + self.term.BOLD +
    1248                               '='*dots + background[dots:] + self.term.NORMAL +
    1249                               self.term.GREEN + '] ' + self.term.NORMAL +
    1250                               message + self.term.BOL)
    1251              sys.stdout.flush()
    1252              self._progress = percent
    1253          elif self._progress_mode == 'multiline-bar':
    1254              dots = int((self.term.COLS-10)*percent)
    1255              background = '-'*(self.term.COLS-10)
    1256
    1257              if len(message) > self.term.COLS-10:
    1258                  message = message[:self.term.COLS-10-3]+'...'
    1259              else:
    1260                  message = message.center(self.term.COLS-10)
    1261
    1262              time_elapsed = time.time()-self._progress_start_time
    1263              if percent > 0:
    1264                  time_remain = (time_elapsed / percent) * (1-percent)
    1265              else:
    1266                  time_remain = 0
    1267
    1268              sys.stdout.write(
    1269                  # Line 1:
    1270                  self.term.CLEAR_EOL + '      ' +
    1271                  '%-8s' % self._timestr(time_elapsed) +
    1272                  self.term.BOLD + 'Progress:'.center(self.term.COLS-26) +
    1273                  self.term.NORMAL + '%8s' % self._timestr(time_remain) + '\n' +
    1274                  # Line 2:
    1275                  self.term.CLEAR_EOL + ('%3d%% ' % (100*percent)) +
    1276                  self.term.GREEN + '[' +  self.term.BOLD + '='*dots +
    1277                  background[dots:] + self.term.NORMAL + self.term.GREEN +
    1278                  ']' + self.term.NORMAL + '\n' +
    1279                  # Line 3:
    1280                  self.term.CLEAR_EOL + '      ' + message + self.term.BOL +
    1281                  self.term.UP + self.term.UP)
    1282
    1283              sys.stdout.flush()
    1284              self._progress = percent
    1285          elif self._progress_mode == 'simple-bar':
    1286              if self._progress is None:
    1287                  sys.stdout.write('  [')
    1288                  self._progress = 0.0
    1289              dots = int((self.term.COLS-2)*percent)
    1290              progress_dots = int((self.term.COLS-2)*self._progress)
    1291              if dots > progress_dots:
    1292                  sys.stdout.write('.'*(dots-progress_dots))
    1293                  sys.stdout.flush()
    1294                  self._progress = percent
    1295
    1296 -    def _timestr(self, dt):
    1297          dt = int(dt)
    1298          if dt >= 3600:
    1299              return '%d:%02d:%02d' % (dt/3600, dt%3600/60, dt%60)
    1300          else:
    1301              return '%02d:%02d' % (dt/60, dt%60)
    1302
    1303 -    def start_progress(self, header=None):
    1304          if self._progress is not None:
    1305              raise ValueError
    1306          self._progress = None
    1307          self._progress_start_time = time.time()
    1308          self._progress_header = header
    1309          if self._progress_mode != 'hide' and header:
    1310              print self.term.BOLD + header + self.term.NORMAL
    1311
    1312 -    def end_progress(self):
    1313          self.progress(1.)
    1314          if self._progress_mode == 'bar':
    1315              sys.stdout.write(self.term.CLEAR_LINE)
    1316          if self._progress_mode == 'multiline-bar':
    1317                  sys.stdout.write((self.term.CLEAR_EOL + '\n')*2 +
    1318                                   self.term.CLEAR_EOL + self.term.UP*2)
    1319          if self._progress_mode == 'simple-bar':
    1320              print ']'
    1321          self._progress = None
    1322          self._task_times.append( (time.time()-self._progress_start_time,
    1323                                    self._progress_header) )
    1324
    1325 -    def print_times(self):
    1326          print
    1327          print 'Timing summary:'
    1328          total = sum([time for (time, task) in self._task_times])
    1329          max_t = max([time for (time, task) in self._task_times])
    1330          for (time, task) in self._task_times:
    1331              task = task[:31]
    1332              print '  %s%s %7.1fs' % (task, '.'*(35-len(task)), time),
    1333              if self.term.COLS > 55:
    1334                  print '|'+'=' * int((self.term.COLS-53) * time / max_t)
    1335              else:
    1336                  print
    1337          print
    1338
    1339 -class UnifiedProgressConsoleLogger(ConsoleLogger):
    1340 -    def __init__(self, verbosity, stages, progress_mode=None):
    1341          self.stage = 0
    1342          self.stages = stages
    1343          self.task = None
    1344          ConsoleLogger.__init__(self, verbosity, progress_mode)
    1345
    1346 -    def progress(self, percent, message=''):
    1347          #p = float(self.stage-1+percent)/self.stages
    1348          i = self.stage-1
    1349          p = ((sum(self.stages[:i]) + percent*self.stages[i]) /
    1350               float(sum(self.stages)))
    1351
    1352          if message is UNKNOWN: message = None
    1353          if message: message = '%s: %s' % (self.task, message)
    1354          ConsoleLogger.progress(self, p, message)
    1355
    1356 -    def start_progress(self, header=None):
    1357          self.task = header
    1358          if self.stage == 0:
    1359              ConsoleLogger.start_progress(self)
    1360          self.stage += 1
    1361
    1362 -    def end_progress(self):
    1363          if self.stage == len(self.stages):
    1364              ConsoleLogger.end_progress(self)
    1365
    1366 -    def print_times(self):
    1367          pass
    1368
    1369 -class HTMLLogger(log.Logger):
    1370      """
    1371      A logger used to generate a log of all warnings and messages to an
    1372      HTML file.
    1373      """
    1374
    1375      FILENAME = "epydoc-log.html"
    1376      HEADER = textwrap.dedent('''\
    1377          <?xml version="1.0" encoding="ascii"?>
    1378          <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    1379                    "DTD/xhtml1-transitional.dtd">
    1380          <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
    1381          <head>
    1382            <title>Epydoc Log</title>
    1383            <link rel="stylesheet" href="epydoc.css" type="text/css" />
    1384          </head>
    1385
    1386          <body bgcolor="white" text="black" link="blue" vlink="#204080"
    1387                alink="#204080">
    1388          <h1 class="epydoc">Epydoc Log</h1>
    1389          <p class="log">Epydoc started at %s</p>''')
    1390      START_BLOCK = '<div class="log-block"><h2 class="log-hdr">%s</h2>'
    1391      MESSAGE = ('<div class="log-%s"><b>%s</b>: \n'
    1392                 '%s</div>\n')
    1393      END_BLOCK = '</div>'
    1394      FOOTER = "</body>\n</html>\n"
    1395
    1396 -    def __init__(self, directory, options):
    1397          self.start_time = time.time()
    1398          self.out = open(os.path.join(directory, self.FILENAME), 'w')
    1399          self.out.write(self.HEADER % time.ctime(self.start_time))
    1400          self.is_empty = True
    1401          self.options = options
    1402
    1403 -    def write_options(self, options):
    1404          self.out.write(self.START_BLOCK % 'Epydoc Options')
    1405          msg = '<table border="0" cellpadding="0" cellspacing="0">\n'
    1406          opts = [(key, getattr(options, key)) for key in dir(options)
    1407                  if key not in dir(optparse.Values)]
    1408          opts = [(val==OPTION_DEFAULTS.get(key), key, val)
    1409                  for (key, val) in opts]
    1410          for is_default, key, val in sorted(opts):
    1411              css = is_default and 'opt-default' or 'opt-changed'
    1412              msg += ('<tr valign="top" class="%s"><td valign="top">%s</td>'
    1413                      '<td valign="top"><tt>&nbsp;=&nbsp;</tt></td>'
    1414                      '<td valign="top"><tt>%s</tt></td></tr>' %
    1415                      (css, key, plaintext_to_html(repr(val))))
    1416          msg += '</table>\n'
    1417          self.out.write('<div class="log-info">\n%s</div>\n' % msg)
    1418          self.out.write(self.END_BLOCK)
    1419
    1420 -    def start_block(self, header):
    1421          self.out.write(self.START_BLOCK % header)
    1422
    1423 -    def end_block(self):
    1424          self.out.write(self.END_BLOCK)
    1425
    1426 -    def log(self, level, message):
    1427          if message.endswith("(-v) to display markup errors."): return
    1428          if level >= log.ERROR:
    1429              self.out.write(self._message('error', message))
    1430          elif level >= log.WARNING:
    1431              self.out.write(self._message('warning', message))
    1432          elif level >= log.DOCSTRING_WARNING:
    1433              self.out.write(self._message('docstring warning', message))
    1434
    1435 -    def _message(self, level, message):
    1436          self.is_empty = False
    1437          message = plaintext_to_html(message)
    1438          if '\n' in message:
    1439              message = '<pre class="log">%s</pre>' % message
    1440          hdr = ' '.join([w.capitalize() for w in level.split()])
    1441          return self.MESSAGE % (level.split()[-1], hdr, message)
    1442
    1443 -    def close(self):
    1444          if self.is_empty:
    1445              self.out.write('<div class="log-info">'
    1446                             'No warnings or errors!</div>')
    1447          self.write_options(self.options)
    1448          self.out.write('<p class="log">Epydoc finished at %s</p>\n'
    1449                         '<p class="log">(Elapsed time: %s)</p>' %
    1450                         (time.ctime(), self._elapsed_time()))
    1451          self.out.write(self.FOOTER)
    1452          self.out.close()
    1453
    1454 -    def _elapsed_time(self):
    1455          secs = int(time.time()-self.start_time)
    1456          if secs < 60:
    1457              return '%d seconds' % secs
    1458          if secs < 3600:
    1459              return '%d minutes, %d seconds' % (secs/60, secs%60)
    1460          else:
    1461              return '%d hours, %d minutes' % (secs/3600, secs%3600)
    1462
    1463
    1464  ######################################################################
    1465  ## main
    1466  ######################################################################
    1467
    1468  if __name__ == '__main__':
    1469      cli()
    1470