| Tor Norbye | 3a2425a | 2013-11-04 10:16:08 -0800 | [diff] [blame^] | 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> = </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 | |