DebugClients/Python/coverage/control.py

changeset 6219
d6c795b5ce33
parent 5178
878ce843ca9f
child 6649
f1b3a73831c9
--- a/DebugClients/Python/coverage/control.py	Sat Apr 07 13:17:06 2018 +0200
+++ b/DebugClients/Python/coverage/control.py	Sat Apr 07 13:35:10 2018 +0200
@@ -3,43 +3,61 @@
 
 """Core control stuff for coverage.py."""
 
+
 import atexit
 import inspect
+import itertools
 import os
 import platform
 import re
 import sys
+import time
 import traceback
 
-from coverage import env, files
+from coverage import env
 from coverage.annotate import AnnotateReporter
 from coverage.backward import string_class, iitems
 from coverage.collector import Collector
-from coverage.config import CoverageConfig
+from coverage.config import read_coverage_config
 from coverage.data import CoverageData, CoverageDataFiles
-from coverage.debug import DebugControl
+from coverage.debug import DebugControl, write_formatted_info
 from coverage.files import TreeMatcher, FnmatchMatcher
 from coverage.files import PathAliases, find_python_files, prep_patterns
+from coverage.files import canonical_filename, set_relative_directory
 from coverage.files import ModuleMatcher, abs_file
 from coverage.html import HtmlReporter
 from coverage.misc import CoverageException, bool_or_none, join_regex
 from coverage.misc import file_be_gone, isolate_module
-from coverage.monkey import patch_multiprocessing
 from coverage.plugin import FileReporter
 from coverage.plugin_support import Plugins
-from coverage.python import PythonFileReporter
+from coverage.python import PythonFileReporter, source_for_file
 from coverage.results import Analysis, Numbers
 from coverage.summary import SummaryReporter
 from coverage.xmlreport import XmlReporter
 
+try:
+    from coverage.multiproc import patch_multiprocessing
+except ImportError:                                         # pragma: only jython
+    # Jython has no multiprocessing module.
+    patch_multiprocessing = None
+
 os = isolate_module(os)
 
 # Pypy has some unusual stuff in the "stdlib".  Consider those locations
-# when deciding where the stdlib is.
-try:
-    import _structseq
-except ImportError:
-    _structseq = None
+# when deciding where the stdlib is.  These modules are not used for anything,
+# they are modules importable from the pypy lib directories, so that we can
+# find those directories.
+_structseq = _pypy_irc_topic = None
+if env.PYPY:
+    try:
+        import _structseq
+    except ImportError:
+        pass
+
+    try:
+        import _pypy_irc_topic
+    except ImportError:
+        pass
 
 
 class Coverage(object):
@@ -92,8 +110,8 @@
               file can't be read, it is an error.
 
             * If it is True, then a few standard files names are tried
-              (".coveragerc", "setup.cfg").  It is not an error for these files
-              to not be found.
+              (".coveragerc", "setup.cfg", "tox.ini").  It is not an error for
+              these files to not be found.
 
             * If it is False, then no configuration file is read.
 
@@ -110,55 +128,31 @@
 
         `concurrency` is a string indicating the concurrency library being used
         in the measured code.  Without this, coverage.py will get incorrect
-        results.  Valid strings are "greenlet", "eventlet", "gevent",
-        "multiprocessing", or "thread" (the default).
+        results if these libraries are in use.  Valid strings are "greenlet",
+        "eventlet", "gevent", "multiprocessing", or "thread" (the default).
+        This can also be a list of these strings.
 
         .. versionadded:: 4.0
             The `concurrency` parameter.
 
-        """
-        # Build our configuration from a number of sources:
-        # 1: defaults:
-        self.config = CoverageConfig()
-
-        # 2: from the rcfile, .coveragerc or setup.cfg file:
-        if config_file:
-            did_read_rc = False
-            # Some API users were specifying ".coveragerc" to mean the same as
-            # True, so make it so.
-            if config_file == ".coveragerc":
-                config_file = True
-            specified_file = (config_file is not True)
-            if not specified_file:
-                config_file = ".coveragerc"
-
-            did_read_rc = self.config.from_file(config_file)
+        .. versionadded:: 4.2
+            The `concurrency` parameter can now be a list of strings.
 
-            if not did_read_rc:
-                if specified_file:
-                    raise CoverageException(
-                        "Couldn't read '%s' as a config file" % config_file
-                        )
-                self.config.from_file("setup.cfg", section_prefix="coverage:")
-
-        # 3: from environment variables:
-        env_data_file = os.environ.get('COVERAGE_FILE')
-        if env_data_file:
-            self.config.data_file = env_data_file
-        debugs = os.environ.get('COVERAGE_DEBUG')
-        if debugs:
-            self.config.debug.extend(debugs.split(","))
-
-        # 4: from constructor arguments:
-        self.config.from_args(
+        """
+        # Build our configuration from a number of sources.
+        self.config_file, self.config = read_coverage_config(
+            config_file=config_file,
             data_file=data_file, cover_pylib=cover_pylib, timid=timid,
             branch=branch, parallel=bool_or_none(data_suffix),
-            source=source, omit=omit, include=include, debug=debug,
+            source=source, run_omit=omit, run_include=include, debug=debug,
+            report_omit=omit, report_include=include,
             concurrency=concurrency,
             )
 
+        # This is injectable by tests.
         self._debug_file = None
-        self._auto_data = auto_data
+
+        self._auto_load = self._auto_save = auto_data
         self._data_suffix = data_suffix
 
         # The matchers for _should_trace.
@@ -176,10 +170,11 @@
 
         # Other instance attributes, set later.
         self.omit = self.include = self.source = None
+        self.source_pkgs_unmatched = None
         self.source_pkgs = None
         self.data = self.data_files = self.collector = None
         self.plugins = None
-        self.pylib_dirs = self.cover_dirs = None
+        self.pylib_paths = self.cover_paths = None
         self.data_suffix = self.run_suffix = None
         self._exclude_re = None
         self.debug = None
@@ -189,8 +184,14 @@
         self._inited = False
         # Have we started collecting and not stopped it?
         self._started = False
-        # Have we measured some data and not harvested it?
-        self._measured = False
+
+        # If we have sub-process measurement happening automatically, then we
+        # want any explicit creation of a Coverage object to mean, this process
+        # is already coverage-aware, so don't auto-measure it.  By now, the
+        # auto-creation of a Coverage object has already happened.  But we can
+        # find it and tell it not to save its data.
+        if not env.METACOV:
+            _prevent_sub_process_measurement()
 
     def _init(self):
         """Set all the initial state.
@@ -203,6 +204,8 @@
         if self._inited:
             return
 
+        self._inited = True
+
         # Create and configure the debugging controller. COVERAGE_DEBUG_FILE
         # is an environment variable, the name of a file to append debug logs
         # to.
@@ -214,32 +217,45 @@
                 self._debug_file = sys.stderr
         self.debug = DebugControl(self.config.debug, self._debug_file)
 
+        # _exclude_re is a dict that maps exclusion list names to compiled regexes.
+        self._exclude_re = {}
+
+        set_relative_directory()
+
         # Load plugins
         self.plugins = Plugins.load_plugins(self.config.plugins, self.config, self.debug)
 
-        # _exclude_re is a dict that maps exclusion list names to compiled
-        # regexes.
-        self._exclude_re = {}
-        self._exclude_regex_stale()
-
-        files.set_relative_directory()
+        # Run configuring plugins.
+        for plugin in self.plugins.configurers:
+            # We need an object with set_option and get_option. Either self or
+            # self.config will do. Choosing randomly stops people from doing
+            # other things with those objects, against the public API.  Yes,
+            # this is a bit childish. :)
+            plugin.configure([self, self.config][int(time.time()) % 2])
 
         # The source argument can be directories or package names.
         self.source = []
         self.source_pkgs = []
         for src in self.config.source or []:
-            if os.path.exists(src):
-                self.source.append(files.canonical_filename(src))
+            if os.path.isdir(src):
+                self.source.append(canonical_filename(src))
             else:
                 self.source_pkgs.append(src)
+        self.source_pkgs_unmatched = self.source_pkgs[:]
 
-        self.omit = prep_patterns(self.config.omit)
-        self.include = prep_patterns(self.config.include)
+        self.omit = prep_patterns(self.config.run_omit)
+        self.include = prep_patterns(self.config.run_include)
 
-        concurrency = self.config.concurrency
-        if concurrency == "multiprocessing":
-            patch_multiprocessing()
-            concurrency = None
+        concurrency = self.config.concurrency or []
+        if "multiprocessing" in concurrency:
+            if not patch_multiprocessing:
+                raise CoverageException(                    # pragma: only jython
+                    "multiprocessing is not supported on this Python"
+                )
+            patch_multiprocessing(rcfile=self.config_file)
+            # Multi-processing uses parallel for the subprocesses, so also use
+            # it for the main process.
+            self.config.parallel = True
 
         self.collector = Collector(
             should_trace=self._should_trace,
@@ -281,19 +297,22 @@
         # data file will be written into the directory where the process
         # started rather than wherever the process eventually chdir'd to.
         self.data = CoverageData(debug=self.debug)
-        self.data_files = CoverageDataFiles(basename=self.config.data_file, warn=self._warn)
+        self.data_files = CoverageDataFiles(
+            basename=self.config.data_file, warn=self._warn, debug=self.debug,
+        )
 
         # The directories for files considered "installed with the interpreter".
-        self.pylib_dirs = set()
+        self.pylib_paths = set()
         if not self.config.cover_pylib:
             # Look at where some standard modules are located. That's the
             # indication for "installed with the interpreter". In some
             # environments (virtualenv, for example), these modules may be
             # spread across a few locations. Look at all the candidate modules
             # we've imported, and take all the different ones.
-            for m in (atexit, inspect, os, platform, re, _structseq, traceback):
+            for m in (atexit, inspect, os, platform, _pypy_irc_topic, re, _structseq, traceback):
                 if m is not None and hasattr(m, "__file__"):
-                    self.pylib_dirs.add(self._canonical_dir(m))
+                    self.pylib_paths.add(self._canonical_path(m, directory=True))
+
             if _structseq and not hasattr(_structseq, '__file__'):
                 # PyPy 2.4 has no __file__ in the builtin modules, but the code
                 # objects still have the file names.  So dig into one to find
@@ -303,94 +322,77 @@
                     structseq_file = structseq_new.func_code.co_filename
                 except AttributeError:
                     structseq_file = structseq_new.__code__.co_filename
-                self.pylib_dirs.add(self._canonical_dir(structseq_file))
+                self.pylib_paths.add(self._canonical_path(structseq_file))
 
         # To avoid tracing the coverage.py code itself, we skip anything
         # located where we are.
-        self.cover_dirs = [self._canonical_dir(__file__)]
+        self.cover_paths = [self._canonical_path(__file__, directory=True)]
         if env.TESTING:
+            # Don't include our own test code.
+            self.cover_paths.append(os.path.join(self.cover_paths[0], "tests"))
+
             # When testing, we use PyContracts, which should be considered
             # part of coverage.py, and it uses six. Exclude those directories
             # just as we exclude ourselves.
-            import contracts, six
+            import contracts
+            import six
             for mod in [contracts, six]:
-                self.cover_dirs.append(self._canonical_dir(mod))
+                self.cover_paths.append(self._canonical_path(mod))
 
         # Set the reporting precision.
         Numbers.set_precision(self.config.precision)
 
         atexit.register(self._atexit)
 
-        self._inited = True
-
         # Create the matchers we need for _should_trace
         if self.source or self.source_pkgs:
             self.source_match = TreeMatcher(self.source)
             self.source_pkgs_match = ModuleMatcher(self.source_pkgs)
         else:
-            if self.cover_dirs:
-                self.cover_match = TreeMatcher(self.cover_dirs)
-            if self.pylib_dirs:
-                self.pylib_match = TreeMatcher(self.pylib_dirs)
+            if self.cover_paths:
+                self.cover_match = TreeMatcher(self.cover_paths)
+            if self.pylib_paths:
+                self.pylib_match = TreeMatcher(self.pylib_paths)
         if self.include:
             self.include_match = FnmatchMatcher(self.include)
         if self.omit:
             self.omit_match = FnmatchMatcher(self.omit)
 
         # The user may want to debug things, show info if desired.
-        wrote_any = False
-        if self.debug.should('config'):
-            config_info = sorted(self.config.__dict__.items())
-            self.debug.write_formatted_info("config", config_info)
-            wrote_any = True
+        self._write_startup_debug()
 
-        if self.debug.should('sys'):
-            self.debug.write_formatted_info("sys", self.sys_info())
-            for plugin in self.plugins:
-                header = "sys: " + plugin._coverage_plugin_name
-                info = plugin.sys_info()
-                self.debug.write_formatted_info(header, info)
-            wrote_any = True
+    def _write_startup_debug(self):
+        """Write out debug info at startup if needed."""
+        wrote_any = False
+        with self.debug.without_callers():
+            if self.debug.should('config'):
+                config_info = sorted(self.config.__dict__.items())
+                write_formatted_info(self.debug, "config", config_info)
+                wrote_any = True
+
+            if self.debug.should('sys'):
+                write_formatted_info(self.debug, "sys", self.sys_info())
+                for plugin in self.plugins:
+                    header = "sys: " + plugin._coverage_plugin_name
+                    info = plugin.sys_info()
+                    write_formatted_info(self.debug, header, info)
+                wrote_any = True
 
         if wrote_any:
-            self.debug.write_formatted_info("end", ())
+            write_formatted_info(self.debug, "end", ())
 
-    def _canonical_dir(self, morf):
-        """Return the canonical directory of the module or file `morf`."""
-        morf_filename = PythonFileReporter(morf, self).filename
-        return os.path.split(morf_filename)[0]
+    def _canonical_path(self, morf, directory=False):
+        """Return the canonical path of the module or file `morf`.
 
-    def _source_for_file(self, filename):
-        """Return the source file for `filename`.
-
-        Given a file name being traced, return the best guess as to the source
-        file to attribute it to.
+        If the module is a package, then return its directory. If it is a
+        module, then return its file, unless `directory` is True, in which
+        case return its enclosing directory.
 
         """
-        if filename.endswith(".py"):
-            # .py files are themselves source files.
-            return filename
-
-        elif filename.endswith((".pyc", ".pyo")):
-            # Bytecode files probably have source files near them.
-            py_filename = filename[:-1]
-            if os.path.exists(py_filename):
-                # Found a .py file, use that.
-                return py_filename
-            if env.WINDOWS:
-                # On Windows, it could be a .pyw file.
-                pyw_filename = py_filename + "w"
-                if os.path.exists(pyw_filename):
-                    return pyw_filename
-            # Didn't find source, but it's probably the .py file we want.
-            return py_filename
-
-        elif filename.endswith("$py.class"):
-            # Jython is easy to guess.
-            return filename[:-9] + ".py"
-
-        # No idea, just use the file name as-is.
-        return filename
+        morf_path = PythonFileReporter(morf, self).filename
+        if morf_path.endswith("__init__.py") or directory:
+            morf_path = os.path.split(morf_path)[0]
+        return morf_path
 
     def _name_for_module(self, module_globals, filename):
         """Get the name of the module for a set of globals and file name.
@@ -404,6 +406,10 @@
         can't be determined, None is returned.
 
         """
+        if module_globals is None:          # pragma: only ironpython
+            # IronPython doesn't provide globals: https://github.com/IronLanguages/main/issues/1296
+            module_globals = {}
+
         dunder_name = module_globals.get('__name__', None)
 
         if isinstance(dunder_name, str) and dunder_name != '__main__':
@@ -452,9 +458,9 @@
         # .pyc files can be moved after compilation (for example, by being
         # installed), we look for __file__ in the frame and prefer it to the
         # co_filename value.
-        dunder_file = frame.f_globals.get('__file__')
+        dunder_file = frame.f_globals and frame.f_globals.get('__file__')
         if dunder_file:
-            filename = self._source_for_file(dunder_file)
+            filename = source_for_file(dunder_file)
             if original_filename and not original_filename.startswith('<'):
                 orig = os.path.basename(original_filename)
                 if orig != os.path.basename(filename):
@@ -486,7 +492,7 @@
         if filename.endswith("$py.class"):
             filename = filename[:-9] + ".py"
 
-        canonical = files.canonical_filename(filename)
+        canonical = canonical_filename(filename)
         disp.canonical_filename = canonical
 
         # Try the plugins, see if they have an opinion about the file.
@@ -504,13 +510,13 @@
                     if file_tracer.has_dynamic_source_filename():
                         disp.has_dynamic_filename = True
                     else:
-                        disp.source_filename = files.canonical_filename(
+                        disp.source_filename = canonical_filename(
                             file_tracer.source_filename()
                         )
                     break
             except Exception:
                 self._warn(
-                    "Disabling plugin %r due to an exception:" % (
+                    "Disabling plug-in %r due to an exception:" % (
                         plugin._coverage_plugin_name
                     )
                 )
@@ -551,11 +557,9 @@
         # stdlib and coverage.py directories.
         if self.source_match:
             if self.source_pkgs_match.match(modulename):
-                if modulename in self.source_pkgs:
-                    self.source_pkgs.remove(modulename)
-                return None  # There's no reason to skip this file.
-
-            if not self.source_match.match(filename):
+                if modulename in self.source_pkgs_unmatched:
+                    self.source_pkgs_unmatched.remove(modulename)
+            elif not self.source_match.match(filename):
                 return "falls outside the --source trees"
         elif self.include_match:
             if not self.include_match.match(filename):
@@ -605,9 +609,18 @@
 
         return not reason
 
-    def _warn(self, msg):
-        """Use `msg` as a warning."""
+    def _warn(self, msg, slug=None):
+        """Use `msg` as a warning.
+
+        For warning suppression, use `slug` as the shorthand.
+        """
+        if slug in self.config.disable_warnings:
+            # Don't issue the warning
+            return
+
         self._warnings.append(msg)
+        if slug:
+            msg = "%s (%s)" % (msg, slug)
         if self.debug.should('pid'):
             msg = "[%d] %s" % (os.getpid(), msg)
         sys.stderr.write("Coverage.py warning: %s\n" % msg)
@@ -633,8 +646,8 @@
         option name.  For example, the ``branch`` option in the ``[run]``
         section of the config file would be indicated with ``"run:branch"``.
 
-        `value` is the new value for the option.  This should be a Python
-        value where appropriate.  For example, use True for booleans, not the
+        `value` is the new value for the option.  This should be an
+        appropriate Python value.  For example, use True for booleans, not the
         string ``"True"``.
 
         As an example, calling::
@@ -666,7 +679,7 @@
     def start(self):
         """Start measuring code coverage.
 
-        Coverage measurement actually occurs in functions called after
+        Coverage measurement only occurs in functions called after
         :meth:`start` is invoked.  Statements in the same scope as
         :meth:`start` won't be measured.
 
@@ -675,16 +688,18 @@
 
         """
         self._init()
+        if self.include:
+            if self.source or self.source_pkgs:
+                self._warn("--include is ignored because --source is set", slug="include-ignored")
         if self.run_suffix:
             # Calling start() means we're running code, so use the run_suffix
             # as the data_suffix when we eventually save the data.
             self.data_suffix = self.run_suffix
-        if self._auto_data:
+        if self._auto_load:
             self.load()
 
         self.collector.start()
         self._started = True
-        self._measured = True
 
     def stop(self):
         """Stop measuring code coverage."""
@@ -694,9 +709,11 @@
 
     def _atexit(self):
         """Clean up on process shutdown."""
+        if self.debug.should("process"):
+            self.debug.write("atexit: {0!r}".format(self))
         if self._started:
             self.stop()
-        if self._auto_data:
+        if self._auto_save:
             self.save()
 
     def erase(self):
@@ -764,7 +781,7 @@
         self.get_data()
         self.data_files.write(self.data, suffix=self.data_suffix)
 
-    def combine(self, data_paths=None):
+    def combine(self, data_paths=None, strict=False):
         """Combine together a number of similarly-named coverage data files.
 
         All coverage data files whose name starts with `data_file` (from the
@@ -776,9 +793,15 @@
         directory indicated by the current data file (probably the current
         directory) will be combined.
 
+        If `strict` is true, then it is an error to attempt to combine when
+        there are no data files to combine.
+
         .. versionadded:: 4.0
             The `data_paths` parameter.
 
+        .. versionadded:: 4.3
+            The `strict` parameter.
+
         """
         self._init()
         self.get_data()
@@ -791,10 +814,12 @@
                 for pattern in paths[1:]:
                     aliases.add(pattern, result)
 
-        self.data_files.combine_parallel_data(self.data, aliases=aliases, data_paths=data_paths)
+        self.data_files.combine_parallel_data(
+            self.data, aliases=aliases, data_paths=data_paths, strict=strict,
+        )
 
     def get_data(self):
-        """Get the collected data and reset the collector.
+        """Get the collected data.
 
         Also warn about various problems collecting data.
 
@@ -804,46 +829,98 @@
 
         """
         self._init()
-        if not self._measured:
-            return self.data
+
+        if self.collector.save_data(self.data):
+            self._post_save_work()
+
+        return self.data
 
-        self.collector.save_data(self.data)
+    def _post_save_work(self):
+        """After saving data, look for warnings, post-work, etc.
 
-        # If there are still entries in the source_pkgs list, then we never
-        # encountered those packages.
+        Warn about things that should have happened but didn't.
+        Look for unexecuted files.
+
+        """
+        # If there are still entries in the source_pkgs_unmatched list,
+        # then we never encountered those packages.
         if self._warn_unimported_source:
-            for pkg in self.source_pkgs:
-                if pkg not in sys.modules:
-                    self._warn("Module %s was never imported." % pkg)
-                elif not (
-                    hasattr(sys.modules[pkg], '__file__') and
-                    os.path.exists(sys.modules[pkg].__file__)
-                ):
-                    self._warn("Module %s has no Python source." % pkg)
-                else:
-                    self._warn("Module %s was previously imported, but not measured." % pkg)
+            for pkg in self.source_pkgs_unmatched:
+                self._warn_about_unmeasured_code(pkg)
 
         # Find out if we got any data.
         if not self.data and self._warn_no_data:
-            self._warn("No data was collected.")
+            self._warn("No data was collected.", slug="no-data-collected")
 
         # Find files that were never executed at all.
-        for src in self.source:
-            for py_file in find_python_files(src):
-                py_file = files.canonical_filename(py_file)
+        for pkg in self.source_pkgs:
+            if (not pkg in sys.modules or
+                not hasattr(sys.modules[pkg], '__file__') or
+                not os.path.exists(sys.modules[pkg].__file__)):
+                continue
+            pkg_file = source_for_file(sys.modules[pkg].__file__)
+            self._find_unexecuted_files(self._canonical_path(pkg_file))
 
-                if self.omit_match and self.omit_match.match(py_file):
-                    # Turns out this file was omitted, so don't pull it back
-                    # in as unexecuted.
-                    continue
-
-                self.data.touch_file(py_file)
+        for src in self.source:
+            self._find_unexecuted_files(src)
 
         if self.config.note:
             self.data.add_run_info(note=self.config.note)
 
-        self._measured = False
-        return self.data
+    def _warn_about_unmeasured_code(self, pkg):
+        """Warn about a package or module that we never traced.
+
+        `pkg` is a string, the name of the package or module.
+
+        """
+        mod = sys.modules.get(pkg)
+        if mod is None:
+            self._warn("Module %s was never imported." % pkg, slug="module-not-imported")
+            return
+
+        is_namespace = hasattr(mod, '__path__') and not hasattr(mod, '__file__')
+        has_file = hasattr(mod, '__file__') and os.path.exists(mod.__file__)
+
+        if is_namespace:
+            # A namespace package. It's OK for this not to have been traced,
+            # since there is no code directly in it.
+            return
+
+        if not has_file:
+            self._warn("Module %s has no Python source." % pkg, slug="module-not-python")
+            return
+
+        # The module was in sys.modules, and seems like a module with code, but
+        # we never measured it. I guess that means it was imported before
+        # coverage even started.
+        self._warn(
+            "Module %s was previously imported, but not measured" % pkg,
+            slug="module-not-measured",
+        )
+
+    def _find_plugin_files(self, src_dir):
+        """Get executable files from the plugins."""
+        for plugin in self.plugins.file_tracers:
+            for x_file in plugin.find_executable_files(src_dir):
+                yield x_file, plugin._coverage_plugin_name
+
+    def _find_unexecuted_files(self, src_dir):
+        """Find unexecuted files in `src_dir`.
+
+        Search for files in `src_dir` that are probably importable,
+        and add them as unexecuted files in `self.data`.
+
+        """
+        py_files = ((py_file, None) for py_file in find_python_files(src_dir))
+        plugin_files = self._find_plugin_files(src_dir)
+
+        for file_path, plugin_name in itertools.chain(py_files, plugin_files):
+            file_path = canonical_filename(file_path)
+            if self.omit_match and self.omit_match.match(file_path):
+                # Turns out this file was omitted, so don't pull it back
+                # in as unexecuted.
+                continue
+            self.data.touch_file(file_path, plugin_name)
 
     # Backward compatibility with version 1.
     def analysis(self, morf):
@@ -954,12 +1031,14 @@
         included in the report. Files matching `omit` will not be included in
         the report.
 
+        If `skip_covered` is True, don't report on files with 100% coverage.
+
         Returns a float, the total percentage covered.
 
         """
         self.get_data()
         self.config.from_args(
-            ignore_errors=ignore_errors, omit=omit, include=include,
+            ignore_errors=ignore_errors, report_omit=omit, report_include=include,
             show_missing=show_missing, skip_covered=skip_covered,
             )
         reporter = SummaryReporter(self, self.config)
@@ -981,13 +1060,14 @@
         """
         self.get_data()
         self.config.from_args(
-            ignore_errors=ignore_errors, omit=omit, include=include
+            ignore_errors=ignore_errors, report_omit=omit, report_include=include
             )
         reporter = AnnotateReporter(self, self.config)
         reporter.report(morfs, directory=directory)
 
     def html_report(self, morfs=None, directory=None, ignore_errors=None,
-                    omit=None, include=None, extra_css=None, title=None):
+                    omit=None, include=None, extra_css=None, title=None,
+                    skip_covered=None):
         """Generate an HTML report.
 
         The HTML is written to `directory`.  The file "index.html" is the
@@ -1007,8 +1087,9 @@
         """
         self.get_data()
         self.config.from_args(
-            ignore_errors=ignore_errors, omit=omit, include=include,
+            ignore_errors=ignore_errors, report_omit=omit, report_include=include,
             html_dir=directory, extra_css=extra_css, html_title=title,
+            skip_covered=skip_covered,
             )
         reporter = HtmlReporter(self, self.config)
         return reporter.report(morfs)
@@ -1031,7 +1112,7 @@
         """
         self.get_data()
         self.config.from_args(
-            ignore_errors=ignore_errors, omit=omit, include=include,
+            ignore_errors=ignore_errors, report_omit=omit, report_include=include,
             xml_output=outfile,
             )
         file_to_close = None
@@ -1071,20 +1152,24 @@
 
         self._init()
 
-        ft_plugins = []
-        for ft in self.plugins.file_tracers:
-            ft_name = ft._coverage_plugin_name
-            if not ft._coverage_enabled:
-                ft_name += " (disabled)"
-            ft_plugins.append(ft_name)
+        def plugin_info(plugins):
+            """Make an entry for the sys_info from a list of plug-ins."""
+            entries = []
+            for plugin in plugins:
+                entry = plugin._coverage_plugin_name
+                if not plugin._coverage_enabled:
+                    entry += " (disabled)"
+                entries.append(entry)
+            return entries
 
         info = [
             ('version', covmod.__version__),
             ('coverage', covmod.__file__),
-            ('cover_dirs', self.cover_dirs),
-            ('pylib_dirs', self.pylib_dirs),
+            ('cover_paths', self.cover_paths),
+            ('pylib_paths', self.pylib_paths),
             ('tracer', self.collector.tracer_name()),
-            ('plugins.file_tracers', ft_plugins),
+            ('plugins.file_tracers', plugin_info(self.plugins.file_tracers)),
+            ('plugins.configurers', plugin_info(self.plugins.configurers)),
             ('config_files', self.config.attempted_config_files),
             ('configs_read', self.config.config_files),
             ('data_path', self.data_files.filename),
@@ -1185,15 +1270,23 @@
     # https://bitbucket.org/ned/coveragepy/issue/340/keyerror-subpy has more
     # details.
 
-    if hasattr(process_startup, "done"):
+    if hasattr(process_startup, "coverage"):
         # We've annotated this function before, so we must have already
         # started coverage.py in this process.  Nothing to do.
         return None
 
-    process_startup.done = True
-    cov = Coverage(config_file=cps, auto_data=True)
+    cov = Coverage(config_file=cps)
+    process_startup.coverage = cov
     cov.start()
     cov._warn_no_data = False
     cov._warn_unimported_source = False
+    cov._auto_save = True
 
     return cov
+
+
+def _prevent_sub_process_measurement():
+    """Stop any subprocess auto-measurement from writing data."""
+    auto_created_coverage = getattr(process_startup, "coverage", None)
+    if auto_created_coverage is not None:
+        auto_created_coverage._auto_save = False

eric ide

mercurial