"""Command-line support for Coverage.""" import optparse, os, sys, traceback from coverage.backward import sorted # pylint: disable=W0622 from coverage.execfile import run_python_file, run_python_module from coverage.misc import CoverageException, ExceptionDuringRun, NoSource from coverage.debug import info_formatter 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." ) branch = optparse.make_option( '', '--branch', action='store_true', help="Measure branch coverage in addition to statement coverage." ) debug = optparse.make_option( '', '--debug', action='store', metavar="OPTS", help="Debug options, separated by commas" ) directory = optparse.make_option( '-d', '--directory', action='store', metavar="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 = optparse.make_option( '-h', '--help', action='store_true', help="Get help on this command." ) ignore_errors = optparse.make_option( '-i', '--ignore-errors', action='store_true', help="Ignore errors while reading source files." ) include = optparse.make_option( '', '--include', action='store', metavar="PAT1,PAT2,...", help="Include files only when their filename path matches one of " "these patterns. Usually needs quoting on the command line." ) pylib = optparse.make_option( '-L', '--pylib', action='store_true', 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." ) old_omit = optparse.make_option( '-o', '--omit', action='store', metavar="PAT1,PAT2,...", help="Omit files when their filename matches one of these patterns. " "Usually needs quoting on the command line." ) omit = optparse.make_option( '', '--omit', action='store', metavar="PAT1,PAT2,...", help="Omit files when their filename matches one of these patterns. " "Usually needs quoting on the command line." ) output_xml = optparse.make_option( '-o', '', action='store', dest="outfile", metavar="OUTFILE", 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." ) module = optparse.make_option( '-m', '--module', action='store_true', help=" 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'" ) source = optparse.make_option( '', '--source', action='store', metavar="SRC1,SRC2,...", 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!" ) title = optparse.make_option( '', '--title', action='store', metavar="TITLE", 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." ) 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): super(CoverageOptionParser, self).__init__( add_help_option=False, *args, **kwargs ) self.set_defaults( actions=[], branch=None, debug=None, directory=None, fail_under=None, help=None, ignore_errors=None, include=None, omit=None, parallel_mode=None, module=None, pylib=None, rcfile=True, show_missing=None, source=None, timid=None, title=None, erase_first=None, version=None, ) self.disable_interspersed_args() 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 = \ super(CoverageOptionParser, self).parse_args(args, options) 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) raise self.OptionParserError class ClassicOptionParser(CoverageOptionParser): """Command-line parser for coverage.py classic arguments.""" 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') self.add_action('-e', '--erase', 'erase') self.add_action('-r', '--report', 'report') self.add_action('-x', '--execute', 'execute') self.add_options([ Opts.directory, Opts.help, Opts.ignore_errors, Opts.pylib, Opts.show_missing, Opts.old_omit, Opts.parallel_mode, Opts.timid, Opts.version, ]) def add_action(self, dash, dashdash, action_code): """Add a specialized option that is the action to execute.""" option = self.add_option(dash, dashdash, action='callback', 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) 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 super(CmdOptionParser, self).__init__( prog="coverage %s" % (cmd or action), usage=usage, description=description, ) self.set_defaults(actions=[action], **(defaults or {})) if options: self.add_options(options) self.cmd = cmd or action def __eq__(self, other): # A convenience equality, so that I can put strings in unit test # results, and they will compare equal to objects. return (other == "" % self.cmd) GLOBAL_ARGS = [ Opts.rcfile, Opts.help, ] CMDS = { 'annotate': CmdOptionParser("annotate", [ Opts.directory, Opts.ignore_errors, Opts.omit, 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 !." ), '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 = "", 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", [ Opts.directory, Opts.fail_under, Opts.ignore_errors, Opts.omit, Opts.include, Opts.title, ] + 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." ), 'report': CmdOptionParser("report", [ Opts.fail_under, Opts.ignore_errors, Opts.omit, Opts.include, Opts.show_missing, ] + GLOBAL_ARGS, usage = "[options] [modules]", description = "Report coverage statistics on modules." ), 'run': CmdOptionParser("execute", [ Opts.append, Opts.branch, Opts.debug, Opts.pylib, Opts.parallel_mode, Opts.module, Opts.timid, Opts.source, Opts.omit, Opts.include, ] + GLOBAL_ARGS, defaults = {'erase_first': True}, cmd = "run", usage = "[options] [program options]", description = "Run a Python program, measuring code execution." ), 'xml': CmdOptionParser("xml", [ Opts.fail_under, Opts.ignore_errors, Opts.omit, Opts.include, Opts.output_xml, ] + GLOBAL_ARGS, cmd = "xml", 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.""" def __init__(self, _covpkg=None, _run_python_file=None, _run_python_module=None, _help_fn=None): # _covpkg is for dependency injection, so we can test this code. if _covpkg: self.covpkg = _covpkg else: import coverage self.covpkg = coverage # For dependency injection: 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.classic = False self.coverage = None 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 # The command syntax we parse depends on the first argument. Classic # syntax always starts with an option. self.classic = argv[0].startswith('-') if self.classic: parser = ClassicOptionParser() else: parser = CMDS.get(argv[0]) if not parser: self.help_fn("Unknown command: '%s'" % argv[0]) return ERR argv = argv[1:] parser.help_fn = self.help_fn ok, options, args = parser.parse_args(argv) if not ok: return ERR # Handle help and version. if self.do_help(options, args, parser): return OK # Check for conflicts and problems in the options. if not self.args_ok(options, args): return ERR # Listify the list options. source = unshell_list(options.source) omit = unshell_list(options.omit) include = unshell_list(options.include) debug = unshell_list(options.debug) # 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, ) if 'debug' in options.actions: return self.do_debug(args) if 'erase' in options.actions or options.erase_first: self.coverage.erase() else: self.coverage.load() if 'execute' in options.actions: self.do_execute(options, args) if 'combine' in options.actions: self.coverage.combine() self.coverage.save() # Remaining actions are reporting, with some common options. report_args = dict( morfs = args, ignore_errors = options.ignore_errors, omit = omit, include = include, ) if 'report' in options.actions: total = self.coverage.report( show_missing=options.show_missing, **report_args) if 'annotate' in options.actions: self.coverage.annotate( directory=options.directory, **report_args) if 'html' in options.actions: total = self.coverage.html_report( directory=options.directory, title=options.title, **report_args) if 'xml' in options.actions: outfile = options.outfile total = self.coverage.xml_report(outfile=outfile, **report_args) if options.fail_under is not None: if total >= options.fail_under: return OK else: return FAIL_UNDER else: return OK def help(self, error=None, topic=None, parser=None): """Display an error message, or the named topic.""" assert error or topic or parser if error: print(error) print("Use 'coverage help' for help.") elif parser: print(parser.format_help().strip()) else: help_msg = HELP_TOPICS.get(topic, '').strip() if help_msg: print(help_msg % self.covpkg.__dict__) else: print("Don't know topic %r" % topic) def do_help(self, options, args, parser): """Deal with help requests. Return True if it handled the request, False if not. """ # Handle help. if options.help: if self.classic: self.help_fn(topic='help') else: self.help_fn(parser=parser) return True if "help" in options.actions: if args: for a in args: parser = CMDS.get(a) if parser: self.help_fn(parser=parser) else: self.help_fn(topic=a) else: self.help_fn(topic='help') return True # Handle version. if options.version: self.help_fn(topic='version') return True return False def args_ok(self, options, args): """Check for conflicts and problems in the options. Returns True if everything is ok, or False if not. """ for i in ['erase', 'execute']: for j in ['annotate', 'html', 'report', 'combine']: if (i in options.actions) and (j in options.actions): self.help_fn("You can't specify the '%s' and '%s' " "options at the same time." % (i, j)) return False if not options.actions: self.help_fn( "You must specify at least one of -e, -x, -c, -r, -a, or -b." ) return False args_allowed = ( 'execute' in options.actions or 'annotate' in options.actions or 'html' in options.actions or 'debug' in options.actions or 'report' in options.actions or 'xml' in options.actions ) if not args_allowed and args: self.help_fn("Unexpected arguments: %s" % " ".join(args)) return False if 'execute' in options.actions and not args: self.help_fn("Nothing to do.") return False return True def do_execute(self, options, args): """Implementation of 'coverage run'.""" # Set the first path element properly. old_path0 = sys.path[0] # Run the script. self.coverage.start() code_ran = True try: 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 raise finally: self.coverage.stop() if code_ran: self.coverage.save() # Restore the old path sys.path[0] = old_path0 def do_debug(self, args): """Implementation of 'coverage debug'.""" if not args: self.help_fn("What information would you like: data, sys?") return ERR for info in args: if info == 'sys': print("-- sys ----------------------------------------") for line in info_formatter(self.coverage.sysinfo()): print(" %s" % line) 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: print("No data collected") else: self.help_fn("Don't know what you mean by %r" % info) return ERR return OK def unshell_list(s): """Turn a command-line argument into a list.""" if not s: return None if sys.platform == 'win32': # When running coverage 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 # line, but (not) helpfully, the single quotes are included in the # argument, so we have to strip them off here. s = s.strip("'") return s.split(',') HELP_TOPICS = { # ------------------------- 'classic': r"""Coverage.py version %(__version__)s Measure, collect, and report on code coverage in Python programs. Usage: coverage -x [-p] [-L] [--timid] MODULE.py [ARG1 ARG2 ...] Execute the module, passing the given command-line arguments, collecting coverage data. With the -p option, include the machine name and process id in the .coverage file name. With -L, measure coverage even inside the Python installed library, which isn't done by default. With --timid, use a simpler but slower trace method. coverage -e Erase collected coverage data. coverage -c Combine data from multiple coverage files (as created by -p option above) and store it into a single file representing the union of the coverage. coverage -r [-m] [-i] [-o DIR,...] [FILE1 FILE2 ...] Report on the statement coverage for the given files. With the -m option, show line numbers of the statements that weren't executed. coverage -b -d DIR [-i] [-o DIR,...] [FILE1 FILE2 ...] Create an HTML report of the coverage of the given files. Each file gets its own page, with the file listing decorated to show executed, excluded, and missed lines. coverage -a [-d DIR] [-i] [-o DIR,...] [FILE1 FILE2 ...] Make annotated copies of the given files, marking statements that are executed with > and statements that are missed with !. -d DIR Write output files for -b or -a to this directory. -i Ignore errors while reporting or annotating. -o DIR,... Omit reporting or annotating files when their filename path starts with a directory listed in the omit list. e.g. coverage -i -r -o c:\python25,lib\enthought\traits Coverage data is saved in the file .coverage by default. Set the COVERAGE_FILE environment variable to save it somewhere else. """, # ------------------------- 'help': """\ Coverage.py, version %(__version__)s Measure, collect, and report on code coverage in Python programs. usage: coverage [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 " for detailed help on any command. Use "coverage help classic" for help on older command syntax. 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(argv=None): """The main entry point to Coverage. This is installed as the script entry point. """ if argv is None: argv = sys.argv[1:] try: 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 argument, if any. _, err, _ = sys.exc_info() if err.args: status = err.args[0] else: status = None return status