eric6/DebugClients/Python/coverage/cmdline.py

changeset 6942
2602857055c5
parent 6649
f1b3a73831c9
child 7427
362cd1b6f81a
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
3
4 """Command-line support for coverage.py."""
5
6 from __future__ import print_function
7
8 import glob
9 import optparse
10 import os.path
11 import sys
12 import textwrap
13 import traceback
14
15 from coverage import env
16 from coverage.collector import CTracer
17 from coverage.debug import info_formatter, info_header
18 from coverage.execfile import run_python_file, run_python_module
19 from coverage.misc import BaseCoverageException, ExceptionDuringRun, NoSource
20 from coverage.results import should_fail_under
21
22
23 class Opts(object):
24 """A namespace class for individual options we'll build parsers from."""
25
26 append = optparse.make_option(
27 '-a', '--append', action='store_true',
28 help="Append coverage data to .coverage, otherwise it starts clean each time.",
29 )
30 branch = optparse.make_option(
31 '', '--branch', action='store_true',
32 help="Measure branch coverage in addition to statement coverage.",
33 )
34 CONCURRENCY_CHOICES = [
35 "thread", "gevent", "greenlet", "eventlet", "multiprocessing",
36 ]
37 concurrency = optparse.make_option(
38 '', '--concurrency', action='store', metavar="LIB",
39 choices=CONCURRENCY_CHOICES,
40 help=(
41 "Properly measure code using a concurrency library. "
42 "Valid values are: %s."
43 ) % ", ".join(CONCURRENCY_CHOICES),
44 )
45 debug = optparse.make_option(
46 '', '--debug', action='store', metavar="OPTS",
47 help="Debug options, separated by commas",
48 )
49 directory = optparse.make_option(
50 '-d', '--directory', action='store', metavar="DIR",
51 help="Write the output files to DIR.",
52 )
53 fail_under = optparse.make_option(
54 '', '--fail-under', action='store', metavar="MIN", type="float",
55 help="Exit with a status of 2 if the total coverage is less than MIN.",
56 )
57 help = optparse.make_option(
58 '-h', '--help', action='store_true',
59 help="Get help on this command.",
60 )
61 ignore_errors = optparse.make_option(
62 '-i', '--ignore-errors', action='store_true',
63 help="Ignore errors while reading source files.",
64 )
65 include = optparse.make_option(
66 '', '--include', action='store',
67 metavar="PAT1,PAT2,...",
68 help=(
69 "Include only files whose paths match one of these patterns. "
70 "Accepts shell-style wildcards, which must be quoted."
71 ),
72 )
73 pylib = optparse.make_option(
74 '-L', '--pylib', action='store_true',
75 help=(
76 "Measure coverage even inside the Python installed library, "
77 "which isn't done by default."
78 ),
79 )
80 show_missing = optparse.make_option(
81 '-m', '--show-missing', action='store_true',
82 help="Show line numbers of statements in each module that weren't executed.",
83 )
84 skip_covered = optparse.make_option(
85 '--skip-covered', action='store_true',
86 help="Skip files with 100% coverage.",
87 )
88 omit = optparse.make_option(
89 '', '--omit', action='store',
90 metavar="PAT1,PAT2,...",
91 help=(
92 "Omit files whose paths match one of these patterns. "
93 "Accepts shell-style wildcards, which must be quoted."
94 ),
95 )
96 output_xml = optparse.make_option(
97 '-o', '', action='store', dest="outfile",
98 metavar="OUTFILE",
99 help="Write the XML report to this file. Defaults to 'coverage.xml'",
100 )
101 parallel_mode = optparse.make_option(
102 '-p', '--parallel-mode', action='store_true',
103 help=(
104 "Append the machine name, process id and random number to the "
105 ".coverage data file name to simplify collecting data from "
106 "many processes."
107 ),
108 )
109 module = optparse.make_option(
110 '-m', '--module', action='store_true',
111 help=(
112 "<pyfile> is an importable Python module, not a script path, "
113 "to be run as 'python -m' would run it."
114 ),
115 )
116 rcfile = optparse.make_option(
117 '', '--rcfile', action='store',
118 help=(
119 "Specify configuration file. "
120 "By default '.coveragerc', 'setup.cfg' and 'tox.ini' are tried."
121 ),
122 )
123 source = optparse.make_option(
124 '', '--source', action='store', metavar="SRC1,SRC2,...",
125 help="A list of packages or directories of code to be measured.",
126 )
127 timid = optparse.make_option(
128 '', '--timid', action='store_true',
129 help=(
130 "Use a simpler but slower trace method. Try this if you get "
131 "seemingly impossible results!"
132 ),
133 )
134 title = optparse.make_option(
135 '', '--title', action='store', metavar="TITLE",
136 help="A text string to use as the title on the HTML.",
137 )
138 version = optparse.make_option(
139 '', '--version', action='store_true',
140 help="Display version information and exit.",
141 )
142
143
144 class CoverageOptionParser(optparse.OptionParser, object):
145 """Base OptionParser for coverage.py.
146
147 Problems don't exit the program.
148 Defaults are initialized for all options.
149
150 """
151
152 def __init__(self, *args, **kwargs):
153 super(CoverageOptionParser, self).__init__(
154 add_help_option=False, *args, **kwargs
155 )
156 self.set_defaults(
157 action=None,
158 append=None,
159 branch=None,
160 concurrency=None,
161 debug=None,
162 directory=None,
163 fail_under=None,
164 help=None,
165 ignore_errors=None,
166 include=None,
167 module=None,
168 omit=None,
169 parallel_mode=None,
170 pylib=None,
171 rcfile=True,
172 show_missing=None,
173 skip_covered=None,
174 source=None,
175 timid=None,
176 title=None,
177 version=None,
178 )
179
180 self.disable_interspersed_args()
181 self.help_fn = self.help_noop
182
183 def help_noop(self, error=None, topic=None, parser=None):
184 """No-op help function."""
185 pass
186
187 class OptionParserError(Exception):
188 """Used to stop the optparse error handler ending the process."""
189 pass
190
191 def parse_args_ok(self, args=None, options=None):
192 """Call optparse.parse_args, but return a triple:
193
194 (ok, options, args)
195
196 """
197 try:
198 options, args = \
199 super(CoverageOptionParser, self).parse_args(args, options)
200 except self.OptionParserError:
201 return False, None, None
202 return True, options, args
203
204 def error(self, msg):
205 """Override optparse.error so sys.exit doesn't get called."""
206 self.help_fn(msg)
207 raise self.OptionParserError
208
209
210 class GlobalOptionParser(CoverageOptionParser):
211 """Command-line parser for coverage.py global option arguments."""
212
213 def __init__(self):
214 super(GlobalOptionParser, self).__init__()
215
216 self.add_options([
217 Opts.help,
218 Opts.version,
219 ])
220
221
222 class CmdOptionParser(CoverageOptionParser):
223 """Parse one of the new-style commands for coverage.py."""
224
225 def __init__(self, action, options, defaults=None, usage=None, description=None):
226 """Create an OptionParser for a coverage.py command.
227
228 `action` is the slug to put into `options.action`.
229 `options` is a list of Option's for the command.
230 `defaults` is a dict of default value for options.
231 `usage` is the usage string to display in help.
232 `description` is the description of the command, for the help text.
233
234 """
235 if usage:
236 usage = "%prog " + usage
237 super(CmdOptionParser, self).__init__(
238 usage=usage,
239 description=description,
240 )
241 self.set_defaults(action=action, **(defaults or {}))
242 self.add_options(options)
243 self.cmd = action
244
245 def __eq__(self, other):
246 # A convenience equality, so that I can put strings in unit test
247 # results, and they will compare equal to objects.
248 return (other == "<CmdOptionParser:%s>" % self.cmd)
249
250 __hash__ = None # This object doesn't need to be hashed.
251
252 def get_prog_name(self):
253 """Override of an undocumented function in optparse.OptionParser."""
254 program_name = super(CmdOptionParser, self).get_prog_name()
255
256 # Include the sub-command for this parser as part of the command.
257 return "{command} {subcommand}".format(command=program_name, subcommand=self.cmd)
258
259
260 GLOBAL_ARGS = [
261 Opts.debug,
262 Opts.help,
263 Opts.rcfile,
264 ]
265
266 CMDS = {
267 'annotate': CmdOptionParser(
268 "annotate",
269 [
270 Opts.directory,
271 Opts.ignore_errors,
272 Opts.include,
273 Opts.omit,
274 ] + GLOBAL_ARGS,
275 usage="[options] [modules]",
276 description=(
277 "Make annotated copies of the given files, marking statements that are executed "
278 "with > and statements that are missed with !."
279 ),
280 ),
281
282 'combine': CmdOptionParser(
283 "combine",
284 [
285 Opts.append,
286 ] + GLOBAL_ARGS,
287 usage="[options] <path1> <path2> ... <pathN>",
288 description=(
289 "Combine data from multiple coverage files collected "
290 "with 'run -p'. The combined results are written to a single "
291 "file representing the union of the data. The positional "
292 "arguments are data files or directories containing data files. "
293 "If no paths are provided, data files in the default data file's "
294 "directory are combined."
295 ),
296 ),
297
298 'debug': CmdOptionParser(
299 "debug", GLOBAL_ARGS,
300 usage="<topic>",
301 description=(
302 "Display information on the internals of coverage.py, "
303 "for diagnosing problems. "
304 "Topics are 'data' to show a summary of the collected data, "
305 "or 'sys' to show installation information."
306 ),
307 ),
308
309 'erase': CmdOptionParser(
310 "erase", GLOBAL_ARGS,
311 description="Erase previously collected coverage data.",
312 ),
313
314 'help': CmdOptionParser(
315 "help", GLOBAL_ARGS,
316 usage="[command]",
317 description="Describe how to use coverage.py",
318 ),
319
320 'html': CmdOptionParser(
321 "html",
322 [
323 Opts.directory,
324 Opts.fail_under,
325 Opts.ignore_errors,
326 Opts.include,
327 Opts.omit,
328 Opts.title,
329 Opts.skip_covered,
330 ] + GLOBAL_ARGS,
331 usage="[options] [modules]",
332 description=(
333 "Create an HTML report of the coverage of the files. "
334 "Each file gets its own page, with the source decorated to show "
335 "executed, excluded, and missed lines."
336 ),
337 ),
338
339 'report': CmdOptionParser(
340 "report",
341 [
342 Opts.fail_under,
343 Opts.ignore_errors,
344 Opts.include,
345 Opts.omit,
346 Opts.show_missing,
347 Opts.skip_covered,
348 ] + GLOBAL_ARGS,
349 usage="[options] [modules]",
350 description="Report coverage statistics on modules."
351 ),
352
353 'run': CmdOptionParser(
354 "run",
355 [
356 Opts.append,
357 Opts.branch,
358 Opts.concurrency,
359 Opts.include,
360 Opts.module,
361 Opts.omit,
362 Opts.pylib,
363 Opts.parallel_mode,
364 Opts.source,
365 Opts.timid,
366 ] + GLOBAL_ARGS,
367 usage="[options] <pyfile> [program options]",
368 description="Run a Python program, measuring code execution."
369 ),
370
371 'xml': CmdOptionParser(
372 "xml",
373 [
374 Opts.fail_under,
375 Opts.ignore_errors,
376 Opts.include,
377 Opts.omit,
378 Opts.output_xml,
379 ] + GLOBAL_ARGS,
380 usage="[options] [modules]",
381 description="Generate an XML report of coverage results."
382 ),
383 }
384
385
386 OK, ERR, FAIL_UNDER = 0, 1, 2
387
388
389 class CoverageScript(object):
390 """The command-line interface to coverage.py."""
391
392 def __init__(self, _covpkg=None, _run_python_file=None,
393 _run_python_module=None, _help_fn=None, _path_exists=None):
394 # _covpkg is for dependency injection, so we can test this code.
395 if _covpkg:
396 self.covpkg = _covpkg
397 else:
398 import coverage
399 self.covpkg = coverage
400
401 # For dependency injection:
402 self.run_python_file = _run_python_file or run_python_file
403 self.run_python_module = _run_python_module or run_python_module
404 self.help_fn = _help_fn or self.help
405 self.path_exists = _path_exists or os.path.exists
406 self.global_option = False
407
408 self.coverage = None
409
410 program_path = sys.argv[0]
411 if program_path.endswith(os.path.sep + '__main__.py'):
412 # The path is the main module of a package; get that path instead.
413 program_path = os.path.dirname(program_path)
414 self.program_name = os.path.basename(program_path)
415 if env.WINDOWS:
416 # entry_points={'console_scripts':...} on Windows makes files
417 # called coverage.exe, coverage3.exe, and coverage-3.5.exe. These
418 # invoke coverage-script.py, coverage3-script.py, and
419 # coverage-3.5-script.py. argv[0] is the .py file, but we want to
420 # get back to the original form.
421 auto_suffix = "-script.py"
422 if self.program_name.endswith(auto_suffix):
423 self.program_name = self.program_name[:-len(auto_suffix)]
424
425 def command_line(self, argv):
426 """The bulk of the command line interface to coverage.py.
427
428 `argv` is the argument list to process.
429
430 Returns 0 if all is well, 1 if something went wrong.
431
432 """
433 # Collect the command-line options.
434 if not argv:
435 self.help_fn(topic='minimum_help')
436 return OK
437
438 # The command syntax we parse depends on the first argument. Global
439 # switch syntax always starts with an option.
440 self.global_option = argv[0].startswith('-')
441 if self.global_option:
442 parser = GlobalOptionParser()
443 else:
444 parser = CMDS.get(argv[0])
445 if not parser:
446 self.help_fn("Unknown command: '%s'" % argv[0])
447 return ERR
448 argv = argv[1:]
449
450 parser.help_fn = self.help_fn
451 ok, options, args = parser.parse_args_ok(argv)
452 if not ok:
453 return ERR
454
455 # Handle help and version.
456 if self.do_help(options, args, parser):
457 return OK
458
459 # We need to be able to import from the current directory, because
460 # plugins may try to, for example, to read Django settings.
461 sys.path[0] = ''
462
463 # Listify the list options.
464 source = unshell_list(options.source)
465 omit = unshell_list(options.omit)
466 include = unshell_list(options.include)
467 debug = unshell_list(options.debug)
468
469 # Do something.
470 self.coverage = self.covpkg.Coverage(
471 data_suffix=options.parallel_mode,
472 cover_pylib=options.pylib,
473 timid=options.timid,
474 branch=options.branch,
475 config_file=options.rcfile,
476 source=source,
477 omit=omit,
478 include=include,
479 debug=debug,
480 concurrency=options.concurrency,
481 )
482
483 if options.action == "debug":
484 return self.do_debug(args)
485
486 elif options.action == "erase":
487 self.coverage.erase()
488 return OK
489
490 elif options.action == "run":
491 return self.do_run(options, args)
492
493 elif options.action == "combine":
494 if options.append:
495 self.coverage.load()
496 data_dirs = args or None
497 self.coverage.combine(data_dirs, strict=True)
498 self.coverage.save()
499 return OK
500
501 # Remaining actions are reporting, with some common options.
502 report_args = dict(
503 morfs=unglob_args(args),
504 ignore_errors=options.ignore_errors,
505 omit=omit,
506 include=include,
507 )
508
509 self.coverage.load()
510
511 total = None
512 if options.action == "report":
513 total = self.coverage.report(
514 show_missing=options.show_missing,
515 skip_covered=options.skip_covered, **report_args)
516 elif options.action == "annotate":
517 self.coverage.annotate(
518 directory=options.directory, **report_args)
519 elif options.action == "html":
520 total = self.coverage.html_report(
521 directory=options.directory, title=options.title,
522 skip_covered=options.skip_covered, **report_args)
523 elif options.action == "xml":
524 outfile = options.outfile
525 total = self.coverage.xml_report(outfile=outfile, **report_args)
526
527 if total is not None:
528 # Apply the command line fail-under options, and then use the config
529 # value, so we can get fail_under from the config file.
530 if options.fail_under is not None:
531 self.coverage.set_option("report:fail_under", options.fail_under)
532
533 fail_under = self.coverage.get_option("report:fail_under")
534 precision = self.coverage.get_option("report:precision")
535 if should_fail_under(total, fail_under, precision):
536 return FAIL_UNDER
537
538 return OK
539
540 def help(self, error=None, topic=None, parser=None):
541 """Display an error message, or the named topic."""
542 assert error or topic or parser
543 if error:
544 print(error, file=sys.stderr)
545 print("Use '%s help' for help." % (self.program_name,), file=sys.stderr)
546 elif parser:
547 print(parser.format_help().strip())
548 else:
549 help_params = dict(self.covpkg.__dict__)
550 help_params['program_name'] = self.program_name
551 if CTracer is not None:
552 help_params['extension_modifier'] = 'with C extension'
553 else:
554 help_params['extension_modifier'] = 'without C extension'
555 help_msg = textwrap.dedent(HELP_TOPICS.get(topic, '')).strip()
556 if help_msg:
557 print(help_msg.format(**help_params))
558 else:
559 print("Don't know topic %r" % topic)
560
561 def do_help(self, options, args, parser):
562 """Deal with help requests.
563
564 Return True if it handled the request, False if not.
565
566 """
567 # Handle help.
568 if options.help:
569 if self.global_option:
570 self.help_fn(topic='help')
571 else:
572 self.help_fn(parser=parser)
573 return True
574
575 if options.action == "help":
576 if args:
577 for a in args:
578 parser = CMDS.get(a)
579 if parser:
580 self.help_fn(parser=parser)
581 else:
582 self.help_fn(topic=a)
583 else:
584 self.help_fn(topic='help')
585 return True
586
587 # Handle version.
588 if options.version:
589 self.help_fn(topic='version')
590 return True
591
592 return False
593
594 def do_run(self, options, args):
595 """Implementation of 'coverage run'."""
596
597 if not args:
598 self.help_fn("Nothing to do.")
599 return ERR
600
601 if options.append and self.coverage.get_option("run:parallel"):
602 self.help_fn("Can't append to data files in parallel mode.")
603 return ERR
604
605 if options.concurrency == "multiprocessing":
606 # Can't set other run-affecting command line options with
607 # multiprocessing.
608 for opt_name in ['branch', 'include', 'omit', 'pylib', 'source', 'timid']:
609 # As it happens, all of these options have no default, meaning
610 # they will be None if they have not been specified.
611 if getattr(options, opt_name) is not None:
612 self.help_fn(
613 "Options affecting multiprocessing must be specified "
614 "in a configuration file."
615 )
616 return ERR
617
618 if not self.coverage.get_option("run:parallel"):
619 if not options.append:
620 self.coverage.erase()
621
622 # Run the script.
623 self.coverage.start()
624 code_ran = True
625 try:
626 if options.module:
627 self.run_python_module(args[0], args)
628 else:
629 filename = args[0]
630 self.run_python_file(filename, args)
631 except NoSource:
632 code_ran = False
633 raise
634 finally:
635 self.coverage.stop()
636 if code_ran:
637 if options.append:
638 data_file = self.coverage.get_option("run:data_file")
639 if self.path_exists(data_file):
640 self.coverage.combine(data_paths=[data_file])
641 self.coverage.save()
642
643 return OK
644
645 def do_debug(self, args):
646 """Implementation of 'coverage debug'."""
647
648 if not args:
649 self.help_fn("What information would you like: config, data, sys?")
650 return ERR
651
652 for info in args:
653 if info == 'sys':
654 sys_info = self.coverage.sys_info()
655 print(info_header("sys"))
656 for line in info_formatter(sys_info):
657 print(" %s" % line)
658 elif info == 'data':
659 self.coverage.load()
660 data = self.coverage.data
661 print(info_header("data"))
662 print("path: %s" % self.coverage.data_files.filename)
663 if data:
664 print("has_arcs: %r" % data.has_arcs())
665 summary = data.line_counts(fullpath=True)
666 filenames = sorted(summary.keys())
667 print("\n%d files:" % len(filenames))
668 for f in filenames:
669 line = "%s: %d lines" % (f, summary[f])
670 plugin = data.file_tracer(f)
671 if plugin:
672 line += " [%s]" % plugin
673 print(line)
674 else:
675 print("No data collected")
676 elif info == 'config':
677 print(info_header("config"))
678 config_info = self.coverage.config.__dict__.items()
679 for line in info_formatter(config_info):
680 print(" %s" % line)
681 else:
682 self.help_fn("Don't know what you mean by %r" % info)
683 return ERR
684
685 return OK
686
687
688 def unshell_list(s):
689 """Turn a command-line argument into a list."""
690 if not s:
691 return None
692 if env.WINDOWS:
693 # When running coverage.py as coverage.exe, some of the behavior
694 # of the shell is emulated: wildcards are expanded into a list of
695 # file names. So you have to single-quote patterns on the command
696 # line, but (not) helpfully, the single quotes are included in the
697 # argument, so we have to strip them off here.
698 s = s.strip("'")
699 return s.split(',')
700
701
702 def unglob_args(args):
703 """Interpret shell wildcards for platforms that need it."""
704 if env.WINDOWS:
705 globbed = []
706 for arg in args:
707 if '?' in arg or '*' in arg:
708 globbed.extend(glob.glob(arg))
709 else:
710 globbed.append(arg)
711 args = globbed
712 return args
713
714
715 HELP_TOPICS = {
716 'help': """\
717 Coverage.py, version {__version__} {extension_modifier}
718 Measure, collect, and report on code coverage in Python programs.
719
720 usage: {program_name} <command> [options] [args]
721
722 Commands:
723 annotate Annotate source files with execution information.
724 combine Combine a number of data files.
725 erase Erase previously collected coverage data.
726 help Get help on using coverage.py.
727 html Create an HTML report.
728 report Report coverage stats on modules.
729 run Run a Python program and measure code execution.
730 xml Create an XML report of coverage results.
731
732 Use "{program_name} help <command>" for detailed help on any command.
733 For full documentation, see {__url__}
734 """,
735
736 'minimum_help': """\
737 Code coverage for Python. Use '{program_name} help' for help.
738 """,
739
740 'version': """\
741 Coverage.py, version {__version__} {extension_modifier}
742 Documentation at {__url__}
743 """,
744 }
745
746
747 def main(argv=None):
748 """The main entry point to coverage.py.
749
750 This is installed as the script entry point.
751
752 """
753 if argv is None:
754 argv = sys.argv[1:]
755 try:
756 status = CoverageScript().command_line(argv)
757 except ExceptionDuringRun as err:
758 # An exception was caught while running the product code. The
759 # sys.exc_info() return tuple is packed into an ExceptionDuringRun
760 # exception.
761 traceback.print_exception(*err.args) # pylint: disable=no-value-for-parameter
762 status = ERR
763 except BaseCoverageException as err:
764 # A controlled error inside coverage.py: print the message to the user.
765 print(err)
766 status = ERR
767 except SystemExit as err:
768 # The user called `sys.exit()`. Exit with their argument, if any.
769 if err.args:
770 status = err.args[0]
771 else:
772 status = None
773 return status

eric ide

mercurial