diff options
Diffstat (limited to 'coverage/cmdline.py')
-rw-r--r-- | coverage/cmdline.py | 445 |
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. |