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 1196 2006-04-09 18:15:55Z 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 
 42    name: Example 
 43    graph: classtree 
 44    introspect: no 
 45   
 46  Verbosity Levels 
 47  ================ 
 48  The C{-v} and C{-q} options increase and decrease verbosity, 
 49  respectively.  The default verbosity level is zero.  The verbosity 
 50  levels are currently defined as follows:: 
 51   
 52                  Progress    Markup warnings   Warnings   Errors 
 53   -3               none            no             no        no 
 54   -2               none            no             no        yes 
 55   -1               none            no             yes       yes 
 56    0 (default)     bar             no             yes       yes 
 57    1               bar             yes            yes       yes 
 58    2               list            yes            yes       yes 
 59  """ 
 60  __docformat__ = 'epytext en' 
 61   
 62  import sys, os, time, re, pstats 
 63  from glob import glob 
 64  from optparse import OptionParser, OptionGroup 
 65  import epydoc 
 66  from epydoc import log 
 67  from epydoc.util import wordwrap, run_subprocess, RunSubprocessError 
 68  from epydoc.apidoc import UNKNOWN 
 69  import ConfigParser 
 70   
 71  INHERITANCE_STYLES = ('grouped', 'listed', 'included') 
 72  GRAPH_TYPES = ('classtree', 'callgraph', 'umlclasstree') 
 73  ACTIONS = ('html', 'text', 'latex', 'dvi', 'ps', 'pdf', 'check') 
 74  DEFAULT_DOCFORMAT = 'epytext' 
 75   
 76  ###################################################################### 
 77  #{ Argument & Config File Parsing 
 78  ###################################################################### 
 79   
80 -def parse_arguments():
81 # Construct the option parser. 82 usage = '%prog ACTION [options] NAMES...' 83 version = "Epydoc, version %s" % epydoc.__version__ 84 optparser = OptionParser(usage=usage, version=version) 85 action_group = OptionGroup(optparser, 'Actions') 86 options_group = OptionGroup(optparser, 'Options') 87 88 # Add options -- Actions 89 action_group.add_option( # --html 90 "--html", action="store_const", dest="action", const="html", 91 help="Write HTML output.") 92 action_group.add_option( # --latex 93 "--text", action="store_const", dest="action", const="text", 94 help="Write plaintext output. (not implemented yet)") 95 action_group.add_option( # --latex 96 "--latex", action="store_const", dest="action", const="latex", 97 help="Write LaTeX output.") 98 action_group.add_option( # --dvi 99 "--dvi", action="store_const", dest="action", const="dvi", 100 help="Write DVI output.") 101 action_group.add_option( # --ps 102 "--ps", action="store_const", dest="action", const="ps", 103 help="Write Postscript output.") 104 action_group.add_option( # --pdf 105 "--pdf", action="store_const", dest="action", const="pdf", 106 help="Write PDF output.") 107 action_group.add_option( # --check 108 "--check", action="store_const", dest="action", const="check", 109 help="Check completeness of docs.") 110 111 # Add options -- Options 112 options_group.add_option( # --output 113 "--output", "-o", dest="target", metavar="PATH", 114 help="The output directory. If PATH does not exist, then " 115 "it will be created.") 116 options_group.add_option( # --show-imports 117 "--inheritance", dest="inheritance", metavar="STYLE", 118 help="The format for showing inheritance objects. STYLE " 119 "should be one of: %s." % ', '.join(INHERITANCE_STYLES)) 120 options_group.add_option( # --output 121 "--docformat", dest="docformat", metavar="NAME", 122 help="The default markup language for docstrings. Defaults " 123 "to \"%s\"." % DEFAULT_DOCFORMAT) 124 options_group.add_option( # --css 125 "--css", dest="css", metavar="STYLESHEET", 126 help="The CSS stylesheet. STYLESHEET can be either a " 127 "builtin stylesheet or the name of a CSS file.") 128 options_group.add_option( # --name 129 "--name", dest="prj_name", metavar="NAME", 130 help="The documented project's name (for the navigation bar).") 131 options_group.add_option( # --url 132 "--url", dest="prj_url", metavar="URL", 133 help="The documented project's URL (for the navigation bar).") 134 options_group.add_option( # --navlink 135 "--navlink", dest="prj_link", metavar="HTML", 136 help="HTML code for a navigation link to place in the " 137 "navigation bar.") 138 options_group.add_option( # --top 139 "--top", dest="top_page", metavar="PAGE", 140 help="The \"top\" page for the HTML documentation. PAGE can " 141 "be a URL, the name of a module or class, or one of the " 142 "special names \"trees.html\", \"indices.html\", or \"help.html\"") 143 options_group.add_option( # --help-file 144 "--help-file", dest="help_file", metavar="FILE", 145 help="An alternate help file. FILE should contain the body " 146 "of an HTML file -- navigation bars will be added to it.") 147 options_group.add_option( # --frames 148 "--show-frames", action="store_true", dest="show_frames", 149 help="Include frames in the HTML output. (default)") 150 options_group.add_option( # --no-frames 151 "--no-frames", action="store_false", dest="show_frames", 152 help="Do not include frames in the HTML output.") 153 options_group.add_option( # --private 154 "--show-private", action="store_true", dest="show_private", 155 help="Include private variables in the output. (default)") 156 options_group.add_option( # --no-private 157 "--no-private", action="store_false", dest="show_private", 158 help="Do not include private variables in the output.") 159 options_group.add_option( # --show-imports 160 "--show-imports", action="store_true", dest="show_imports", 161 help="List each module's imports.") 162 options_group.add_option( # --show-imports 163 "--no-imports", action="store_false", dest="show_imports", 164 help="Do not list each module's imports. (default)") 165 options_group.add_option( # --quiet 166 "--quiet", "-q", action="count", dest="quiet", 167 help="Decrease the verbosity.") 168 options_group.add_option( # --verbose 169 "--verbose", "-v", action="count", dest="verbose", 170 help="Increase the verbosity.") 171 options_group.add_option( # --debug 172 "--debug", action="store_true", dest="debug", 173 help="Show full tracebacks for internal errors.") 174 options_group.add_option( # --parse-only 175 "--parse-only", action="store_false", dest="introspect", 176 help="Get all information from parsing (don't introspect)") 177 options_group.add_option( # --introspect-only 178 "--introspect-only", action="store_false", dest="parse", 179 help="Get all information from introspecting (don't parse)") 180 if epydoc.DEBUG: 181 # this option is for developers, not users. 182 options_group.add_option( 183 "--profile-epydoc", action="store_true", dest="profile", 184 help="Run the profiler. Output will be written to profile.out") 185 options_group.add_option( 186 "--dotpath", dest="dotpath", metavar='PATH', 187 help="The path to the Graphviz 'dot' executable.") 188 options_group.add_option( 189 '--config', action='append', dest="configfiles", metavar='FILE', 190 help=("A configuration file, specifying additional OPTIONS " 191 "and/or NAMES. This option may be repeated.")) 192 options_group.add_option( 193 '--graph', action='append', dest='graphs', metavar='GRAPHTYPE', 194 help=("Include graphs of type GRAPHTYPE in the generated output. " 195 "Graphs are generated using the Graphviz dot executable. " 196 "If this executable is not on the path, then use --dotpath " 197 "to specify its location. This option may be repeated to " 198 "include multiple graph types in the output. GRAPHTYPE " 199 "should be one of: all, %s." % ', '.join(GRAPH_TYPES))) 200 options_group.add_option( 201 '--separate-classes', action='store_true', 202 dest='list_classes_separately', 203 help=("When generating LaTeX or PDF output, list each class in " 204 "its own section, instead of listing them under their " 205 "containing module.")) 206 options_group.add_option( 207 '--show-sourcecode', action='store_true', dest='include_source_code', 208 help=("Include source code with syntax highlighting in the " 209 "HTML output.")) 210 options_group.add_option( 211 '--no-sourcecode', action='store_false', dest='include_source_code', 212 help=("Do not include source code with syntax highlighting in the " 213 "HTML output.")) 214 options_group.add_option( 215 '--pstat', action='append', dest='pstat_files', metavar='FILE', 216 help="A pstat output file, to be used in generating call graphs.") 217 218 # Add the option groups. 219 optparser.add_option_group(action_group) 220 optparser.add_option_group(options_group) 221 222 # Set the option parser's defaults. 223 optparser.set_defaults(action="html", show_frames=True, 224 docformat=DEFAULT_DOCFORMAT, 225 show_private=True, show_imports=False, 226 inheritance="listed", 227 verbose=0, quiet=0, 228 parse=True, introspect=True, 229 debug=epydoc.DEBUG, profile=False, 230 graphs=[], list_classes_separately=False, 231 include_source_code=True, pstat_files=[]) 232 233 # Parse the arguments. 234 options, names = optparser.parse_args() 235 236 # Process any config files. 237 if options.configfiles: 238 try: 239 parse_configfiles(options.configfiles, options, names) 240 except (KeyboardInterrupt,SystemExit): raise 241 except Exception, e: 242 optparser.error('Error reading config file:\n %s' % e) 243 244 # Check to make sure all options are valid. 245 if len(names) == 0: 246 optparser.error("No names specified.") 247 248 # perform shell expansion. 249 for i, name in enumerate(names[:]): 250 if '?' in name or '*' in name: 251 names[i:i+1] = glob(name) 252 253 if options.inheritance not in INHERITANCE_STYLES: 254 optparser.error("Bad inheritance style. Valid options are " + 255 ",".join(INHERITANCE_STYLES)) 256 if not options.parse and not options.introspect: 257 optparser.error("Invalid option combination: --parse-only " 258 "and --introspect-only.") 259 if options.action == 'text' and len(names) > 1: 260 optparser.error("--text option takes only one name.") 261 262 # Check the list of requested graph types to make sure they're 263 # acceptable. 264 options.graphs = [graph_type.lower() for graph_type in options.graphs] 265 for graph_type in options.graphs: 266 if graph_type == 'callgraph' and not options.pstat_files: 267 optparser.error('"callgraph" graph type may only be used if ' 268 'one or more pstat files are specified.') 269 # If it's 'all', then add everything (but don't add callgraph if 270 # we don't have any profiling info to base them on). 271 if graph_type == 'all': 272 if options.pstat_files: 273 options.graphs = GRAPH_TYPES 274 else: 275 options.graphs = [g for g in GRAPH_TYPES if g != 'callgraph'] 276 break 277 elif graph_type not in GRAPH_TYPES: 278 optparser.error("Invalid graph type %s." % graph_type) 279 280 # Calculate verbosity. 281 options.verbosity = options.verbose - options.quiet 282 283 # The target default depends on the action. 284 if options.target is None: 285 options.target = options.action 286 287 # Return parsed args. 288 return options, names
289
290 -def parse_configfiles(configfiles, options, names):
291 configparser = ConfigParser.ConfigParser() 292 # ConfigParser.read() silently ignores errors, so open the files 293 # manually (since we want to notify the user of any errors). 294 for configfile in configfiles: 295 fp = open(configfile, 'r') # may raise IOError. 296 configparser.readfp(fp, configfile) 297 fp.close() 298 for optname in configparser.options('epydoc'): 299 val = configparser.get('epydoc', optname).strip() 300 optname = optname.lower().strip() 301 if optname in ('modules', 'objects', 'values', 302 'module', 'object', 'value'): 303 names.extend(val.replace(',', ' ').split()) 304 elif optname == 'output': 305 if optname not in ACTIONS: 306 raise ValueError('"%s" expected one of: %s' % 307 (optname, ', '.join(ACTIONS))) 308 options.action = action 309 elif optname == 'target': 310 options.target = val 311 elif optname == 'inheritance': 312 if val.lower() not in INHERITANCE_STYLES: 313 raise ValueError('"%s" expected one of: %s.' % 314 (optname, ', '.join(INHERITANCE_STYLES))) 315 options.inerhitance = val.lower() 316 elif optname == 'docformat': 317 options.docformat = val 318 elif optname == 'css': 319 options.css = val 320 elif optname == 'name': 321 options.prj_name = val 322 elif optname == 'url': 323 options.prj_url = val 324 elif optname == 'link': 325 options.prj_link = val 326 elif optname == 'top': 327 options.top_page = val 328 elif optname == 'help': 329 options.help_file = val 330 elif optname =='frames': 331 options.frames = _str_to_bool(val, optname) 332 elif optname =='private': 333 options.private = _str_to_bool(val, optname) 334 elif optname =='imports': 335 options.imports = _str_to_bool(val, optname) 336 elif optname == 'verbosity': 337 try: 338 options.verbosity = int(val) 339 except ValueError: 340 raise ValueError('"%s" expected an int' % optname) 341 elif optname == 'parse': 342 options.parse = _str_to_bool(val, optname) 343 elif optname == 'introspect': 344 options.introspect = _str_to_bool(val, optname) 345 elif optname == 'dotpath': 346 options.dotpath = val 347 elif optname == 'graph': 348 graphtypes = val.replace(',', '').split() 349 for graphtype in graphtypes: 350 if graphtype not in GRAPH_TYPES: 351 raise ValueError('"%s" expected one of: %s.' % 352 (optname, ', '.join(GRAPH_TYPES))) 353 options.graphs.extend(graphtypes) 354 elif optname in ('separate-classes', 'separate_classes'): 355 options.list_classes_separately = _str_to_bool(val, optname) 356 elif optname == 'sourcecode': 357 options.include_source_code = _str_to_bool(val, optname) 358 elif optname == 'pstat': 359 options.pstat_files.extend(val.replace(',', ' ').split()) 360 else: 361 raise ValueError('Unknown option %s' % optname)
362
363 -def _str_to_bool(val, optname):
364 if val.lower() in ('0', 'no', 'false', 'n', 'f', 'hide'): 365 return False 366 elif val.lower() in ('1', 'yes', 'true', 'y', 't', 'show'): 367 return True 368 else: 369 raise ValueError('"%s" option expected a boolean' % optname)
370 371 ###################################################################### 372 #{ Interface 373 ###################################################################### 374
375 -def main(options, names):
376 if options.action == 'text': 377 if options.parse and options.introspect: 378 options.parse = False 379 380 # Set up the logger 381 if options.action == 'text': 382 logger = None # no logger for text output. 383 elif options.verbosity > 1: 384 logger = ConsoleLogger(options.verbosity) 385 log.register_logger(logger) 386 else: 387 # Each number is a rough approximation of how long we spend on 388 # that task, used to divide up the unified progress bar. 389 stages = [40, # Building documentation 390 7, # Merging parsed & introspected information 391 1, # Linking imported variables 392 3, # Indexing documentation 393 30, # Parsing Docstrings 394 1, # Inheriting documentation 395 2] # Sorting & Grouping 396 if options.action == 'html': stages += [100] 397 elif options.action == 'text': stages += [30] 398 elif options.action == 'latex': stages += [60] 399 elif options.action == 'dvi': stages += [60,30] 400 elif options.action == 'ps': stages += [60,40] 401 elif options.action == 'pdf': stages += [60,50] 402 elif options.action == 'check': stages += [10] 403 else: raise ValueError, '%r not supported' % options.action 404 if options.parse and not options.introspect: 405 del stages[1] # no merging 406 if options.introspect and not options.parse: 407 del stages[1:3] # no merging or linking 408 logger = UnifiedProgressConsoleLogger(options.verbosity, stages) 409 log.register_logger(logger) 410 411 # check the output directory. 412 if options.action != 'text': 413 if os.path.exists(options.target): 414 if not os.path.isdir(options.target): 415 return log.error("%s is not a directory" % options.target) 416 417 # Set the default docformat 418 from epydoc import docstringparser 419 docstringparser.DEFAULT_DOCFORMAT = options.docformat 420 421 # Set the dot path 422 if options.dotpath: 423 from epydoc import dotgraph 424 dotgraph.DOT_PATH = options.dotpath 425 426 # Build docs for the named values. 427 from epydoc.docbuilder import build_doc_index 428 docindex = build_doc_index(names, options.introspect, options.parse, 429 add_submodules=(options.action!='text')) 430 431 if docindex is None: 432 return # docbuilder already logged an error. 433 434 # Load profile information, if it was given. 435 if options.pstat_files: 436 try: 437 profile_stats = pstats.Stats(options.pstat_files[0]) 438 for filename in options.pstat_files[1:]: 439 profile_stats.add(filename) 440 except KeyboardInterrupt: raise 441 except Exception, e: 442 log.error("Error reading pstat file: %s" % e) 443 profile_stats = None 444 if profile_stats is not None: 445 docindex.read_profiling_info(profile_stats) 446 447 # Perform the specified action. 448 if options.action == 'html': 449 write_html(docindex, options) 450 elif options.action in ('latex', 'dvi', 'ps', 'pdf'): 451 write_latex(docindex, options, options.action) 452 elif options.action == 'text': 453 write_text(docindex, options) 454 elif options.action == 'check': 455 check_docs(docindex, options) 456 else: 457 print >>sys.stderr, '\nUnsupported action %s!' % options.action 458 459 # If we supressed docstring warnings, then let the user know. 460 if logger is not None and logger.supressed_docstring_warning: 461 if logger.supressed_docstring_warning == 1: 462 prefix = '1 markup error was found' 463 else: 464 prefix = ('%d markup errors were found' % 465 logger.supressed_docstring_warning) 466 log.warning("%s while processing docstrings. Use the verbose " 467 "switch (-v) to display markup errors." % prefix) 468 469 # Basic timing breakdown: 470 if options.verbosity >= 2 and logger is not None: 471 logger.print_times()
472
473 -def write_html(docindex, options):
474 from epydoc.docwriter.html import HTMLWriter 475 html_writer = HTMLWriter(docindex, **options.__dict__) 476 if options.verbose > 0: 477 log.start_progress('Writing HTML docs to %r' % options.target) 478 else: 479 log.start_progress('Writing HTML docs') 480 html_writer.write(options.target) 481 log.end_progress()
482 483 _RERUN_LATEX_RE = re.compile(r'(?im)^LaTeX\s+Warning:\s+Label\(s\)\s+may' 484 r'\s+have\s+changed.\s+Rerun') 485
486 -def write_latex(docindex, options, format):
487 from epydoc.docwriter.latex import LatexWriter 488 latex_writer = LatexWriter(docindex, **options.__dict__) 489 log.start_progress('Writing LaTeX docs') 490 latex_writer.write(options.target) 491 log.end_progress() 492 # If we're just generating the latex, and not any output format, 493 # then we're done. 494 if format == 'latex': return 495 496 if format == 'dvi': steps = 4 497 elif format == 'ps': steps = 5 498 elif format == 'pdf': steps = 6 499 500 log.start_progress('Processing LaTeX docs') 501 oldpath = os.path.abspath(os.curdir) 502 running = None # keep track of what we're doing. 503 try: 504 try: 505 os.chdir(options.target) 506 507 # Clear any old files out of the way. 508 for ext in 'tex aux log out idx ilg toc ind'.split(): 509 if os.path.exists('apidoc.%s' % ext): 510 os.remove('apidoc.%s' % ext) 511 512 # The first pass generates index files. 513 running = 'latex' 514 log.progress(0./steps, 'LaTeX: First pass') 515 run_subprocess('latex api.tex') 516 517 # Build the index. 518 running = 'makeindex' 519 log.progress(1./steps, 'LaTeX: Build index') 520 run_subprocess('makeindex api.idx') 521 522 # The second pass generates our output. 523 running = 'latex' 524 log.progress(2./steps, 'LaTeX: Second pass') 525 out, err = run_subprocess('latex api.tex') 526 527 # The third pass is only necessary if the second pass 528 # changed what page some things are on. 529 running = 'latex' 530 if _RERUN_LATEX_RE.match(out): 531 log.progress(3./steps, 'LaTeX: Third pass') 532 out, err = run_subprocess('latex api.tex') 533 534 # A fourth path should (almost?) never be necessary. 535 running = 'latex' 536 if _RERUN_LATEX_RE.match(out): 537 log.progress(3./steps, 'LaTeX: Fourth pass') 538 run_subprocess('latex api.tex') 539 540 # If requested, convert to postscript. 541 if format in ('ps', 'pdf'): 542 running = 'dvips' 543 log.progress(4./steps, 'dvips') 544 run_subprocess('dvips api.dvi -o api.ps -G0 -Ppdf') 545 546 # If requested, convert to pdf. 547 if format in ('pdf'): 548 running = 'ps2pdf' 549 log.progress(5./steps, 'ps2pdf') 550 run_subprocess( 551 'ps2pdf -sPAPERSIZE=letter -dMaxSubsetPct=100 ' 552 '-dSubsetFonts=true -dCompatibilityLevel=1.2 ' 553 '-dEmbedAllFonts=true api.ps api.pdf') 554 except RunSubprocessError, e: 555 if running == 'latex': 556 e.out = re.sub(r'(?sm)\A.*?!( LaTeX Error:)?', r'', e.out) 557 e.out = re.sub(r'(?sm)\s*Type X to quit.*', '', e.out) 558 e.out = re.sub(r'(?sm)^! Emergency stop.*', '', e.out) 559 log.error("%s failed: %s" % (running, (e.out+e.err).lstrip())) 560 except OSError, e: 561 log.error("%s failed: %s" % (running, e)) 562 finally: 563 os.chdir(oldpath) 564 log.end_progress()
565
566 -def write_text(docindex, options):
567 log.start_progress('Writing output') 568 from epydoc.docwriter.plaintext import PlaintextWriter 569 plaintext_writer = PlaintextWriter() 570 s = '' 571 for apidoc in docindex.root: 572 s += plaintext_writer.write(apidoc) 573 log.end_progress() 574 if isinstance(s, unicode): 575 s = s.encode('ascii', 'backslashreplace') 576 print s
577
578 -def check_docs(docindex, options):
579 from epydoc.checker import DocChecker 580 DocChecker(docindex).check()
581
582 -def cli():
583 # Parse command-line arguments. 584 options, names = parse_arguments() 585 586 try: 587 if options.profile: 588 _profile() 589 else: 590 main(options, names) 591 except KeyboardInterrupt: 592 print '\n\n' 593 print >>sys.stderr, 'Keyboard interrupt.' 594 except: 595 if options.debug: raise 596 print '\n\n' 597 exc_info = sys.exc_info() 598 if isinstance(exc_info[0], basestring): e = exc_info[0] 599 else: e = exc_info[1] 600 print >>sys.stderr, ('\nUNEXPECTED ERROR:\n' 601 '%s\n' % (str(e) or e.__class__.__name__)) 602 print >>sys.stderr, 'Use --debug to see trace information.'
603
604 -def _profile():
605 import profile, pstats 606 try: 607 prof = profile.Profile() 608 prof = prof.runctx('main(*parse_arguments())', globals(), {}) 609 except SystemExit: 610 pass 611 prof.dump_stats('profile.out') 612 return 613 # Use the pstats statistical browser. This is made unnecessarily 614 # difficult because the whole browser is wrapped in an 615 # if __name__=='__main__' clause. 616 try: 617 pstats_pyfile = os.path.splitext(pstats.__file__)[0]+'.py' 618 sys.argv = ['pstats.py', 'profile.out'] 619 print 620 execfile(pstats_pyfile, {'__name__':'__main__'}) 621 except: 622 print 'Could not run the pstats browser' 623 624 print 'Profiling output is in "profile.out"'
625 626 ###################################################################### 627 #{ Logging 628 ###################################################################### 629
631 """ 632 A class that can be used to portably generate formatted output to 633 a terminal. See 634 U{http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/475116} 635 for documentation. (This is a somewhat stripped-down version.) 636 """ 637 BOL = '' #: Move the cursor to the beginning of the line 638 UP = '' #: Move the cursor up one line 639 DOWN = '' #: Move the cursor down one line 640 LEFT = '' #: Move the cursor left one char 641 RIGHT = '' #: Move the cursor right one char 642 CLEAR_EOL = '' #: Clear to the end of the line. 643 CLEAR_LINE = '' #: Clear the current line; cursor to BOL. 644 BOLD = '' #: Turn on bold mode 645 NORMAL = '' #: Turn off all modes 646 COLS = 75 #: Width of the terminal (default to 75) 647 BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = '' 648 649 _STRING_CAPABILITIES = """ 650 BOL=cr UP=cuu1 DOWN=cud1 LEFT=cub1 RIGHT=cuf1 651 CLEAR_EOL=el BOLD=bold UNDERLINE=smul NORMAL=sgr0""".split() 652 _COLORS = """BLACK BLUE GREEN CYAN RED MAGENTA YELLOW WHITE""".split() 653 _ANSICOLORS = "BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE".split() 654
655 - def __init__(self, term_stream=sys.stdout):
656 # If the stream isn't a tty, then assume it has no capabilities. 657 if not term_stream.isatty(): return 658 659 # Curses isn't available on all platforms 660 try: import curses 661 except: 662 # If it's not available, then try faking enough to get a 663 # simple progress bar. 664 self.BOL = '\r' 665 self.CLEAR_LINE = '\r' + ' '*self.COLS + '\r' 666 667 # Check the terminal type. If we fail, then assume that the 668 # terminal has no capabilities. 669 try: curses.setupterm() 670 except: return 671 672 # Look up numeric capabilities. 673 self.COLS = curses.tigetnum('cols') 674 675 # Look up string capabilities. 676 for capability in self._STRING_CAPABILITIES: 677 (attrib, cap_name) = capability.split('=') 678 setattr(self, attrib, self._tigetstr(cap_name) or '') 679 if self.BOL and self.CLEAR_EOL: 680 self.CLEAR_LINE = self.BOL+self.CLEAR_EOL 681 682 # Colors 683 set_fg = self._tigetstr('setf') 684 if set_fg: 685 for i,color in zip(range(len(self._COLORS)), self._COLORS): 686 setattr(self, color, curses.tparm(set_fg, i) or '') 687 set_fg_ansi = self._tigetstr('setaf') 688 if set_fg_ansi: 689 for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS): 690 setattr(self, color, curses.tparm(set_fg_ansi, i) or '')
691
692 - def _tigetstr(self, cap_name):
693 # String capabilities can include "delays" of the form "$<2>". 694 # For any modern terminal, we should be able to just ignore 695 # these, so strip them out. 696 import curses 697 cap = curses.tigetstr(cap_name) or '' 698 return re.sub(r'\$<\d+>[/*]?', '', cap)
699
700 -class ConsoleLogger(log.Logger):
701 - def __init__(self, verbosity):
702 self._verbosity = verbosity 703 self._progress = None 704 self._message_blocks = [] 705 # For ETA display: 706 self._progress_start_time = None 707 # For per-task times: 708 self._task_times = [] 709 self._progress_header = None 710 711 self.supressed_docstring_warning = 0 712 """This variable will be incremented once every time a 713 docstring warning is reported tothe logger, but the verbosity 714 level is too low for it to be displayed.""" 715 716 self.term = TerminalController() 717 718 # Set the progress bar mode. 719 if verbosity >= 2: self._progress_mode = 'list' 720 elif verbosity >= 0: 721 if self.term.COLS < 15: 722 self._progress_mode = 'simple-bar' 723 if self.term.BOL and self.term.CLEAR_EOL and self.term.UP: 724 self._progress_mode = 'multiline-bar' 725 elif self.term.BOL and self.term.CLEAR_LINE: 726 self._progress_mode = 'bar' 727 else: 728 self._progress_mode = 'simple-bar' 729 else: self._progress_mode = 'hide'
730
731 - def start_block(self, header):
732 self._message_blocks.append( (header, []) )
733
734 - def end_block(self):
735 header, messages = self._message_blocks.pop() 736 if messages: 737 width = self.term.COLS - 5 - 2*len(self._message_blocks) 738 prefix = self.term.CYAN+self.term.BOLD+'| '+self.term.NORMAL 739 divider = (self.term.CYAN+self.term.BOLD+'+'+'-'*(width-1)+ 740 self.term.NORMAL) 741 # Mark up the header: 742 header = wordwrap(header, right=width-2, splitchars='\\/').rstrip() 743 header = '\n'.join([prefix+self.term.CYAN+l+self.term.NORMAL 744 for l in header.split('\n')]) 745 # Construct the body: 746 body = '' 747 for message in messages: 748 if message.endswith('\n'): body += message 749 else: body += message+'\n' 750 # Indent the body: 751 body = '\n'.join([prefix+' '+l for l in body.split('\n')]) 752 # Put it all together: 753 message = divider + '\n' + header + '\n' + body + '\n' 754 self._report(message)
755
756 - def _format(self, prefix, message, color):
757 """ 758 Rewrap the message; but preserve newlines, and don't touch any 759 lines that begin with spaces. 760 """ 761 lines = message.split('\n') 762 startindex = indent = len(prefix) 763 for i in range(len(lines)): 764 if lines[i].startswith(' '): 765 lines[i] = ' '*(indent-startindex) + lines[i] + '\n' 766 else: 767 width = self.term.COLS - 5 - 4*len(self._message_blocks) 768 lines[i] = wordwrap(lines[i], indent, width, startindex, '\\/') 769 startindex = 0 770 return color+prefix+self.term.NORMAL+''.join(lines)
771
772 - def log(self, level, message):
773 if self._verbosity >= -2 and level >= log.ERROR: 774 message = self._format(' Error: ', message, self.term.RED) 775 elif self._verbosity >= -1 and level >= log.WARNING: 776 message = self._format('Warning: ', message, self.term.YELLOW) 777 elif self._verbosity >= 1 and level >= log.DOCSTRING_WARNING: 778 message = self._format('Warning: ', message, self.term.YELLOW) 779 elif self._verbosity >= 3 and level >= log.INFO: 780 message = self._format(' Info: ', message, self.term.NORMAL) 781 elif epydoc.DEBUG and level == log.DEBUG: 782 message = self._format(' Debug: ', message, self.term.CYAN) 783 else: 784 if level >= log.DOCSTRING_WARNING: 785 self.supressed_docstring_warning += 1 786 return 787 788 self._report(message)
789
790 - def _report(self, message):
791 if not message.endswith('\n'): message += '\n' 792 793 if self._message_blocks: 794 self._message_blocks[-1][-1].append(message) 795 else: 796 # If we're in the middle of displaying a progress bar, 797 # then make room for the message. 798 if self._progress_mode == 'simple-bar': 799 if self._progress is not None: 800 print 801 self._progress = None 802 if self._progress_mode == 'bar': 803 sys.stdout.write(self.term.CLEAR_LINE) 804 if self._progress_mode == 'multiline-bar': 805 sys.stdout.write((self.term.CLEAR_EOL + '\n')*2 + 806 self.term.CLEAR_EOL + self.term.UP*2) 807 808 # Display the message message. 809 sys.stdout.write(message) 810 sys.stdout.flush()
811
812 - def progress(self, percent, message=''):
813 percent = min(1.0, percent) 814 message = '%s' % message 815 816 if self._progress_mode == 'list': 817 if message: 818 print '[%3d%%] %s' % (100*percent, message) 819 sys.stdout.flush() 820 821 elif self._progress_mode == 'bar': 822 dots = int((self.term.COLS/2-8)*percent) 823 background = '-'*(self.term.COLS/2-8) 824 if len(message) > self.term.COLS/2: 825 message = message[:self.term.COLS/2-3]+'...' 826 sys.stdout.write(self.term.CLEAR_LINE + '%3d%% '%(100*percent) + 827 self.term.GREEN + '[' + self.term.BOLD + 828 '='*dots + background[dots:] + self.term.NORMAL + 829 self.term.GREEN + '] ' + self.term.NORMAL + 830 message + self.term.BOL) 831 sys.stdout.flush() 832 self._progress = percent 833 elif self._progress_mode == 'multiline-bar': 834 dots = int((self.term.COLS-10)*percent) 835 background = '-'*(self.term.COLS-10) 836 837 if len(message) > self.term.COLS-10: 838 message = message[:self.term.COLS-10-3]+'...' 839 else: 840 message = message.center(self.term.COLS-10) 841 842 time_elapsed = time.time()-self._progress_start_time 843 if percent > 0: 844 time_remain = (time_elapsed / percent) * (1-percent) 845 else: 846 time_remain = 0 847 848 sys.stdout.write( 849 # Line 1: 850 self.term.CLEAR_EOL + ' ' + 851 '%-8s' % self._timestr(time_elapsed) + 852 self.term.BOLD + 'Progress:'.center(self.term.COLS-26) + 853 self.term.NORMAL + '%8s' % self._timestr(time_remain) + '\n' + 854 # Line 2: 855 self.term.CLEAR_EOL + ('%3d%% ' % (100*percent)) + 856 self.term.GREEN + '[' + self.term.BOLD + '='*dots + 857 background[dots:] + self.term.NORMAL + self.term.GREEN + 858 ']' + self.term.NORMAL + '\n' + 859 # Line 3: 860 self.term.CLEAR_EOL + ' ' + message + self.term.BOL + 861 self.term.UP + self.term.UP) 862 863 sys.stdout.flush() 864 self._progress = percent 865 elif self._progress_mode == 'simple-bar': 866 if self._progress is None: 867 sys.stdout.write(' [') 868 self._progress = 0.0 869 dots = int((self.term.COLS-2)*percent) 870 progress_dots = int((self.term.COLS-2)*self._progress) 871 if dots > progress_dots: 872 sys.stdout.write('.'*(dots-progress_dots)) 873 sys.stdout.flush() 874 self._progress = percent
875
876 - def _timestr(self, dt):
877 dt = int(dt) 878 if dt >= 3600: 879 return '%d:%02d:%02d' % (dt/3600, dt%3600/60, dt%60) 880 else: 881 return '%02d:%02d' % (dt/60, dt%60)
882
883 - def start_progress(self, header=None):
884 if self._progress is not None: 885 raise ValueError 886 self._progress = None 887 self._progress_start_time = time.time() 888 self._progress_header = header 889 if self._progress_mode != 'hide' and header: 890 print self.term.BOLD + header + self.term.NORMAL
891
892 - def end_progress(self):
893 self.progress(1.) 894 if self._progress_mode == 'bar': 895 sys.stdout.write(self.term.CLEAR_LINE) 896 if self._progress_mode == 'multiline-bar': 897 sys.stdout.write((self.term.CLEAR_EOL + '\n')*2 + 898 self.term.CLEAR_EOL + self.term.UP*2) 899 if self._progress_mode == 'simple-bar': 900 print ']' 901 self._progress = None 902 self._task_times.append( (time.time()-self._progress_start_time, 903 self._progress_header) )
904
905 - def print_times(self):
906 print 907 print 'Timing summary:' 908 total = sum([time for (time, task) in self._task_times]) 909 max_t = max([time for (time, task) in self._task_times]) 910 for (time, task) in self._task_times: 911 task = task[:31] 912 print ' %s%s %7.1fs' % (task, '.'*(35-len(task)), time), 913 if self.term.COLS > 55: 914 print '|'+'=' * int((self.term.COLS-53) * time / max_t) 915 else: 916 print 917 print
918
919 -class UnifiedProgressConsoleLogger(ConsoleLogger):
920 - def __init__(self, verbosity, stages):
921 self.stage = 0 922 self.stages = stages 923 self.task = None 924 ConsoleLogger.__init__(self, verbosity)
925
926 - def progress(self, percent, message=''):
927 #p = float(self.stage-1+percent)/self.stages 928 i = self.stage-1 929 p = ((sum(self.stages[:i]) + percent*self.stages[i]) / 930 float(sum(self.stages))) 931 932 if message == UNKNOWN: message = None 933 if message: message = '%s: %s' % (self.task, message) 934 ConsoleLogger.progress(self, p, message)
935
936 - def start_progress(self, header=None):
937 self.task = header 938 if self.stage == 0: 939 ConsoleLogger.start_progress(self) 940 self.stage += 1
941
942 - def end_progress(self):
943 if self.stage == len(self.stages): 944 ConsoleLogger.end_progress(self)
945
946 - def print_times(self):
947 pass
948 949 ###################################################################### 950 ## main 951 ###################################################################### 952 953 if __name__ == '__main__': 954 try: 955 cli() 956 except: 957 print '\n\n' 958 raise 959