summaryrefslogtreecommitdiff
path: root/coverage/cmdline.py
diff options
context:
space:
mode:
Diffstat (limited to 'coverage/cmdline.py')
-rw-r--r--coverage/cmdline.py445
1 files changed, 253 insertions, 192 deletions
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index 66a76fa6..221c18d6 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -1,9 +1,13 @@
-"""Command-line support for Coverage."""
+# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
+
+"""Command-line support for coverage.py."""
import glob
import optparse
-import os
+import os.path
import sys
+import textwrap
import traceback
from coverage import env
@@ -16,110 +20,122 @@ class Opts(object):
"""A namespace class for individual options we'll build parsers from."""
append = optparse.make_option(
- '-a', '--append', action='store_false', dest="erase_first",
- help="Append coverage data to .coverage, otherwise it is started "
- "clean with each run."
- )
+ '-a', '--append', action='store_true',
+ help="Append coverage data to .coverage, otherwise it is started clean with each run.",
+ )
branch = optparse.make_option(
'', '--branch', action='store_true',
- help="Measure branch coverage in addition to statement coverage."
- )
+ help="Measure branch coverage in addition to statement coverage.",
+ )
CONCURRENCY_CHOICES = [
"thread", "gevent", "greenlet", "eventlet", "multiprocessing",
]
concurrency = optparse.make_option(
'', '--concurrency', action='store', metavar="LIB",
choices=CONCURRENCY_CHOICES,
- help="Properly measure code using a concurrency library. "
- "Valid values are: %s." % ", ".join(CONCURRENCY_CHOICES)
- )
+ help=(
+ "Properly measure code using a concurrency library. "
+ "Valid values are: %s."
+ ) % ", ".join(CONCURRENCY_CHOICES),
+ )
debug = optparse.make_option(
'', '--debug', action='store', metavar="OPTS",
- help="Debug options, separated by commas"
- )
+ help="Debug options, separated by commas",
+ )
directory = optparse.make_option(
'-d', '--directory', action='store', metavar="DIR",
- help="Write the output files to DIR."
- )
+ help="Write the output files to DIR.",
+ )
fail_under = optparse.make_option(
'', '--fail-under', action='store', metavar="MIN", type="int",
- help="Exit with a status of 2 if the total coverage is less than MIN."
- )
+ help="Exit with a status of 2 if the total coverage is less than MIN.",
+ )
help = optparse.make_option(
'-h', '--help', action='store_true',
- help="Get help on this command."
- )
+ help="Get help on this command.",
+ )
ignore_errors = optparse.make_option(
'-i', '--ignore-errors', action='store_true',
- help="Ignore errors while reading source files."
- )
+ help="Ignore errors while reading source files.",
+ )
include = optparse.make_option(
'', '--include', action='store',
metavar="PAT1,PAT2,...",
- help="Include only files whose paths match one of these patterns."
- "Accepts shell-style wildcards, which must be quoted."
- )
+ help=(
+ "Include only files whose paths match one of these patterns. "
+ "Accepts shell-style wildcards, which must be quoted."
+ ),
+ )
pylib = optparse.make_option(
'-L', '--pylib', action='store_true',
- help="Measure coverage even inside the Python installed library, "
- "which isn't done by default."
- )
+ help=(
+ "Measure coverage even inside the Python installed library, "
+ "which isn't done by default."
+ ),
+ )
show_missing = optparse.make_option(
'-m', '--show-missing', action='store_true',
- help="Show line numbers of statements in each module that weren't "
- "executed."
- )
+ help="Show line numbers of statements in each module that weren't executed.",
+ )
skip_covered = optparse.make_option(
'--skip-covered', action='store_true',
- help="Skip files with 100% coverage."
- )
+ help="Skip files with 100% coverage.",
+ )
omit = optparse.make_option(
'', '--omit', action='store',
metavar="PAT1,PAT2,...",
- help="Omit files whose paths match one of these patterns. "
- "Accepts shell-style wildcards, which must be quoted."
- )
+ help=(
+ "Omit files whose paths match one of these patterns. "
+ "Accepts shell-style wildcards, which must be quoted."
+ ),
+ )
output_xml = optparse.make_option(
'-o', '', action='store', dest="outfile",
metavar="OUTFILE",
- help="Write the XML report to this file. Defaults to 'coverage.xml'"
- )
+ help="Write the XML report to this file. Defaults to 'coverage.xml'",
+ )
parallel_mode = optparse.make_option(
'-p', '--parallel-mode', action='store_true',
- help="Append the machine name, process id and random number to the "
- ".coverage data file name to simplify collecting data from "
- "many processes."
- )
+ help=(
+ "Append the machine name, process id and random number to the "
+ ".coverage data file name to simplify collecting data from "
+ "many processes."
+ ),
+ )
module = optparse.make_option(
'-m', '--module', action='store_true',
- help="<pyfile> is an importable Python module, not a script path, "
- "to be run as 'python -m' would run it."
- )
+ help=(
+ "<pyfile> is an importable Python module, not a script path, "
+ "to be run as 'python -m' would run it."
+ ),
+ )
rcfile = optparse.make_option(
'', '--rcfile', action='store',
- help="Specify configuration file. Defaults to '.coveragerc'"
- )
+ help="Specify configuration file. Defaults to '.coveragerc'",
+ )
source = optparse.make_option(
'', '--source', action='store', metavar="SRC1,SRC2,...",
- help="A list of packages or directories of code to be measured."
- )
+ help="A list of packages or directories of code to be measured.",
+ )
timid = optparse.make_option(
'', '--timid', action='store_true',
- help="Use a simpler but slower trace method. Try this if you get "
- "seemingly impossible results!"
- )
+ help=(
+ "Use a simpler but slower trace method. Try this if you get "
+ "seemingly impossible results!"
+ ),
+ )
title = optparse.make_option(
'', '--title', action='store', metavar="TITLE",
- help="A text string to use as the title on the HTML."
- )
+ help="A text string to use as the title on the HTML.",
+ )
version = optparse.make_option(
'', '--version', action='store_true',
- help="Display version information and exit."
- )
+ help="Display version information and exit.",
+ )
class CoverageOptionParser(optparse.OptionParser, object):
- """Base OptionParser for coverage.
+ """Base OptionParser for coverage.py.
Problems don't exit the program.
Defaults are initialized for all options.
@@ -132,6 +148,7 @@ class CoverageOptionParser(optparse.OptionParser, object):
)
self.set_defaults(
action=None,
+ append=None,
branch=None,
concurrency=None,
debug=None,
@@ -140,9 +157,9 @@ class CoverageOptionParser(optparse.OptionParser, object):
help=None,
ignore_errors=None,
include=None,
+ module=None,
omit=None,
parallel_mode=None,
- module=None,
pylib=None,
rcfile=True,
show_missing=None,
@@ -150,7 +167,6 @@ class CoverageOptionParser(optparse.OptionParser, object):
source=None,
timid=None,
title=None,
- erase_first=None,
version=None,
)
@@ -199,10 +215,8 @@ class GlobalOptionParser(CoverageOptionParser):
class CmdOptionParser(CoverageOptionParser):
"""Parse one of the new-style commands for coverage.py."""
- def __init__(self, action, options=None, defaults=None, usage=None,
- description=None
- ):
- """Create an OptionParser for a coverage command.
+ def __init__(self, action, options=None, defaults=None, usage=None, description=None):
+ """Create an OptionParser for a coverage.py command.
`action` is the slug to put into `options.action`.
`options` is a list of Option's for the command.
@@ -214,7 +228,6 @@ class CmdOptionParser(CoverageOptionParser):
if usage:
usage = "%prog " + usage
super(CmdOptionParser, self).__init__(
- prog="coverage %s" % action,
usage=usage,
description=description,
)
@@ -228,6 +241,14 @@ class CmdOptionParser(CoverageOptionParser):
# results, and they will compare equal to objects.
return (other == "<CmdOptionParser:%s>" % self.cmd)
+ def get_prog_name(self):
+ """Override of an undocumented function in optparse.OptionParser."""
+ program_name = super(CmdOptionParser, self).get_prog_name()
+
+ # Include the sub-command for this parser as part of the command.
+ return "%(command)s %(subcommand)s" % {'command': program_name, 'subcommand': self.cmd}
+
+
GLOBAL_ARGS = [
Opts.debug,
Opts.help,
@@ -235,115 +256,131 @@ GLOBAL_ARGS = [
]
CMDS = {
- 'annotate': CmdOptionParser("annotate",
+ 'annotate': CmdOptionParser(
+ "annotate",
[
Opts.directory,
Opts.ignore_errors,
- Opts.omit,
Opts.include,
+ Opts.omit,
] + GLOBAL_ARGS,
- usage = "[options] [modules]",
- description = "Make annotated copies of the given files, marking "
- "statements that are executed with > and statements that are "
- "missed with !."
+ usage="[options] [modules]",
+ description=(
+ "Make annotated copies of the given files, marking statements that are executed "
+ "with > and statements that are missed with !."
),
-
- 'combine': CmdOptionParser("combine", GLOBAL_ARGS,
- usage = "<dir1> <dir2> ... <dirN>",
- description = "Combine data from multiple coverage files collected "
+ ),
+
+ 'combine': CmdOptionParser(
+ "combine",
+ GLOBAL_ARGS,
+ usage="<path1> <path2> ... <pathN>",
+ description=(
+ "Combine data from multiple coverage files collected "
"with 'run -p'. The combined results are written to a single "
"file representing the union of the data. The positional "
- "arguments are directories from which the data files should be "
- "combined. By default, only data files in the current directory "
- "are combined."
+ "arguments are data files or directories containing data files. "
+ "If no paths are provided, data files in the default data file's "
+ "directory are combined."
),
+ ),
- 'debug': CmdOptionParser("debug", GLOBAL_ARGS,
- usage = "<topic>",
- description = "Display information on the internals of coverage.py, "
+ 'debug': CmdOptionParser(
+ "debug", GLOBAL_ARGS,
+ usage="<topic>",
+ description=(
+ "Display information on the internals of coverage.py, "
"for diagnosing problems. "
"Topics are 'data' to show a summary of the collected data, "
"or 'sys' to show installation information."
),
-
- 'erase': CmdOptionParser("erase", GLOBAL_ARGS,
- usage = " ",
- description = "Erase previously collected coverage data."
- ),
-
- 'help': CmdOptionParser("help", GLOBAL_ARGS,
- usage = "[command]",
- description = "Describe how to use coverage.py"
- ),
-
- 'html': CmdOptionParser("html",
+ ),
+
+ 'erase': CmdOptionParser(
+ "erase", GLOBAL_ARGS,
+ usage=" ",
+ description="Erase previously collected coverage data.",
+ ),
+
+ 'help': CmdOptionParser(
+ "help", GLOBAL_ARGS,
+ usage="[command]",
+ description="Describe how to use coverage.py",
+ ),
+
+ 'html': CmdOptionParser(
+ "html",
[
Opts.directory,
Opts.fail_under,
Opts.ignore_errors,
- Opts.omit,
Opts.include,
+ Opts.omit,
Opts.title,
] + GLOBAL_ARGS,
- usage = "[options] [modules]",
- description = "Create an HTML report of the coverage of the files. "
+ usage="[options] [modules]",
+ description=(
+ "Create an HTML report of the coverage of the files. "
"Each file gets its own page, with the source decorated to show "
"executed, excluded, and missed lines."
),
+ ),
- 'report': CmdOptionParser("report",
+ 'report': CmdOptionParser(
+ "report",
[
Opts.fail_under,
Opts.ignore_errors,
- Opts.omit,
Opts.include,
+ Opts.omit,
Opts.show_missing,
- Opts.skip_covered
+ Opts.skip_covered,
] + GLOBAL_ARGS,
- usage = "[options] [modules]",
- description = "Report coverage statistics on modules."
- ),
+ usage="[options] [modules]",
+ description="Report coverage statistics on modules."
+ ),
- 'run': CmdOptionParser("run",
+ 'run': CmdOptionParser(
+ "run",
[
Opts.append,
Opts.branch,
Opts.concurrency,
+ Opts.include,
+ Opts.module,
+ Opts.omit,
Opts.pylib,
Opts.parallel_mode,
- Opts.module,
- Opts.timid,
Opts.source,
- Opts.omit,
- Opts.include,
+ Opts.timid,
] + GLOBAL_ARGS,
- defaults = {'erase_first': True},
- usage = "[options] <pyfile> [program options]",
- description = "Run a Python program, measuring code execution."
- ),
+ usage="[options] <pyfile> [program options]",
+ description="Run a Python program, measuring code execution."
+ ),
- 'xml': CmdOptionParser("xml",
+ 'xml': CmdOptionParser(
+ "xml",
[
Opts.fail_under,
Opts.ignore_errors,
- Opts.omit,
Opts.include,
+ Opts.omit,
Opts.output_xml,
] + GLOBAL_ARGS,
- usage = "[options] [modules]",
- description = "Generate an XML report of coverage results."
- ),
- }
+ usage="[options] [modules]",
+ description="Generate an XML report of coverage results."
+ ),
+}
OK, ERR, FAIL_UNDER = 0, 1, 2
class CoverageScript(object):
- """The command-line interface to Coverage."""
+ """The command-line interface to coverage.py."""
def __init__(self, _covpkg=None, _run_python_file=None,
- _run_python_module=None, _help_fn=None):
+ _run_python_module=None, _help_fn=None, _path_exists=None):
# _covpkg is for dependency injection, so we can test this code.
if _covpkg:
self.covpkg = _covpkg
@@ -355,12 +392,24 @@ class CoverageScript(object):
self.run_python_file = _run_python_file or run_python_file
self.run_python_module = _run_python_module or run_python_module
self.help_fn = _help_fn or self.help
+ self.path_exists = _path_exists or os.path.exists
self.global_option = False
self.coverage = None
+ self.program_name = os.path.basename(sys.argv[0])
+ if env.WINDOWS:
+ # entry_points={'console_scripts':...} on Windows makes files
+ # called coverage.exe, coverage3.exe, and coverage-3.5.exe. These
+ # invoke coverage-script.py, coverage3-script.py, and
+ # coverage-3.5-script.py. argv[0] is the .py file, but we want to
+ # get back to the original form.
+ auto_suffix = "-script.py"
+ if self.program_name.endswith(auto_suffix):
+ self.program_name = self.program_name[:-len(auto_suffix)]
+
def command_line(self, argv):
- """The bulk of the command line interface to Coverage.
+ """The bulk of the command line interface to coverage.py.
`argv` is the argument list to process.
@@ -409,55 +458,58 @@ class CoverageScript(object):
# Do something.
self.coverage = self.covpkg.coverage(
- data_suffix = options.parallel_mode,
- cover_pylib = options.pylib,
- timid = options.timid,
- branch = options.branch,
- config_file = options.rcfile,
- source = source,
- omit = omit,
- include = include,
- debug = debug,
- concurrency = options.concurrency,
+ data_suffix=options.parallel_mode,
+ cover_pylib=options.pylib,
+ timid=options.timid,
+ branch=options.branch,
+ config_file=options.rcfile,
+ source=source,
+ omit=omit,
+ include=include,
+ debug=debug,
+ concurrency=options.concurrency,
)
if options.action == "debug":
return self.do_debug(args)
- if options.action == "erase" or options.erase_first:
+ elif options.action == "erase":
self.coverage.erase()
- else:
- self.coverage.load()
+ return OK
- if options.action == "run":
- self.do_run(options, args)
+ elif options.action == "run":
+ return self.do_run(options, args)
- if options.action == "combine":
- data_dirs = argv if argv else None
+ elif options.action == "combine":
+ self.coverage.load()
+ data_dirs = args or None
self.coverage.combine(data_dirs)
self.coverage.save()
+ return OK
# Remaining actions are reporting, with some common options.
report_args = dict(
- morfs = unglob_args(args),
- ignore_errors = options.ignore_errors,
- omit = omit,
- include = include,
+ morfs=unglob_args(args),
+ ignore_errors=options.ignore_errors,
+ omit=omit,
+ include=include,
)
+ self.coverage.load()
+
total = None
if options.action == "report":
total = self.coverage.report(
show_missing=options.show_missing,
skip_covered=options.skip_covered, **report_args)
- if options.action == "annotate":
+ elif options.action == "annotate":
self.coverage.annotate(
directory=options.directory, **report_args)
- if options.action == "html":
+ elif options.action == "html":
total = self.coverage.html_report(
directory=options.directory, title=options.title,
**report_args)
- if options.action == "xml":
+ elif options.action == "xml":
outfile = options.outfile
total = self.coverage.xml_report(outfile=outfile, **report_args)
@@ -465,9 +517,9 @@ class CoverageScript(object):
# Apply the command line fail-under options, and then use the config
# value, so we can get fail_under from the config file.
if options.fail_under is not None:
- self.coverage.config["report:fail_under"] = options.fail_under
+ self.coverage.set_option("report:fail_under", options.fail_under)
- if self.coverage.config["report:fail_under"]:
+ if self.coverage.get_option("report:fail_under"):
# Total needs to be rounded, but be careful of 0 and 100.
if 0 < total < 1:
@@ -477,7 +529,7 @@ class CoverageScript(object):
else:
total = round(total)
- if total >= self.coverage.config["report:fail_under"]:
+ if total >= self.coverage.get_option("report:fail_under"):
return OK
else:
return FAIL_UNDER
@@ -489,13 +541,15 @@ class CoverageScript(object):
assert error or topic or parser
if error:
print(error)
- print("Use 'coverage help' for help.")
+ print("Use '%s help' for help." % (self.program_name,))
elif parser:
print(parser.format_help().strip())
else:
- help_msg = HELP_TOPICS.get(topic, '').strip()
+ help_params = dict(self.covpkg.__dict__)
+ help_params['program_name'] = self.program_name
+ help_msg = textwrap.dedent(HELP_TOPICS.get(topic, '')).strip()
if help_msg:
- print(help_msg % self.covpkg.__dict__)
+ print(help_msg % help_params)
else:
print("Don't know topic %r" % topic)
@@ -547,19 +601,22 @@ class CoverageScript(object):
def do_run(self, options, args):
"""Implementation of 'coverage run'."""
- # Set the first path element properly.
- old_path0 = sys.path[0]
+ if options.append and self.coverage.get_option("run:parallel"):
+ self.help_fn("Can't append to data files in parallel mode.")
+ return ERR
+
+ if not self.coverage.get_option("run:parallel"):
+ if not options.append:
+ self.coverage.erase()
# Run the script.
self.coverage.start()
code_ran = True
try:
if options.module:
- sys.path[0] = ''
self.run_python_module(args[0], args)
else:
filename = args[0]
- sys.path[0] = os.path.abspath(os.path.dirname(filename))
self.run_python_file(filename, args)
except NoSource:
code_ran = False
@@ -567,10 +624,13 @@ class CoverageScript(object):
finally:
self.coverage.stop()
if code_ran:
+ if options.append:
+ data_file = self.coverage.get_option("run:data_file")
+ if self.path_exists(data_file):
+ self.coverage.combine(data_paths=[data_file])
self.coverage.save()
- # Restore the old path
- sys.path[0] = old_path0
+ return OK
def do_debug(self, args):
"""Implementation of 'coverage debug'."""
@@ -578,6 +638,7 @@ class CoverageScript(object):
if not args:
self.help_fn("What information would you like: data, sys?")
return ERR
+
for info in args:
if info == 'sys':
sys_info = self.coverage.sys_info()
@@ -586,17 +647,17 @@ class CoverageScript(object):
print(" %s" % line)
elif info == 'data':
self.coverage.load()
+ data = self.coverage.data
print(info_header("data"))
- print("path: %s" % self.coverage.data.filename)
- print("has_arcs: %r" % self.coverage.data.has_arcs())
- summary = self.coverage.data.summary(fullpath=True)
- if summary:
- plugins = self.coverage.data.plugin_data()
+ print("path: %s" % self.coverage.data_files.filename)
+ if data:
+ print("has_arcs: %r" % data.has_arcs())
+ summary = data.line_counts(fullpath=True)
filenames = sorted(summary.keys())
print("\n%d files:" % len(filenames))
for f in filenames:
line = "%s: %d lines" % (f, summary[f])
- plugin = plugins.get(f)
+ plugin = data.file_tracer(f)
if plugin:
line += " [%s]" % plugin
print(line)
@@ -605,6 +666,7 @@ class CoverageScript(object):
else:
self.help_fn("Don't know what you mean by %r" % info)
return ERR
+
return OK
@@ -613,9 +675,9 @@ def unshell_list(s):
if not s:
return None
if env.WINDOWS:
- # When running coverage as coverage.exe, some of the behavior
+ # When running coverage.py as coverage.exe, some of the behavior
# of the shell is emulated: wildcards are expanded into a list of
- # filenames. So you have to single-quote patterns on the command
+ # file names. So you have to single-quote patterns on the command
# line, but (not) helpfully, the single quotes are included in the
# argument, so we have to strip them off here.
s = s.strip("'")
@@ -636,40 +698,39 @@ def unglob_args(args):
HELP_TOPICS = {
-# -------------------------
-'help': """\
-Coverage.py, version %(__version__)s
-Measure, collect, and report on code coverage in Python programs.
-
-usage: coverage <command> [options] [args]
-
-Commands:
- annotate Annotate source files with execution information.
- combine Combine a number of data files.
- erase Erase previously collected coverage data.
- help Get help on using coverage.py.
- html Create an HTML report.
- report Report coverage stats on modules.
- run Run a Python program and measure code execution.
- xml Create an XML report of coverage results.
-
-Use "coverage help <command>" for detailed help on any command.
-For full documentation, see %(__url__)s
-""",
-# -------------------------
-'minimum_help': """\
-Code coverage for Python. Use 'coverage help' for help.
-""",
-# -------------------------
-'version': """\
-Coverage.py, version %(__version__)s.
-Documentation at %(__url__)s
-""",
+ 'help': """\
+ Coverage.py, version %(__version__)s
+ Measure, collect, and report on code coverage in Python programs.
+
+ usage: %(program_name)s <command> [options] [args]
+
+ Commands:
+ annotate Annotate source files with execution information.
+ combine Combine a number of data files.
+ erase Erase previously collected coverage data.
+ help Get help on using coverage.py.
+ html Create an HTML report.
+ report Report coverage stats on modules.
+ run Run a Python program and measure code execution.
+ xml Create an XML report of coverage results.
+
+ Use "%(program_name)s help <command>" for detailed help on any command.
+ For full documentation, see %(__url__)s
+ """,
+
+ 'minimum_help': """\
+ Code coverage for Python. Use '%(program_name)s help' for help.
+ """,
+
+ 'version': """\
+ Coverage.py, version %(__version__)s.
+ Documentation at %(__url__)s
+ """,
}
def main(argv=None):
- """The main entry point to Coverage.
+ """The main entry point to coverage.py.
This is installed as the script entry point.