diff options
Diffstat (limited to 'coverage/cmdline.py')
-rw-r--r-- | coverage/cmdline.py | 224 |
1 files changed, 143 insertions, 81 deletions
diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 1029ad63..cb47690c 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -1,17 +1,23 @@ """Command-line support for Coverage.""" -import optparse, sys +import optparse, re, sys, traceback +from coverage.backward import sorted # pylint: disable-msg=W0622 from coverage.execfile import run_python_file -from coverage.misc import CoverageException +from coverage.misc import CoverageException, ExceptionDuringRun class Opts(object): """A namespace class for individual options we'll build parsers from.""" - + + append = optparse.Option( + '-a', '--append', action='store_false', dest="erase_first", + help="Append coverage data to .coverage, otherwise it is started " + "clean with each run." + ) branch = optparse.Option( '', '--branch', action='store_true', - help="Measure branch execution. HIGHLY EXPERIMENTAL!" + help="Measure branch coverage in addition to statement coverage." ) directory = optparse.Option( '-d', '--directory', action='store', @@ -26,6 +32,12 @@ class Opts(object): '-i', '--ignore-errors', action='store_true', help="Ignore errors while reading source files." ) + include = optparse.Option( + '', '--include', action='store', + metavar="PRE1,PRE2,...", + help="Include files only when their filename path starts with one of " + "these prefixes." + ) pylib = optparse.Option( '-L', '--pylib', action='store_true', help="Measure coverage even inside the Python installed library, " @@ -55,26 +67,31 @@ class Opts(object): ) parallel_mode = optparse.Option( '-p', '--parallel-mode', action='store_true', - help="Include the machine name and process id in the .coverage " - "data file name." + help="Append the machine name, process id and random number to the " + ".coverage data file name to simplify collecting data from " + "many processes." + ) + rcfile = optparse.Option( + '', '--rcfile', action='store', + help="Specify configuration file. Defaults to '.coveragerc'" ) timid = optparse.Option( '', '--timid', action='store_true', help="Use a simpler but slower trace method. Try this if you get " "seemingly impossible results!" ) - append = optparse.Option( - '-a', '--append', action='store_false', dest="erase_first", - help="Append coverage data to .coverage, otherwise it is started " - "clean with each run." + version = optparse.Option( + '', '--version', action='store_true', + help="Display version information and exit." ) - + + class CoverageOptionParser(optparse.OptionParser, object): """Base OptionParser for coverage. - + Problems don't exit the program. Defaults are initialized for all options. - + """ def __init__(self, *args, **kwargs): @@ -87,26 +104,33 @@ class CoverageOptionParser(optparse.OptionParser, object): directory=None, help=None, ignore_errors=None, + include=None, omit=None, parallel_mode=None, pylib=None, + rcfile=True, show_missing=None, timid=None, erase_first=None, + version=None, ) self.disable_interspersed_args() - self.help_fn = lambda: None + self.help_fn = self.help_noop + + def help_noop(self, error=None, topic=None, parser=None): + """No-op help function.""" + pass class OptionParserError(Exception): """Used to stop the optparse error handler ending the process.""" pass - + def parse_args(self, args=None, options=None): """Call optparse.parse_args, but return a triple: - + (ok, options, args) - + """ try: options, args = \ @@ -114,7 +138,7 @@ class CoverageOptionParser(optparse.OptionParser, object): except self.OptionParserError: return False, None, None return True, options, args - + def error(self, msg): """Override optparse.error so sys.exit doesn't get called.""" self.help_fn(msg) @@ -126,7 +150,7 @@ class ClassicOptionParser(CoverageOptionParser): def __init__(self): super(ClassicOptionParser, self).__init__() - + self.add_action('-a', '--annotate', 'annotate') self.add_action('-b', '--html', 'html') self.add_action('-c', '--combine', 'combine') @@ -143,6 +167,7 @@ class ClassicOptionParser(CoverageOptionParser): Opts.old_omit, Opts.parallel_mode, Opts.timid, + Opts.version, ]) def add_action(self, dash, dashdash, action_code): @@ -151,7 +176,7 @@ class ClassicOptionParser(CoverageOptionParser): callback=self._append_action ) option.action_code = action_code - + def _append_action(self, option, opt_unused, value_unused, parser): """Callback for an option that adds to the `actions` list.""" parser.values.actions.append(option.action_code) @@ -159,19 +184,19 @@ class ClassicOptionParser(CoverageOptionParser): class CmdOptionParser(CoverageOptionParser): """Parse one of the new-style commands for coverage.py.""" - + def __init__(self, action, options=None, defaults=None, usage=None, cmd=None, description=None ): """Create an OptionParser for a coverage command. - + `action` is the slug to put into `options.actions`. `options` is a list of Option's for the command. `defaults` is a dict of default value for options. `usage` is the usage string to display in help. `cmd` is the command name, if different than `action`. `description` is the description of the command, for the help text. - + """ if usage: usage = "%prog " + usage @@ -190,6 +215,10 @@ class CmdOptionParser(CoverageOptionParser): # results, and they will compare equal to objects. return (other == "<CmdOptionParser:%s>" % self.cmd) +GLOBAL_ARGS = [ + Opts.rcfile, + Opts.help, + ] CMDS = { 'annotate': CmdOptionParser("annotate", @@ -197,15 +226,35 @@ CMDS = { Opts.directory, Opts.ignore_errors, Opts.omit, - Opts.help, - ], + Opts.include, + ] + 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 !." ), - 'help': CmdOptionParser("help", [Opts.help], + 'combine': CmdOptionParser("combine", GLOBAL_ARGS, + usage = " ", + 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." + ), + + '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" ), @@ -215,43 +264,23 @@ CMDS = { Opts.directory, Opts.ignore_errors, Opts.omit, - Opts.help, - ], + Opts.include, + ] + GLOBAL_ARGS, 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." ), - - 'combine': CmdOptionParser("combine", [Opts.help], - usage = " ", - description = "Combine data from multiple coverage files collected " - "with 'run -p'. The combined results are stored into a single " - "file representing the union of the coverage." - ), - - 'debug': CmdOptionParser("debug", [Opts.help], - usage = "<topic>", - description = "Display information the internals of coverage.py, " - "for diagnosing problems. " - "Topics are 'data' to show a summary of the data collected in " - ".coverage, or 'sys' to show installation information." - ), - - 'erase': CmdOptionParser("erase", [Opts.help], - usage = " ", - description = "Erase previously collected coverage data." - ), 'report': CmdOptionParser("report", [ Opts.ignore_errors, Opts.omit, + Opts.include, Opts.show_missing, - Opts.help, - ], + ] + GLOBAL_ARGS, usage = "[options] [modules]", - description = "Report coverage stats on modules." + description = "Report coverage statistics on modules." ), 'run': CmdOptionParser("execute", @@ -261,21 +290,22 @@ CMDS = { Opts.pylib, Opts.parallel_mode, Opts.timid, - Opts.help, - ], - defaults = {'erase_first':True}, + Opts.omit, + Opts.include, + ] + GLOBAL_ARGS, + defaults = {'erase_first': True}, cmd = "run", usage = "[options] <pyfile> [program options]", - description = "Run a python program, measuring code execution." + description = "Run a Python program, measuring code execution." ), - + 'xml': CmdOptionParser("xml", [ Opts.ignore_errors, Opts.omit, + Opts.include, Opts.output_xml, - Opts.help, - ], + ] + GLOBAL_ARGS, cmd = "xml", defaults = {'outfile': 'coverage.xml'}, usage = "[options] [modules]", @@ -286,9 +316,10 @@ CMDS = { OK, ERR = 0, 1 + class CoverageScript(object): """The command-line interface to Coverage.""" - + def __init__(self, _covpkg=None, _run_python_file=None, _help_fn=None): # _covpkg is for dependency injection, so we can test this code. if _covpkg: @@ -296,13 +327,13 @@ class CoverageScript(object): else: import coverage self.covpkg = coverage - + # _run_python_file is for dependency injection also. self.run_python_file = _run_python_file or run_python_file - + # _help_fn is for dependency injection. self.help_fn = _help_fn or self.help - + self.coverage = None def help(self, error=None, topic=None, parser=None): @@ -315,7 +346,6 @@ class CoverageScript(object): print(parser.format_help().strip()) else: # Parse out the topic we want from HELP_TOPICS - import re topic_list = re.split("(?m)^=+ (\w+) =+$", HELP_TOPICS) topics = dict(zip(topic_list[1::2], topic_list[2::2])) help_msg = topics.get(topic, '').strip() @@ -326,14 +356,14 @@ class CoverageScript(object): def command_line(self, argv): """The bulk of the command line interface to Coverage. - + `argv` is the argument list to process. Returns 0 if all is well, 1 if something went wrong. """ # Collect the command-line options. - + if not argv: self.help_fn(topic='minimum_help') return OK @@ -375,6 +405,11 @@ class CoverageScript(object): self.help_fn(topic='help') return OK + # Handle version. + if options.version: + self.help_fn(topic='version') + return OK + # Check for conflicts and problems in the options. for i in ['erase', 'execute']: for j in ['annotate', 'html', 'report', 'combine']: @@ -399,17 +434,28 @@ class CoverageScript(object): if not args_allowed and args: self.help_fn("Unexpected arguments: %s" % " ".join(args)) return ERR - + if 'execute' in options.actions and not args: self.help_fn("Nothing to do.") return ERR - + + # Listify the list options. + omit = None + if options.omit: + omit = options.omit.split(',') + include = None + if options.include: + include = options.include.split(',') + # Do something. self.coverage = self.covpkg.coverage( - data_suffix = bool(options.parallel_mode), + data_suffix = options.parallel_mode, cover_pylib = options.pylib, timid = options.timid, branch = options.branch, + config_file = options.rcfile, + omit_prefixes = omit, + include_prefixes = include, ) if 'debug' in options.actions: @@ -429,9 +475,12 @@ class CoverageScript(object): elif info == 'data': print("-- data ---------------------------------------") self.coverage.load() + 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: filenames = sorted(summary.keys()) + print("\n%d files:" % len(filenames)) for f in filenames: print("%s: %d lines" % (f, summary[f])) else: @@ -465,11 +514,9 @@ class CoverageScript(object): 'ignore_errors': options.ignore_errors, } - omit = None - if options.omit: - omit = options.omit.split(',') report_args['omit_prefixes'] = omit - + report_args['include_prefixes'] = include + if 'report' in options.actions: self.coverage.report( show_missing=options.show_missing, **report_args) @@ -481,8 +528,6 @@ class CoverageScript(object): directory=options.directory, **report_args) if 'xml' in options.actions: outfile = options.outfile - if outfile == '-': - outfile = None self.coverage.xml_report(outfile=outfile, **report_args) return OK @@ -491,7 +536,7 @@ class CoverageScript(object): HELP_TOPICS = r""" == classic ==================================================================== -Coverage version %(__version__)s +Coverage.py version %(__version__)s Measure, collect, and report on code coverage in Python programs. Usage: @@ -537,14 +582,14 @@ Coverage data is saved in the file .coverage by default. Set the COVERAGE_FILE environment variable to save it somewhere else. == help ======================================================================= -Coverage version %(__version__)s +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. + 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. @@ -559,19 +604,36 @@ For more information, see %(__url__)s == minimum_help =============================================================== Code coverage for Python. Use 'coverage help' for help. +== version ==================================================================== +Coverage.py, version %(__version__)s. %(__url__)s + """ -def main(): +def main(argv=None): """The main entrypoint to Coverage. - + This is installed as the script entrypoint. - + """ + if argv is None: + argv = sys.argv[1:] try: - status = CoverageScript().command_line(sys.argv[1:]) + status = CoverageScript().command_line(argv) + except ExceptionDuringRun: + # An exception was caught while running the product code. The + # sys.exc_info() return tuple is packed into an ExceptionDuringRun + # exception. + _, err, _ = sys.exc_info() + traceback.print_exception(*err.args) + status = ERR except CoverageException: + # A controlled error inside coverage.py: print the message to the user. _, err, _ = sys.exc_info() print(err) status = ERR + except SystemExit: + # The user called `sys.exit()`. Exit with their status code. + _, err, _ = sys.exc_info() + status = err.args[0] return status |