1
2
3
4
5
6
7
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
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'
90
91
92
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
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
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
143
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
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
347
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
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
417 optparser.set_defaults(**OPTION_DEFAULTS)
418
419
420 options, names = optparser.parse_args()
421
422
423
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
434 if options.action == 'version':
435 print version
436 sys.exit(0)
437
438
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
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
459 if len(names) == 0:
460 optparser.error("No names specified.")
461
462
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
477
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
484
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
495 verbosity = getattr(options, 'verbosity', 0)
496 options.verbosity = verbosity + options.verbose - options.quiet
497
498
499 if options.target is None:
500 options.target = options.action
501
502
503 options.names = names
504 return options, names
505
507 configparser = ConfigParser.ConfigParser()
508
509
510 for configfile in configfiles:
511 fp = open(configfile, 'r')
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
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
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
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
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
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
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
633 try:
634 return int(val)
635 except ValueError:
636 raise ValueError('"%s" option expected an int' % optname)
637
639 return val.replace(',', ' ').split()
640
641
642
643
644
645 -def main(options, names):
646
647 if options.debug:
648 epydoc.DEBUG = True
649
650
651
652
653
654
655
656 if options.simple_term:
657 TerminalController.FORCE_SIMPLE_TERM = True
658 if options.action == 'text':
659 logger = None
660 elif options.verbosity > 1:
661 logger = ConsoleLogger(options.verbosity)
662 log.register_logger(logger)
663 else:
664
665
666 stages = [40,
667 7,
668 1,
669 3,
670 1,
671 30,
672 1,
673 2]
674 if options.load_pickle:
675 stages = [30]
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]
687 if options.introspect and not options.parse:
688 del stages[1:3]
689 logger = UnifiedProgressConsoleLogger(options.verbosity, stages)
690 log.register_logger(logger)
691
692
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
708 from epydoc import docstringparser
709 docstringparser.DEFAULT_DOCFORMAT = options.docformat
710
711
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
720 if options.dotpath:
721 from epydoc.docwriter import dotgraph
722 dotgraph.DOT_COMMAND = options.dotpath
723
724
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
737
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
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
764
765
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
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
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
806 if options.verbosity >= 2 and logger is not None:
807 logger.print_times()
808
809
810
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
825
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
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
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
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
867
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
877 try:
878 try:
879 os.chdir(options.target)
880
881
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
887 running = 'latex'
888 log.progress(0./steps, 'LaTeX: First pass')
889 run_subprocess('latex api.tex')
890
891
892 running = 'makeindex'
893 log.progress(1./steps, 'LaTeX: Build index')
894 run_subprocess('makeindex api.idx')
895
896
897 running = 'latex'
898 log.progress(2./steps, 'LaTeX: Second pass')
899 out, err = run_subprocess('latex api.tex')
900
901
902
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
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
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
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
955
957
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
985
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
998 print 'Consolidating hotshot profiling info...'
999 hotshot.stats.load('hotshot.out').dump_stats('profile.out')
1000
1001
1002 elif PROFILER == 'profile':
1003
1004
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
1013
1014
1015
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
1034
1035
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 = ''
1044 UP = ''
1045 DOWN = ''
1046 LEFT = ''
1047 RIGHT = ''
1048 CLEAR_EOL = ''
1049 CLEAR_LINE = ''
1050 BOLD = ''
1051 NORMAL = ''
1052 COLS = 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
1062
1063
1064 FORCE_SIMPLE_TERM = False
1065
1066 - def __init__(self, term_stream=sys.stdout):
1067
1068 if not term_stream.isatty(): return
1069 if self.FORCE_SIMPLE_TERM: return
1070
1071
1072 try: import curses
1073 except:
1074
1075
1076 self.BOL = '\r'
1077 self.CLEAR_LINE = '\r' + ' '*self.COLS + '\r'
1078
1079
1080
1081 try: curses.setupterm()
1082 except: return
1083
1084
1085 self.COLS = curses.tigetnum('cols')
1086
1087
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
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
1105
1106
1107
1108 import curses
1109 cap = curses.tigetstr(cap_name) or ''
1110 return re.sub(r'\$<\d+>[/*]?', '', cap)
1111
1113 - def __init__(self, verbosity, progress_mode=None):
1114 self._verbosity = verbosity
1115 self._progress = None
1116 self._message_blocks = []
1117
1118 self._progress_start_time = None
1119
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
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
1151 self._message_blocks.append( (header, []) )
1152
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
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
1165 body = ''
1166 for message in messages:
1167 if message.endswith('\n'): body += message
1168 else: body += message+'\n'
1169
1170 body = '\n'.join([prefix+' '+l for l in body.split('\n')])
1171
1172 message = divider + '\n' + header + '\n' + body + '\n'
1173 self._report(message)
1174
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
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
1217
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
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
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
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
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
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
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
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
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
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
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
1361
1365
1368
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
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
1422
1425
1426 - def log(self, level, message):
1434
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
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
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
1466
1467
1468 if __name__ == '__main__':
1469 cli()
1470