DebugClients/Python/coverage/cmdline.py

changeset 31
744cd0b4b8cd
parent 0
de9c2efb9d02
child 32
01f04fbc1842
equal deleted inserted replaced
30:9513afbd57f1 31:744cd0b4b8cd
1 """Command-line support for Coverage.""" 1 """Command-line support for Coverage."""
2 2
3 import getopt, sys 3 import optparse, re, sys
4 4
5 from execfile import run_python_file 5 from coverage.execfile import run_python_file
6 6 from coverage.misc import CoverageException
7 USAGE = r""" 7
8 Coverage version %(__version__)s 8
9 class Opts(object):
10 """A namespace class for individual options we'll build parsers from."""
11
12 append = optparse.Option(
13 '-a', '--append', action='store_false', dest="erase_first",
14 help="Append coverage data to .coverage, otherwise it is started "
15 "clean with each run."
16 )
17 branch = optparse.Option(
18 '', '--branch', action='store_true',
19 help="Measure branch coverage in addition to statement coverage."
20 )
21 directory = optparse.Option(
22 '-d', '--directory', action='store',
23 metavar="DIR",
24 help="Write the output files to DIR."
25 )
26 help = optparse.Option(
27 '-h', '--help', action='store_true',
28 help="Get help on this command."
29 )
30 ignore_errors = optparse.Option(
31 '-i', '--ignore-errors', action='store_true',
32 help="Ignore errors while reading source files."
33 )
34 pylib = optparse.Option(
35 '-L', '--pylib', action='store_true',
36 help="Measure coverage even inside the Python installed library, "
37 "which isn't done by default."
38 )
39 show_missing = optparse.Option(
40 '-m', '--show-missing', action='store_true',
41 help="Show line numbers of statements in each module that weren't "
42 "executed."
43 )
44 old_omit = optparse.Option(
45 '-o', '--omit', action='store',
46 metavar="PRE1,PRE2,...",
47 help="Omit files when their filename path starts with one of these "
48 "prefixes."
49 )
50 omit = optparse.Option(
51 '', '--omit', action='store',
52 metavar="PRE1,PRE2,...",
53 help="Omit files when their filename path starts with one of these "
54 "prefixes."
55 )
56 output_xml = optparse.Option(
57 '-o', '', action='store', dest="outfile",
58 metavar="OUTFILE",
59 help="Write the XML report to this file. Defaults to 'coverage.xml'"
60 )
61 parallel_mode = optparse.Option(
62 '-p', '--parallel-mode', action='store_true',
63 help="Include the machine name and process id in the .coverage "
64 "data file name."
65 )
66 timid = optparse.Option(
67 '', '--timid', action='store_true',
68 help="Use a simpler but slower trace method. Try this if you get "
69 "seemingly impossible results!"
70 )
71 version = optparse.Option(
72 '', '--version', action='store_true',
73 help="Display version information and exit."
74 )
75
76
77 class CoverageOptionParser(optparse.OptionParser, object):
78 """Base OptionParser for coverage.
79
80 Problems don't exit the program.
81 Defaults are initialized for all options.
82
83 """
84
85 def __init__(self, *args, **kwargs):
86 super(CoverageOptionParser, self).__init__(
87 add_help_option=False, *args, **kwargs
88 )
89 self.set_defaults(
90 actions=[],
91 branch=None,
92 directory=None,
93 help=None,
94 ignore_errors=None,
95 omit=None,
96 parallel_mode=None,
97 pylib=None,
98 show_missing=None,
99 timid=None,
100 erase_first=None,
101 version=None,
102 )
103
104 self.disable_interspersed_args()
105 self.help_fn = lambda: None
106
107 class OptionParserError(Exception):
108 """Used to stop the optparse error handler ending the process."""
109 pass
110
111 def parse_args(self, args=None, options=None):
112 """Call optparse.parse_args, but return a triple:
113
114 (ok, options, args)
115
116 """
117 try:
118 options, args = \
119 super(CoverageOptionParser, self).parse_args(args, options)
120 except self.OptionParserError:
121 return False, None, None
122 return True, options, args
123
124 def error(self, msg):
125 """Override optparse.error so sys.exit doesn't get called."""
126 self.help_fn(msg)
127 raise self.OptionParserError
128
129
130 class ClassicOptionParser(CoverageOptionParser):
131 """Command-line parser for coverage.py classic arguments."""
132
133 def __init__(self):
134 super(ClassicOptionParser, self).__init__()
135
136 self.add_action('-a', '--annotate', 'annotate')
137 self.add_action('-b', '--html', 'html')
138 self.add_action('-c', '--combine', 'combine')
139 self.add_action('-e', '--erase', 'erase')
140 self.add_action('-r', '--report', 'report')
141 self.add_action('-x', '--execute', 'execute')
142
143 self.add_options([
144 Opts.directory,
145 Opts.help,
146 Opts.ignore_errors,
147 Opts.pylib,
148 Opts.show_missing,
149 Opts.old_omit,
150 Opts.parallel_mode,
151 Opts.timid,
152 Opts.version,
153 ])
154
155 def add_action(self, dash, dashdash, action_code):
156 """Add a specialized option that is the action to execute."""
157 option = self.add_option(dash, dashdash, action='callback',
158 callback=self._append_action
159 )
160 option.action_code = action_code
161
162 def _append_action(self, option, opt_unused, value_unused, parser):
163 """Callback for an option that adds to the `actions` list."""
164 parser.values.actions.append(option.action_code)
165
166
167 class CmdOptionParser(CoverageOptionParser):
168 """Parse one of the new-style commands for coverage.py."""
169
170 def __init__(self, action, options=None, defaults=None, usage=None,
171 cmd=None, description=None
172 ):
173 """Create an OptionParser for a coverage command.
174
175 `action` is the slug to put into `options.actions`.
176 `options` is a list of Option's for the command.
177 `defaults` is a dict of default value for options.
178 `usage` is the usage string to display in help.
179 `cmd` is the command name, if different than `action`.
180 `description` is the description of the command, for the help text.
181
182 """
183 if usage:
184 usage = "%prog " + usage
185 super(CmdOptionParser, self).__init__(
186 prog="coverage %s" % (cmd or action),
187 usage=usage,
188 description=description,
189 )
190 self.set_defaults(actions=[action], **(defaults or {}))
191 if options:
192 self.add_options(options)
193 self.cmd = cmd or action
194
195 def __eq__(self, other):
196 # A convenience equality, so that I can put strings in unit test
197 # results, and they will compare equal to objects.
198 return (other == "<CmdOptionParser:%s>" % self.cmd)
199
200
201 CMDS = {
202 'annotate': CmdOptionParser("annotate",
203 [
204 Opts.directory,
205 Opts.ignore_errors,
206 Opts.omit,
207 Opts.help,
208 ],
209 usage = "[options] [modules]",
210 description = "Make annotated copies of the given files, marking "
211 "statements that are executed with > and statements that are "
212 "missed with !."
213 ),
214
215 'help': CmdOptionParser("help", [Opts.help],
216 usage = "[command]",
217 description = "Describe how to use coverage.py"
218 ),
219
220 'html': CmdOptionParser("html",
221 [
222 Opts.directory,
223 Opts.ignore_errors,
224 Opts.omit,
225 Opts.help,
226 ],
227 usage = "[options] [modules]",
228 description = "Create an HTML report of the coverage of the files. "
229 "Each file gets its own page, with the source decorated to show "
230 "executed, excluded, and missed lines."
231 ),
232
233 'combine': CmdOptionParser("combine", [Opts.help],
234 usage = " ",
235 description = "Combine data from multiple coverage files collected "
236 "with 'run -p'. The combined results are written to a single "
237 "file representing the union of the data."
238 ),
239
240 'debug': CmdOptionParser("debug", [Opts.help],
241 usage = "<topic>",
242 description = "Display information on the internals of coverage.py, "
243 "for diagnosing problems. "
244 "Topics are 'data' to show a summary of the collected data, "
245 "or 'sys' to show installation information."
246 ),
247
248 'erase': CmdOptionParser("erase", [Opts.help],
249 usage = " ",
250 description = "Erase previously collected coverage data."
251 ),
252
253 'report': CmdOptionParser("report",
254 [
255 Opts.ignore_errors,
256 Opts.omit,
257 Opts.show_missing,
258 Opts.help,
259 ],
260 usage = "[options] [modules]",
261 description = "Report coverage statistics on modules."
262 ),
263
264 'run': CmdOptionParser("execute",
265 [
266 Opts.append,
267 Opts.branch,
268 Opts.pylib,
269 Opts.parallel_mode,
270 Opts.timid,
271 Opts.help,
272 ],
273 defaults = {'erase_first': True},
274 cmd = "run",
275 usage = "[options] <pyfile> [program options]",
276 description = "Run a Python program, measuring code execution."
277 ),
278
279 'xml': CmdOptionParser("xml",
280 [
281 Opts.ignore_errors,
282 Opts.omit,
283 Opts.output_xml,
284 Opts.help,
285 ],
286 cmd = "xml",
287 defaults = {'outfile': 'coverage.xml'},
288 usage = "[options] [modules]",
289 description = "Generate an XML report of coverage results."
290 ),
291 }
292
293
294 OK, ERR = 0, 1
295
296
297 class CoverageScript(object):
298 """The command-line interface to Coverage."""
299
300 def __init__(self, _covpkg=None, _run_python_file=None, _help_fn=None):
301 # _covpkg is for dependency injection, so we can test this code.
302 if _covpkg:
303 self.covpkg = _covpkg
304 else:
305 import coverage
306 self.covpkg = coverage
307
308 # _run_python_file is for dependency injection also.
309 self.run_python_file = _run_python_file or run_python_file
310
311 # _help_fn is for dependency injection.
312 self.help_fn = _help_fn or self.help
313
314 self.coverage = None
315
316 def help(self, error=None, topic=None, parser=None):
317 """Display an error message, or the named topic."""
318 assert error or topic or parser
319 if error:
320 print(error)
321 print("Use 'coverage help' for help.")
322 elif parser:
323 print(parser.format_help().strip())
324 else:
325 # Parse out the topic we want from HELP_TOPICS
326 topic_list = re.split("(?m)^=+ (\w+) =+$", HELP_TOPICS)
327 topics = dict(zip(topic_list[1::2], topic_list[2::2]))
328 help_msg = topics.get(topic, '').strip()
329 if help_msg:
330 print(help_msg % self.covpkg.__dict__)
331 else:
332 print("Don't know topic %r" % topic)
333
334 def command_line(self, argv):
335 """The bulk of the command line interface to Coverage.
336
337 `argv` is the argument list to process.
338
339 Returns 0 if all is well, 1 if something went wrong.
340
341 """
342 # Collect the command-line options.
343
344 if not argv:
345 self.help_fn(topic='minimum_help')
346 return OK
347
348 # The command syntax we parse depends on the first argument. Classic
349 # syntax always starts with an option.
350 classic = argv[0].startswith('-')
351 if classic:
352 parser = ClassicOptionParser()
353 else:
354 parser = CMDS.get(argv[0])
355 if not parser:
356 self.help_fn("Unknown command: '%s'" % argv[0])
357 return ERR
358 argv = argv[1:]
359
360 parser.help_fn = self.help_fn
361 ok, options, args = parser.parse_args(argv)
362 if not ok:
363 return ERR
364
365 # Handle help.
366 if options.help:
367 if classic:
368 self.help_fn(topic='help')
369 else:
370 self.help_fn(parser=parser)
371 return OK
372
373 if "help" in options.actions:
374 if args:
375 for a in args:
376 parser = CMDS.get(a)
377 if parser:
378 self.help_fn(parser=parser)
379 else:
380 self.help_fn(topic=a)
381 else:
382 self.help_fn(topic='help')
383 return OK
384
385 # Handle version.
386 if options.version:
387 self.help_fn(topic='version')
388 return OK
389
390 # Check for conflicts and problems in the options.
391 for i in ['erase', 'execute']:
392 for j in ['annotate', 'html', 'report', 'combine']:
393 if (i in options.actions) and (j in options.actions):
394 self.help_fn("You can't specify the '%s' and '%s' "
395 "options at the same time." % (i, j))
396 return ERR
397
398 if not options.actions:
399 self.help_fn(
400 "You must specify at least one of -e, -x, -c, -r, -a, or -b."
401 )
402 return ERR
403 args_allowed = (
404 'execute' in options.actions or
405 'annotate' in options.actions or
406 'html' in options.actions or
407 'debug' in options.actions or
408 'report' in options.actions or
409 'xml' in options.actions
410 )
411 if not args_allowed and args:
412 self.help_fn("Unexpected arguments: %s" % " ".join(args))
413 return ERR
414
415 if 'execute' in options.actions and not args:
416 self.help_fn("Nothing to do.")
417 return ERR
418
419 # Do something.
420 self.coverage = self.covpkg.coverage(
421 data_suffix = bool(options.parallel_mode),
422 cover_pylib = options.pylib,
423 timid = options.timid,
424 branch = options.branch,
425 )
426
427 if 'debug' in options.actions:
428 if not args:
429 self.help_fn("What information would you like: data, sys?")
430 return ERR
431 for info in args:
432 if info == 'sys':
433 print("-- sys ----------------------------------------")
434 for label, info in self.coverage.sysinfo():
435 if isinstance(info, list):
436 print("%15s:" % label)
437 for e in info:
438 print("%15s %s" % ("", e))
439 else:
440 print("%15s: %s" % (label, info))
441 elif info == 'data':
442 print("-- data ---------------------------------------")
443 self.coverage.load()
444 print("path: %s" % self.coverage.data.filename)
445 print("has_arcs: %r" % self.coverage.data.has_arcs())
446 summary = self.coverage.data.summary(fullpath=True)
447 if summary:
448 filenames = sorted(summary.keys())
449 print("\n%d files:" % len(filenames))
450 for f in filenames:
451 print("%s: %d lines" % (f, summary[f]))
452 else:
453 print("No data collected")
454 else:
455 self.help_fn("Don't know what you mean by %r" % info)
456 return ERR
457 return OK
458
459 if 'erase' in options.actions or options.erase_first:
460 self.coverage.erase()
461 else:
462 self.coverage.load()
463
464 if 'execute' in options.actions:
465 # Run the script.
466 self.coverage.start()
467 try:
468 self.run_python_file(args[0], args)
469 finally:
470 self.coverage.stop()
471 self.coverage.save()
472
473 if 'combine' in options.actions:
474 self.coverage.combine()
475 self.coverage.save()
476
477 # Remaining actions are reporting, with some common options.
478 report_args = {
479 'morfs': args,
480 'ignore_errors': options.ignore_errors,
481 }
482
483 omit = None
484 if options.omit:
485 omit = options.omit.split(',')
486 report_args['omit_prefixes'] = omit
487
488 if 'report' in options.actions:
489 self.coverage.report(
490 show_missing=options.show_missing, **report_args)
491 if 'annotate' in options.actions:
492 self.coverage.annotate(
493 directory=options.directory, **report_args)
494 if 'html' in options.actions:
495 self.coverage.html_report(
496 directory=options.directory, **report_args)
497 if 'xml' in options.actions:
498 outfile = options.outfile
499 if outfile == '-':
500 outfile = None
501 self.coverage.xml_report(outfile=outfile, **report_args)
502
503 return OK
504
505
506 HELP_TOPICS = r"""
507
508 == classic ====================================================================
509 Coverage.py version %(__version__)s
9 Measure, collect, and report on code coverage in Python programs. 510 Measure, collect, and report on code coverage in Python programs.
10 511
11 Usage: 512 Usage:
12 513
13 coverage -x [-p] [-L] MODULE.py [ARG1 ARG2 ...] 514 coverage -x [-p] [-L] [--timid] MODULE.py [ARG1 ARG2 ...]
14 Execute the module, passing the given command-line arguments, collecting 515 Execute the module, passing the given command-line arguments, collecting
15 coverage data. With the -p option, include the machine name and process 516 coverage data. With the -p option, include the machine name and process
16 ID in the .coverage file name. With -L, measure coverage even inside the 517 id in the .coverage file name. With -L, measure coverage even inside the
17 Python installed library, which isn't done by default. 518 Python installed library, which isn't done by default. With --timid, use a
519 simpler but slower trace method.
18 520
19 coverage -e 521 coverage -e
20 Erase collected coverage data. 522 Erase collected coverage data.
21 523
22 coverage -c 524 coverage -c
44 -o DIR,... 546 -o DIR,...
45 Omit reporting or annotating files when their filename path starts with 547 Omit reporting or annotating files when their filename path starts with
46 a directory listed in the omit list. 548 a directory listed in the omit list.
47 e.g. coverage -i -r -o c:\python25,lib\enthought\traits 549 e.g. coverage -i -r -o c:\python25,lib\enthought\traits
48 550
49 -h Print this help.
50
51 Coverage data is saved in the file .coverage by default. Set the 551 Coverage data is saved in the file .coverage by default. Set the
52 COVERAGE_FILE environment variable to save it somewhere else. 552 COVERAGE_FILE environment variable to save it somewhere else.
53 """.strip() 553
54 554 == help =======================================================================
55 555 Coverage.py, version %(__version__)s
56 class CoverageScript: 556 Measure, collect, and report on code coverage in Python programs.
57 """The command-line interface to Coverage.""" 557
58 558 usage: coverage <command> [options] [args]
59 def __init__(self): 559
60 import coverage 560 Commands:
61 self.covpkg = coverage 561 annotate Annotate source files with execution information.
62 self.coverage = None 562 combine Combine a number of data files.
63 563 erase Erase previously collected coverage data.
64 def help(self, error=None): 564 help Get help on using coverage.py.
65 """Display an error message, or the usage for Coverage.""" 565 html Create an HTML report.
66 if error: 566 report Report coverage stats on modules.
67 print error 567 run Run a Python program and measure code execution.
68 print "Use -h for help." 568 xml Create an XML report of coverage results.
69 else: 569
70 print USAGE % self.covpkg.__dict__ 570 Use "coverage help <command>" for detailed help on any command.
71 571 Use "coverage help classic" for help on older command syntax.
72 def command_line(self, argv, help_fn=None): 572 For more information, see %(__url__)s
73 """The bulk of the command line interface to Coverage. 573
74 574 == minimum_help ===============================================================
75 `argv` is the argument list to process. 575 Code coverage for Python. Use 'coverage help' for help.
76 `help_fn` is the help function to use when something goes wrong. 576
77 577 == version ====================================================================
78 """ 578 Coverage.py, version %(__version__)s. %(__url__)s
79 # Collect the command-line options. 579
80 help_fn = help_fn or self.help 580 """
81 OK, ERR = 0, 1 581
82 settings = {}
83 optmap = {
84 '-a': 'annotate',
85 '-b': 'html',
86 '-c': 'combine',
87 '-d:': 'directory=',
88 '-e': 'erase',
89 '-h': 'help',
90 '-i': 'ignore-errors',
91 '-L': 'pylib',
92 '-m': 'show-missing',
93 '-p': 'parallel-mode',
94 '-r': 'report',
95 '-x': 'execute',
96 '-o:': 'omit=',
97 }
98 short_opts = ''.join([o[1:] for o in optmap.keys()])
99 long_opts = optmap.values()
100 options, args = getopt.getopt(argv, short_opts, long_opts)
101 for o, a in options:
102 if optmap.has_key(o):
103 settings[optmap[o]] = True
104 elif optmap.has_key(o + ':'):
105 settings[optmap[o + ':']] = a
106 elif o[2:] in long_opts:
107 settings[o[2:]] = True
108 elif o[2:] + '=' in long_opts:
109 settings[o[2:]+'='] = a
110
111 if settings.get('help'):
112 help_fn()
113 return OK
114
115 # Check for conflicts and problems in the options.
116 for i in ['erase', 'execute']:
117 for j in ['annotate', 'html', 'report', 'combine']:
118 if settings.get(i) and settings.get(j):
119 help_fn("You can't specify the '%s' and '%s' "
120 "options at the same time." % (i, j))
121 return ERR
122
123 args_needed = (settings.get('execute')
124 or settings.get('annotate')
125 or settings.get('html')
126 or settings.get('report'))
127 action = (settings.get('erase')
128 or settings.get('combine')
129 or args_needed)
130 if not action:
131 help_fn(
132 "You must specify at least one of -e, -x, -c, -r, -a, or -b."
133 )
134 return ERR
135 if not args_needed and args:
136 help_fn("Unexpected arguments: %s" % " ".join(args))
137 return ERR
138
139 # Do something.
140 self.coverage = self.covpkg.coverage(
141 data_suffix = bool(settings.get('parallel-mode')),
142 cover_pylib = settings.get('pylib')
143 )
144
145 if settings.get('erase'):
146 self.coverage.erase()
147 else:
148 self.coverage.load()
149
150 if settings.get('execute'):
151 if not args:
152 help_fn("Nothing to do.")
153 return ERR
154
155 # Run the script.
156 self.coverage.start()
157 try:
158 run_python_file(args[0], args)
159 finally:
160 self.coverage.stop()
161 self.coverage.save()
162
163 if settings.get('combine'):
164 self.coverage.combine()
165 self.coverage.save()
166
167 # Remaining actions are reporting, with some common options.
168 show_missing = settings.get('show-missing')
169 directory = settings.get('directory=')
170 report_args = {
171 'morfs': args,
172 'ignore_errors': settings.get('ignore-errors'),
173 }
174
175 omit = settings.get('omit=')
176 if omit:
177 omit = omit.split(',')
178 report_args['omit_prefixes'] = omit
179
180 if settings.get('report'):
181 self.coverage.report(show_missing=show_missing, **report_args)
182 if settings.get('annotate'):
183 self.coverage.annotate(directory=directory, **report_args)
184 if settings.get('html'):
185 self.coverage.html_report(directory=directory, **report_args)
186
187 return OK
188
189 582
190 def main(): 583 def main():
191 """The main entrypoint to Coverage. 584 """The main entrypoint to Coverage.
192 585
193 This is installed as the script entrypoint. 586 This is installed as the script entrypoint.
194 587
195 """ 588 """
196 return CoverageScript().command_line(sys.argv[1:]) 589 try:
590 status = CoverageScript().command_line(sys.argv[1:])
591 except CoverageException:
592 _, err, _ = sys.exc_info()
593 print(err)
594 status = ERR
595 return status

eric ide

mercurial