eric6/DebugClients/Python/coverage/cmdline.py

changeset 7427
362cd1b6f81a
parent 6942
2602857055c5
child 7702
f8b97639deb5
--- 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()

eric ide

mercurial