--- a/eric6/DebugClients/Python/coverage/cmdline.py Wed Feb 19 19:38:36 2020 +0100 +++ b/eric6/DebugClients/Python/coverage/cmdline.py Sat Feb 22 14:27:42 2020 +0100 @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Command-line support for coverage.py.""" @@ -8,15 +8,19 @@ import glob import optparse import os.path +import shlex import sys import textwrap import traceback +import coverage +from coverage import Coverage from coverage import env from coverage.collector import CTracer +from coverage.data import line_counts from coverage.debug import info_formatter, info_header -from coverage.execfile import run_python_file, run_python_module -from coverage.misc import BaseCoverageException, ExceptionDuringRun, NoSource +from coverage.execfile import PyRunner +from coverage.misc import BaseCoverageException, ExceptionDuringRun, NoSource, output_encoding from coverage.results import should_fail_under @@ -42,9 +46,13 @@ "Valid values are: %s." ) % ", ".join(CONCURRENCY_CHOICES), ) + context = optparse.make_option( + '', '--context', action='store', metavar="LABEL", + help="The context label to record for this coverage run.", + ) debug = optparse.make_option( '', '--debug', action='store', metavar="OPTS", - help="Debug options, separated by commas", + help="Debug options, separated by commas. [env: COVERAGE_DEBUG]", ) directory = optparse.make_option( '-d', '--directory', action='store', metavar="DIR", @@ -85,6 +93,14 @@ '--skip-covered', action='store_true', help="Skip files with 100% coverage.", ) + skip_empty = optparse.make_option( + '--skip-empty', action='store_true', + help="Skip files with no code.", + ) + show_contexts = optparse.make_option( + '--show-contexts', action='store_true', + help="Show contexts for covered lines.", + ) omit = optparse.make_option( '', '--omit', action='store', metavar="PAT1,PAT2,...", @@ -93,11 +109,28 @@ "Accepts shell-style wildcards, which must be quoted." ), ) + contexts = optparse.make_option( + '', '--contexts', action='store', + metavar="REGEX1,REGEX2,...", + help=( + "Only display data from lines covered in the given contexts. " + "Accepts Python regexes, 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'", ) + output_json = optparse.make_option( + '-o', '', action='store', dest="outfile", + metavar="OUTFILE", + help="Write the JSON report to this file. Defaults to 'coverage.json'", + ) + json_pretty_print = optparse.make_option( + '', '--pretty-print', action='store_true', + help="Format the JSON for human readers.", + ) parallel_mode = optparse.make_option( '-p', '--parallel-mode', action='store_true', help=( @@ -116,8 +149,9 @@ rcfile = optparse.make_option( '', '--rcfile', action='store', help=( - "Specify configuration file. " - "By default '.coveragerc', 'setup.cfg' and 'tox.ini' are tried." + "Specify configuration file. " + "By default '.coveragerc', 'setup.cfg', 'tox.ini', and " + "'pyproject.toml' are tried. [env: COVERAGE_RCFILE]" ), ) source = optparse.make_option( @@ -127,7 +161,7 @@ timid = optparse.make_option( '', '--timid', action='store_true', help=( - "Use a simpler but slower trace method. Try this if you get " + "Use a simpler but slower trace method. Try this if you get " "seemingly impossible results!" ), ) @@ -158,6 +192,7 @@ append=None, branch=None, concurrency=None, + context=None, debug=None, directory=None, fail_under=None, @@ -166,11 +201,14 @@ include=None, module=None, omit=None, + contexts=None, parallel_mode=None, pylib=None, rcfile=True, show_missing=None, skip_covered=None, + skip_empty=None, + show_contexts=None, source=None, timid=None, title=None, @@ -178,11 +216,6 @@ ) 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.""" @@ -195,15 +228,14 @@ """ try: - options, args = \ - super(CoverageOptionParser, self).parse_args(args, options) + 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) + show_help(msg) raise self.OptionParserError @@ -320,13 +352,16 @@ 'html': CmdOptionParser( "html", [ + Opts.contexts, Opts.directory, Opts.fail_under, Opts.ignore_errors, Opts.include, Opts.omit, + Opts.show_contexts, + Opts.skip_covered, + Opts.skip_empty, Opts.title, - Opts.skip_covered, ] + GLOBAL_ARGS, usage="[options] [modules]", description=( @@ -336,15 +371,33 @@ ), ), + 'json': CmdOptionParser( + "json", + [ + Opts.contexts, + Opts.fail_under, + Opts.ignore_errors, + Opts.include, + Opts.omit, + Opts.output_json, + Opts.json_pretty_print, + Opts.show_contexts, + ] + GLOBAL_ARGS, + usage="[options] [modules]", + description="Generate a JSON report of coverage results." + ), + 'report': CmdOptionParser( "report", [ + Opts.contexts, Opts.fail_under, Opts.ignore_errors, Opts.include, Opts.omit, Opts.show_missing, Opts.skip_covered, + Opts.skip_empty, ] + GLOBAL_ARGS, usage="[options] [modules]", description="Report coverage statistics on modules." @@ -356,6 +409,7 @@ Opts.append, Opts.branch, Opts.concurrency, + Opts.context, Opts.include, Opts.module, Opts.omit, @@ -383,45 +437,57 @@ } +def show_help(error=None, topic=None, parser=None): + """Display an error message, or the named topic.""" + assert error or topic or parser + + program_path = sys.argv[0] + if program_path.endswith(os.path.sep + '__main__.py'): + # The path is the main module of a package; get that path instead. + program_path = os.path.dirname(program_path) + program_name = os.path.basename(program_path) + 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 program_name.endswith(auto_suffix): + program_name = program_name[:-len(auto_suffix)] + + help_params = dict(coverage.__dict__) + help_params['program_name'] = program_name + if CTracer is not None: + help_params['extension_modifier'] = 'with C extension' + else: + help_params['extension_modifier'] = 'without C extension' + + if error: + print(error, file=sys.stderr) + print("Use '%s help' for help." % (program_name,), file=sys.stderr) + elif parser: + print(parser.format_help().strip()) + print() + else: + help_msg = textwrap.dedent(HELP_TOPICS.get(topic, '')).strip() + if help_msg: + print(help_msg.format(**help_params)) + else: + print("Don't know topic %r" % topic) + print("Full documentation is at {__url__}".format(**help_params)) + + OK, ERR, FAIL_UNDER = 0, 1, 2 class CoverageScript(object): """The command-line interface to coverage.py.""" - def __init__(self, _covpkg=None, _run_python_file=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 - 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.path_exists = _path_exists or os.path.exists + def __init__(self): self.global_option = False - self.coverage = None - program_path = sys.argv[0] - if program_path.endswith(os.path.sep + '__main__.py'): - # The path is the main module of a package; get that path instead. - program_path = os.path.dirname(program_path) - self.program_name = os.path.basename(program_path) - 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.py. @@ -432,7 +498,7 @@ """ # Collect the command-line options. if not argv: - self.help_fn(topic='minimum_help') + show_help(topic='minimum_help') return OK # The command syntax we parse depends on the first argument. Global @@ -443,11 +509,10 @@ else: parser = CMDS.get(argv[0]) if not parser: - self.help_fn("Unknown command: '%s'" % argv[0]) + show_help("Unknown command: '%s'" % argv[0]) return ERR argv = argv[1:] - parser.help_fn = self.help_fn ok, options, args = parser.parse_args_ok(argv) if not ok: return ERR @@ -456,18 +521,15 @@ if self.do_help(options, args, parser): return OK - # We need to be able to import from the current directory, because - # plugins may try to, for example, to read Django settings. - sys.path[0] = '' - # Listify the list options. source = unshell_list(options.source) omit = unshell_list(options.omit) include = unshell_list(options.include) debug = unshell_list(options.debug) + contexts = unshell_list(options.contexts) # Do something. - self.coverage = self.covpkg.Coverage( + self.coverage = Coverage( data_suffix=options.parallel_mode, cover_pylib=options.pylib, timid=options.timid, @@ -478,6 +540,8 @@ include=include, debug=debug, concurrency=options.concurrency, + check_preimported=True, + context=options.context, ) if options.action == "debug": @@ -504,25 +568,45 @@ ignore_errors=options.ignore_errors, omit=omit, include=include, + contexts=contexts, ) + # We need to be able to import from the current directory, because + # plugins may try to, for example, to read Django settings. + sys.path.insert(0, '') + 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) + skip_covered=options.skip_covered, + skip_empty=options.skip_empty, + **report_args + ) elif options.action == "annotate": - self.coverage.annotate( - directory=options.directory, **report_args) + self.coverage.annotate(directory=options.directory, **report_args) elif options.action == "html": total = self.coverage.html_report( - directory=options.directory, title=options.title, - skip_covered=options.skip_covered, **report_args) + directory=options.directory, + title=options.title, + skip_covered=options.skip_covered, + skip_empty=options.skip_empty, + show_contexts=options.show_contexts, + **report_args + ) elif options.action == "xml": outfile = options.outfile total = self.coverage.xml_report(outfile=outfile, **report_args) + elif options.action == "json": + outfile = options.outfile + total = self.coverage.json_report( + outfile=outfile, + pretty_print=options.pretty_print, + show_contexts=options.show_contexts, + **report_args + ) if total is not None: # Apply the command line fail-under options, and then use the config @@ -537,27 +621,6 @@ 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, file=sys.stderr) - print("Use '%s help' for help." % (self.program_name,), file=sys.stderr) - elif parser: - print(parser.format_help().strip()) - else: - help_params = dict(self.covpkg.__dict__) - help_params['program_name'] = self.program_name - if CTracer is not None: - help_params['extension_modifier'] = 'with C extension' - else: - help_params['extension_modifier'] = 'without C extension' - help_msg = textwrap.dedent(HELP_TOPICS.get(topic, '')).strip() - if help_msg: - print(help_msg.format(**help_params)) - else: - print("Don't know topic %r" % topic) - def do_help(self, options, args, parser): """Deal with help requests. @@ -567,9 +630,9 @@ # Handle help. if options.help: if self.global_option: - self.help_fn(topic='help') + show_help(topic='help') else: - self.help_fn(parser=parser) + show_help(parser=parser) return True if options.action == "help": @@ -577,16 +640,16 @@ for a in args: parser = CMDS.get(a) if parser: - self.help_fn(parser=parser) + show_help(parser=parser) else: - self.help_fn(topic=a) + show_help(topic=a) else: - self.help_fn(topic='help') + show_help(topic='help') return True # Handle version. if options.version: - self.help_fn(topic='version') + show_help(topic='version') return True return False @@ -595,11 +658,22 @@ """Implementation of 'coverage run'.""" if not args: - self.help_fn("Nothing to do.") + if options.module: + # Specified -m with nothing else. + show_help("No module specified for -m") + return ERR + command_line = self.coverage.get_option("run:command_line") + if command_line is not None: + args = shlex.split(command_line) + if args and args[0] == "-m": + options.module = True + args = args[1:] + if not args: + show_help("Nothing to do.") return ERR if options.append and self.coverage.get_option("run:parallel"): - self.help_fn("Can't append to data files in parallel mode.") + show_help("Can't append to data files in parallel mode.") return ERR if options.concurrency == "multiprocessing": @@ -609,35 +683,30 @@ # As it happens, all of these options have no default, meaning # they will be None if they have not been specified. if getattr(options, opt_name) is not None: - self.help_fn( - "Options affecting multiprocessing must be specified " - "in a configuration file." + show_help( + "Options affecting multiprocessing must only be specified " + "in a configuration file.\n" + "Remove --{} from the command line.".format(opt_name) ) return ERR - if not self.coverage.get_option("run:parallel"): - if not options.append: - self.coverage.erase() + runner = PyRunner(args, as_module=bool(options.module)) + runner.prepare() + + if options.append: + self.coverage.load() # Run the script. self.coverage.start() code_ran = True try: - if options.module: - self.run_python_module(args[0], args) - else: - filename = args[0] - self.run_python_file(filename, args) + runner.run() except NoSource: code_ran = False raise 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() return OK @@ -646,7 +715,7 @@ """Implementation of 'coverage debug'.""" if not args: - self.help_fn("What information would you like: config, data, sys?") + show_help("What information would you like: config, data, sys, premain?") return ERR for info in args: @@ -657,12 +726,12 @@ print(" %s" % line) elif info == 'data': self.coverage.load() - data = self.coverage.data + data = self.coverage.get_data() print(info_header("data")) - print("path: %s" % self.coverage.data_files.filename) + print("path: %s" % self.coverage.get_data().data_filename()) if data: print("has_arcs: %r" % data.has_arcs()) - summary = data.line_counts(fullpath=True) + summary = line_counts(data, fullpath=True) filenames = sorted(summary.keys()) print("\n%d files:" % len(filenames)) for f in filenames: @@ -678,8 +747,12 @@ config_info = self.coverage.config.__dict__.items() for line in info_formatter(config_info): print(" %s" % line) + elif info == "premain": + print(info_header("premain")) + from coverage.debug import short_stack + print(short_stack()) else: - self.help_fn("Don't know what you mean by %r" % info) + show_help("Don't know what you mean by %r" % info) return ERR return OK @@ -725,12 +798,12 @@ erase Erase previously collected coverage data. help Get help on using coverage.py. html Create an HTML report. + json Create a JSON report of coverage results. 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} help <command>" for detailed help on any command. - For full documentation, see {__url__} """, 'minimum_help': """\ @@ -739,7 +812,6 @@ 'version': """\ Coverage.py, version {__version__} {extension_modifier} - Documentation at {__url__} """, } @@ -762,7 +834,10 @@ status = ERR except BaseCoverageException as err: # A controlled error inside coverage.py: print the message to the user. - print(err) + msg = err.args[0] + if env.PY2: + msg = msg.encode(output_encoding()) + print(msg) status = ERR except SystemExit as err: # The user called `sys.exit()`. Exit with their argument, if any. @@ -771,3 +846,22 @@ else: status = None return status + +# Profiling using ox_profile. Install it from GitHub: +# pip install git+https://github.com/emin63/ox_profile.git +# +# $set_env.py: COVERAGE_PROFILE - Set to use ox_profile. +_profile = os.environ.get("COVERAGE_PROFILE", "") +if _profile: # pragma: debugging + from ox_profile.core.launchers import SimpleLauncher # pylint: disable=import-error + original_main = main + + def main(argv=None): # pylint: disable=function-redefined + """A wrapper around main that profiles.""" + try: + profiler = SimpleLauncher.launch() + return original_main(argv) + finally: + data, _ = profiler.query(re_filter='coverage', max_records=100) + print(profiler.show(query=data, limit=100, sep='', col='')) + profiler.cancel()