eric6/DebugClients/Python/coverage/control.py

changeset 7427
362cd1b6f81a
parent 6942
2602857055c5
child 7702
f8b97639deb5
diff -r dc171b1d8261 -r 362cd1b6f81a eric6/DebugClients/Python/coverage/control.py
--- a/eric6/DebugClients/Python/coverage/control.py	Wed Feb 19 19:38:36 2020 +0100
+++ b/eric6/DebugClients/Python/coverage/control.py	Sat Feb 22 14:27:42 2020 +0100
@@ -1,36 +1,35 @@
 # 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
 
 """Core control stuff for coverage.py."""
 
-
 import atexit
-import inspect
-import itertools
+import contextlib
 import os
+import os.path
 import platform
-import re
 import sys
 import time
-import traceback
 
 from coverage import env
 from coverage.annotate import AnnotateReporter
 from coverage.backward import string_class, iitems
-from coverage.collector import Collector
+from coverage.collector import Collector, CTracer
 from coverage.config import read_coverage_config
-from coverage.data import CoverageData, CoverageDataFiles
-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.context import should_start_context_test_function, combine_context_switchers
+from coverage.data import CoverageData, combine_parallel_data
+from coverage.debug import DebugControl, short_stack, write_formatted_info
+from coverage.disposition import disposition_debug_msg
+from coverage.files import PathAliases, abs_file, relative_filename, set_relative_directory
 from coverage.html import HtmlReporter
+from coverage.inorout import InOrOut
+from coverage.jsonreport import JsonReporter
 from coverage.misc import CoverageException, bool_or_none, join_regex
-from coverage.misc import file_be_gone, isolate_module
+from coverage.misc import DefaultValue, ensure_dir_for_file, isolate_module
 from coverage.plugin import FileReporter
 from coverage.plugin_support import Plugins
-from coverage.python import PythonFileReporter, source_for_file
+from coverage.python import PythonFileReporter
+from coverage.report import render_report
 from coverage.results import Analysis, Numbers
 from coverage.summary import SummaryReporter
 from coverage.xmlreport import XmlReporter
@@ -43,22 +42,23 @@
 
 os = isolate_module(os)
 
-# Pypy has some unusual stuff in the "stdlib".  Consider those locations
-# 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:
+@contextlib.contextmanager
+def override_config(cov, **kwargs):
+    """Temporarily tweak the configuration of `cov`.
+
+    The arguments are applied to `cov.config` with the `from_args` method.
+    At the end of the with-statement, the old configuration is restored.
+    """
+    original_config = cov.config
+    cov.config = cov.config.copy()
     try:
-        import _structseq
-    except ImportError:
-        pass
+        cov.config.from_args(**kwargs)
+        yield
+    finally:
+        cov.config = original_config
 
-    try:
-        import _pypy_irc_topic
-    except ImportError:
-        pass
 
+_DEFAULT_DATAFILE = DefaultValue("MISSING")
 
 class Coverage(object):
     """Programmatic access to coverage.py.
@@ -73,18 +73,45 @@
         cov.stop()
         cov.html_report(directory='covhtml')
 
+    Note: in keeping with Python custom, names starting with underscore are
+    not part of the public API. They might stop working at any point.  Please
+    limit yourself to documented methods to avoid problems.
+
     """
+
+    # The stack of started Coverage instances.
+    _instances = []
+
+    @classmethod
+    def current(cls):
+        """Get the latest started `Coverage` instance, if any.
+
+        Returns: a `Coverage` instance, or None.
+
+        .. versionadded:: 5.0
+
+        """
+        if cls._instances:
+            return cls._instances[-1]
+        else:
+            return None
+
     def __init__(
-        self, data_file=None, data_suffix=None, cover_pylib=None,
+        self, data_file=_DEFAULT_DATAFILE, data_suffix=None, cover_pylib=None,
         auto_data=False, timid=None, branch=None, config_file=True,
         source=None, omit=None, include=None, debug=None,
-        concurrency=None,
+        concurrency=None, check_preimported=False, context=None,
     ):
         """
-        `data_file` is the base name of the data file to use, defaulting to
-        ".coverage".  `data_suffix` is appended (with a dot) to `data_file` to
-        create the final file name.  If `data_suffix` is simply True, then a
-        suffix is created with the machine and process identity included.
+        Many of these arguments duplicate and override values that can be
+        provided in a configuration file.  Parameters that are missing here
+        will use values from the config file.
+
+        `data_file` is the base name of the data file to use. The config value
+        defaults to ".coverage".  None can be provided to prevent writing a data
+        file.  `data_suffix` is appended (with a dot) to `data_file` to create
+        the final file name.  If `data_suffix` is simply True, then a suffix is
+        created with the machine and process identity included.
 
         `cover_pylib` is a boolean determining whether Python code installed
         with the Python interpreter is measured.  This includes the Python
@@ -132,58 +159,73 @@
         "eventlet", "gevent", "multiprocessing", or "thread" (the default).
         This can also be a list of these strings.
 
+        If `check_preimported` is true, then when coverage is started, the
+        already-imported files will be checked to see if they should be
+        measured by coverage.  Importing measured files before coverage is
+        started can mean that code is missed.
+
+        `context` is a string to use as the :ref:`static context
+        <static_contexts>` label for collected data.
+
         .. versionadded:: 4.0
             The `concurrency` parameter.
 
         .. versionadded:: 4.2
             The `concurrency` parameter can now be a list of strings.
 
+        .. versionadded:: 5.0
+            The `check_preimported` and `context` parameters.
+
         """
+        # data_file=None means no disk file at all. data_file missing means
+        # use the value from the config file.
+        self._no_disk = data_file is None
+        if data_file is _DEFAULT_DATAFILE:
+            data_file = None
+
         # Build our configuration from a number of sources.
-        self.config_file, self.config = read_coverage_config(
+        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, run_omit=omit, run_include=include, debug=debug,
             report_omit=omit, report_include=include,
-            concurrency=concurrency,
+            concurrency=concurrency, context=context,
             )
 
         # This is injectable by tests.
         self._debug_file = None
 
         self._auto_load = self._auto_save = auto_data
-        self._data_suffix = data_suffix
-
-        # The matchers for _should_trace.
-        self.source_match = None
-        self.source_pkgs_match = None
-        self.pylib_match = self.cover_match = None
-        self.include_match = self.omit_match = None
+        self._data_suffix_specified = data_suffix
 
         # Is it ok for no data to be collected?
         self._warn_no_data = True
         self._warn_unimported_source = True
+        self._warn_preimported_source = check_preimported
+        self._no_warn_slugs = None
 
         # A record of all the warnings that have been issued.
         self._warnings = []
 
         # 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_paths = self.cover_paths = None
-        self.data_suffix = self.run_suffix = None
+        self._data = self._collector = None
+        self._plugins = None
+        self._inorout = None
+        self._inorout_class = InOrOut
+        self._data_suffix = self._run_suffix = None
         self._exclude_re = None
-        self.debug = None
+        self._debug = None
+        self._file_mapper = None
 
         # State machine variables:
         # Have we initialized everything?
         self._inited = False
+        self._inited_for_start = False
         # Have we started collecting and not stopped it?
         self._started = False
+        # Should we write the debug output?
+        self._should_write_debug = True
 
         # If we have sub-process measurement happening automatically, then we
         # want any explicit creation of a Coverage object to mean, this process
@@ -209,378 +251,61 @@
         # Create and configure the debugging controller. COVERAGE_DEBUG_FILE
         # is an environment variable, the name of a file to append debug logs
         # to.
-        if self._debug_file is None:
-            debug_file_name = os.environ.get("COVERAGE_DEBUG_FILE")
-            if debug_file_name:
-                self._debug_file = open(debug_file_name, "a")
-            else:
-                self._debug_file = sys.stderr
-        self.debug = DebugControl(self.config.debug, self._debug_file)
+        self._debug = DebugControl(self.config.debug, self._debug_file)
+
+        if "multiprocessing" in (self.config.concurrency or ()):
+            # Multi-processing uses parallel for the subprocesses, so also use
+            # it for the main process.
+            self.config.parallel = True
 
         # _exclude_re is a dict that maps exclusion list names to compiled regexes.
         self._exclude_re = {}
 
         set_relative_directory()
+        self._file_mapper = relative_filename if self.config.relative_files else abs_file
 
         # Load plugins
-        self.plugins = Plugins.load_plugins(self.config.plugins, self.config, self.debug)
+        self._plugins = Plugins.load_plugins(self.config.plugins, self.config, self._debug)
 
         # Run configuring plugins.
-        for plugin in self.plugins.configurers:
+        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.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.run_omit)
-        self.include = prep_patterns(self.config.run_include)
-
-        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,
-            check_include=self._check_include_omit_etc,
-            timid=self.config.timid,
-            branch=self.config.branch,
-            warn=self._warn,
-            concurrency=concurrency,
-            )
-
-        # Early warning if we aren't going to be able to support plugins.
-        if self.plugins.file_tracers and not self.collector.supports_plugins:
-            self._warn(
-                "Plugin file tracers (%s) aren't supported with %s" % (
-                    ", ".join(
-                        plugin._coverage_plugin_name
-                            for plugin in self.plugins.file_tracers
-                        ),
-                    self.collector.tracer_name(),
-                    )
-                )
-            for plugin in self.plugins.file_tracers:
-                plugin._coverage_enabled = False
-
-        # Suffixes are a bit tricky.  We want to use the data suffix only when
-        # collecting data, not when combining data.  So we save it as
-        # `self.run_suffix` now, and promote it to `self.data_suffix` if we
-        # find that we are collecting data later.
-        if self._data_suffix or self.config.parallel:
-            if not isinstance(self._data_suffix, string_class):
-                # if data_suffix=True, use .machinename.pid.random
-                self._data_suffix = True
-        else:
-            self._data_suffix = None
-        self.data_suffix = None
-        self.run_suffix = self._data_suffix
+    def _post_init(self):
+        """Stuff to do after everything is initialized."""
+        if self._should_write_debug:
+            self._should_write_debug = False
+            self._write_startup_debug()
 
-        # Create the data file.  We do this at construction time so that the
-        # 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, debug=self.debug,
-        )
-
-        # The directories for files considered "installed with the interpreter".
-        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, _pypy_irc_topic, re, _structseq, traceback):
-                if m is not None and hasattr(m, "__file__"):
-                    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
-                # the path to exclude.
-                structseq_new = _structseq.structseq_new
-                try:
-                    structseq_file = structseq_new.func_code.co_filename
-                except AttributeError:
-                    structseq_file = structseq_new.__code__.co_filename
-                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_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
-            import six
-            for mod in [contracts, six]:
-                self.cover_paths.append(self._canonical_path(mod))
-
-        # Set the reporting precision.
-        Numbers.set_precision(self.config.precision)
-
-        atexit.register(self._atexit)
-
-        # 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_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.
-        self._write_startup_debug()
+        # '[run] _crash' will raise an exception if the value is close by in
+        # the call stack, for testing error handling.
+        if self.config._crash and self.config._crash in short_stack(limit=4):
+            raise Exception("Crashing because called by {}".format(self.config._crash))
 
     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'):
+        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)
+                config_info = [(k, v) for k, v in config_info if not k.startswith('_')]
+                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:
+            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)
+                    write_formatted_info(self._debug, header, info)
                 wrote_any = True
 
         if wrote_any:
-            write_formatted_info(self.debug, "end", ())
-
-    def _canonical_path(self, morf, directory=False):
-        """Return the canonical path of the module or file `morf`.
-
-        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.
-
-        """
-        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.
-
-        For configurability's sake, we allow __main__ modules to be matched by
-        their importable name.
-
-        If loaded via runpy (aka -m), we can usually recover the "original"
-        full dotted module name, otherwise, we resort to interpreting the
-        file name to get the module's name.  In the case that the module name
-        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__':
-            # This is the usual case: an imported module.
-            return dunder_name
-
-        loader = module_globals.get('__loader__', None)
-        for attrname in ('fullname', 'name'):   # attribute renamed in py3.2
-            if hasattr(loader, attrname):
-                fullname = getattr(loader, attrname)
-            else:
-                continue
-
-            if isinstance(fullname, str) and fullname != '__main__':
-                # Module loaded via: runpy -m
-                return fullname
-
-        # Script as first argument to Python command line.
-        inspectedname = inspect.getmodulename(filename)
-        if inspectedname is not None:
-            return inspectedname
-        else:
-            return dunder_name
-
-    def _should_trace_internal(self, filename, frame):
-        """Decide whether to trace execution in `filename`, with a reason.
-
-        This function is called from the trace function.  As each new file name
-        is encountered, this function determines whether it is traced or not.
-
-        Returns a FileDisposition object.
-
-        """
-        original_filename = filename
-        disp = _disposition_init(self.collector.file_disposition_class, filename)
-
-        def nope(disp, reason):
-            """Simple helper to make it easy to return NO."""
-            disp.trace = False
-            disp.reason = reason
-            return disp
-
-        # Compiled Python files have two file names: frame.f_code.co_filename is
-        # the file name at the time the .pyc was compiled.  The second name is
-        # __file__, which is where the .pyc was actually loaded from.  Since
-        # .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 and frame.f_globals.get('__file__')
-        if 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):
-                    # Files shouldn't be renamed when moved. This happens when
-                    # exec'ing code.  If it seems like something is wrong with
-                    # the frame's file name, then just use the original.
-                    filename = original_filename
-
-        if not filename:
-            # Empty string is pretty useless.
-            return nope(disp, "empty string isn't a file name")
-
-        if filename.startswith('memory:'):
-            return nope(disp, "memory isn't traceable")
-
-        if filename.startswith('<'):
-            # Lots of non-file execution is represented with artificial
-            # file names like "<string>", "<doctest readme.txt[0]>", or
-            # "<exec_function>".  Don't ever trace these executions, since we
-            # can't do anything with the data later anyway.
-            return nope(disp, "not a real file name")
-
-        # pyexpat does a dumb thing, calling the trace function explicitly from
-        # C code with a C file name.
-        if re.search(r"[/\\]Modules[/\\]pyexpat.c", filename):
-            return nope(disp, "pyexpat lies about itself")
-
-        # Jython reports the .class file to the tracer, use the source file.
-        if filename.endswith("$py.class"):
-            filename = filename[:-9] + ".py"
-
-        canonical = canonical_filename(filename)
-        disp.canonical_filename = canonical
-
-        # Try the plugins, see if they have an opinion about the file.
-        plugin = None
-        for plugin in self.plugins.file_tracers:
-            if not plugin._coverage_enabled:
-                continue
-
-            try:
-                file_tracer = plugin.file_tracer(canonical)
-                if file_tracer is not None:
-                    file_tracer._coverage_plugin = plugin
-                    disp.trace = True
-                    disp.file_tracer = file_tracer
-                    if file_tracer.has_dynamic_source_filename():
-                        disp.has_dynamic_filename = True
-                    else:
-                        disp.source_filename = canonical_filename(
-                            file_tracer.source_filename()
-                        )
-                    break
-            except Exception:
-                self._warn(
-                    "Disabling plug-in %r due to an exception:" % (
-                        plugin._coverage_plugin_name
-                    )
-                )
-                traceback.print_exc()
-                plugin._coverage_enabled = False
-                continue
-        else:
-            # No plugin wanted it: it's Python.
-            disp.trace = True
-            disp.source_filename = canonical
-
-        if not disp.has_dynamic_filename:
-            if not disp.source_filename:
-                raise CoverageException(
-                    "Plugin %r didn't set source_filename for %r" %
-                    (plugin, disp.original_filename)
-                )
-            reason = self._check_include_omit_etc_internal(
-                disp.source_filename, frame,
-            )
-            if reason:
-                nope(disp, reason)
-
-        return disp
-
-    def _check_include_omit_etc_internal(self, filename, frame):
-        """Check a file name against the include, omit, etc, rules.
-
-        Returns a string or None.  String means, don't trace, and is the reason
-        why.  None means no reason found to not trace.
-
-        """
-        modulename = self._name_for_module(frame.f_globals, filename)
-
-        # If the user specified source or include, then that's authoritative
-        # about the outer bound of what to measure and we don't have to apply
-        # any canned exclusions. If they didn't, then we have to exclude the
-        # stdlib and coverage.py directories.
-        if self.source_match:
-            if self.source_pkgs_match.match(modulename):
-                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):
-                return "falls outside the --include trees"
-        else:
-            # If we aren't supposed to trace installed code, then check if this
-            # is near the Python standard library and skip it if so.
-            if self.pylib_match and self.pylib_match.match(filename):
-                return "is in the stdlib"
-
-            # We exclude the coverage.py code itself, since a little of it
-            # will be measured otherwise.
-            if self.cover_match and self.cover_match.match(filename):
-                return "is part of coverage.py"
-
-        # Check the file against the omit pattern.
-        if self.omit_match and self.omit_match.match(filename):
-            return "is inside an --omit pattern"
-
-        # No reason found to skip this file.
-        return None
+            write_formatted_info(self._debug, "end", ())
 
     def _should_trace(self, filename, frame):
         """Decide whether to trace execution in `filename`.
@@ -588,9 +313,9 @@
         Calls `_should_trace_internal`, and returns the FileDisposition.
 
         """
-        disp = self._should_trace_internal(filename, frame)
-        if self.debug.should('trace'):
-            self.debug.write(_disposition_debug_msg(disp))
+        disp = self._inorout.should_trace(filename, frame)
+        if self._debug.should('trace'):
+            self._debug.write(disposition_debug_msg(disp))
         return disp
 
     def _check_include_omit_etc(self, filename, frame):
@@ -599,32 +324,42 @@
         Returns a boolean: True if the file should be traced, False if not.
 
         """
-        reason = self._check_include_omit_etc_internal(filename, frame)
-        if self.debug.should('trace'):
+        reason = self._inorout.check_include_omit_etc(filename, frame)
+        if self._debug.should('trace'):
             if not reason:
                 msg = "Including %r" % (filename,)
             else:
                 msg = "Not including %r: %s" % (filename, reason)
-            self.debug.write(msg)
+            self._debug.write(msg)
 
         return not reason
 
-    def _warn(self, msg, slug=None):
+    def _warn(self, msg, slug=None, once=False):
         """Use `msg` as a warning.
 
         For warning suppression, use `slug` as the shorthand.
+
+        If `once` is true, only show this warning once (determined by the
+        slug.)
+
         """
-        if slug in self.config.disable_warnings:
+        if self._no_warn_slugs is None:
+            self._no_warn_slugs = list(self.config.disable_warnings)
+
+        if slug in self._no_warn_slugs:
             # Don't issue the warning
             return
 
         self._warnings.append(msg)
         if slug:
             msg = "%s (%s)" % (msg, slug)
-        if self.debug.should('pid'):
+        if self._debug.should('pid'):
             msg = "[%d] %s" % (os.getpid(), msg)
         sys.stderr.write("Coverage.py warning: %s\n" % msg)
 
+        if once:
+            self._no_warn_slugs.append(slug)
+
     def get_option(self, option_name):
         """Get an option from the configuration.
 
@@ -664,17 +399,107 @@
         """
         self.config.set_option(option_name, value)
 
-    def use_cache(self, usecache):
-        """Obsolete method."""
-        self._init()
-        if not usecache:
-            self._warn("use_cache(False) is no longer supported.")
-
     def load(self):
         """Load previously-collected coverage data from the data file."""
         self._init()
-        self.collector.reset()
-        self.data_files.read(self.data)
+        if self._collector:
+            self._collector.reset()
+        should_skip = self.config.parallel and not os.path.exists(self.config.data_file)
+        if not should_skip:
+            self._init_data(suffix=None)
+        self._post_init()
+        if not should_skip:
+            self._data.read()
+
+    def _init_for_start(self):
+        """Initialization for start()"""
+        # Construct the collector.
+        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.config_file)
+
+        dycon = self.config.dynamic_context
+        if not dycon or dycon == "none":
+            context_switchers = []
+        elif dycon == "test_function":
+            context_switchers = [should_start_context_test_function]
+        else:
+            raise CoverageException(
+                "Don't understand dynamic_context setting: {!r}".format(dycon)
+            )
+
+        context_switchers.extend(
+            plugin.dynamic_context for plugin in self._plugins.context_switchers
+        )
+
+        should_start_context = combine_context_switchers(context_switchers)
+
+        self._collector = Collector(
+            should_trace=self._should_trace,
+            check_include=self._check_include_omit_etc,
+            should_start_context=should_start_context,
+            file_mapper=self._file_mapper,
+            timid=self.config.timid,
+            branch=self.config.branch,
+            warn=self._warn,
+            concurrency=concurrency,
+            )
+
+        suffix = self._data_suffix_specified
+        if suffix or self.config.parallel:
+            if not isinstance(suffix, string_class):
+                # if data_suffix=True, use .machinename.pid.random
+                suffix = True
+        else:
+            suffix = None
+
+        self._init_data(suffix)
+
+        self._collector.use_data(self._data, self.config.context)
+
+        # Early warning if we aren't going to be able to support plugins.
+        if self._plugins.file_tracers and not self._collector.supports_plugins:
+            self._warn(
+                "Plugin file tracers (%s) aren't supported with %s" % (
+                    ", ".join(
+                        plugin._coverage_plugin_name
+                            for plugin in self._plugins.file_tracers
+                        ),
+                    self._collector.tracer_name(),
+                    )
+                )
+            for plugin in self._plugins.file_tracers:
+                plugin._coverage_enabled = False
+
+        # Create the file classifying substructure.
+        self._inorout = self._inorout_class(warn=self._warn)
+        self._inorout.configure(self.config)
+        self._inorout.plugins = self._plugins
+        self._inorout.disp_class = self._collector.file_disposition_class
+
+        # It's useful to write debug info after initing for start.
+        self._should_write_debug = True
+
+        atexit.register(self._atexit)
+
+    def _init_data(self, suffix):
+        """Create a data file if we don't have one yet."""
+        if self._data is None:
+            # Create the data file.  We do this at construction time so that the
+            # data file will be written into the directory where the process
+            # started rather than wherever the process eventually chdir'd to.
+            ensure_dir_for_file(self.config.data_file)
+            self._data = CoverageData(
+                basename=self.config.data_file,
+                suffix=suffix,
+                warn=self._warn,
+                debug=self._debug,
+                no_disk=self._no_disk,
+            )
 
     def start(self):
         """Start measuring code coverage.
@@ -688,45 +513,82 @@
 
         """
         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 not self._inited_for_start:
+            self._inited_for_start = True
+            self._init_for_start()
+        self._post_init()
+
+        # Issue warnings for possible problems.
+        self._inorout.warn_conflicting_settings()
+
+        # See if we think some code that would eventually be measured has
+        # already been imported.
+        if self._warn_preimported_source:
+            self._inorout.warn_already_imported_files()
+
         if self._auto_load:
             self.load()
 
-        self.collector.start()
+        self._collector.start()
         self._started = True
+        self._instances.append(self)
 
     def stop(self):
         """Stop measuring code coverage."""
+        if self._instances:
+            if self._instances[-1] is self:
+                self._instances.pop()
         if self._started:
-            self.collector.stop()
+            self._collector.stop()
         self._started = False
 
     def _atexit(self):
         """Clean up on process shutdown."""
-        if self.debug.should("process"):
-            self.debug.write("atexit: {0!r}".format(self))
+        if self._debug.should("process"):
+            self._debug.write("atexit: {!r}".format(self))
         if self._started:
             self.stop()
         if self._auto_save:
             self.save()
 
     def erase(self):
-        """Erase previously-collected coverage data.
+        """Erase previously collected coverage data.
 
         This removes the in-memory data collected in this session as well as
         discarding the data file.
 
         """
         self._init()
-        self.collector.reset()
-        self.data.erase()
-        self.data_files.erase(parallel=self.config.parallel)
+        self._post_init()
+        if self._collector:
+            self._collector.reset()
+        self._init_data(suffix=None)
+        self._data.erase(parallel=self.config.parallel)
+        self._data = None
+        self._inited_for_start = False
+
+    def switch_context(self, new_context):
+        """Switch to a new dynamic context.
+
+        `new_context` is a string to use as the :ref:`dynamic context
+        <dynamic_contexts>` label for collected data.  If a :ref:`static
+        context <static_contexts>` is in use, the static and dynamic context
+        labels will be joined together with a pipe character.
+
+        Coverage collection must be started already.
+
+        .. versionadded:: 5.0
+
+        """
+        if not self._started:                           # pragma: part started
+            raise CoverageException(
+                "Cannot switch context, coverage is not started"
+                )
+
+        if self._collector.should_start_context:
+            self._warn("Conflicting dynamic contexts", slug="dynamic-conflict", once=True)
+
+        self._collector.switch_context(new_context)
 
     def clear_exclude(self, which='exclude'):
         """Clear the exclude list."""
@@ -777,9 +639,8 @@
 
     def save(self):
         """Save the collected coverage data to the data file."""
-        self._init()
-        self.get_data()
-        self.data_files.write(self.data, suffix=self.data_suffix)
+        data = self.get_data()
+        data.write()
 
     def combine(self, data_paths=None, strict=False):
         """Combine together a number of similarly-named coverage data files.
@@ -804,6 +665,8 @@
 
         """
         self._init()
+        self._init_data(suffix=None)
+        self._post_init()
         self.get_data()
 
         aliases = None
@@ -814,9 +677,7 @@
                 for pattern in paths[1:]:
                     aliases.add(pattern, result)
 
-        self.data_files.combine_parallel_data(
-            self.data, aliases=aliases, data_paths=data_paths, strict=strict,
-        )
+        combine_parallel_data(self._data, aliases=aliases, data_paths=data_paths, strict=strict)
 
     def get_data(self):
         """Get the collected data.
@@ -829,11 +690,13 @@
 
         """
         self._init()
+        self._init_data(suffix=None)
+        self._post_init()
 
-        if self.collector.save_data(self.data):
+        if self._collector and self._collector.flush_data():
             self._post_save_work()
 
-        return self.data
+        return self._data
 
     def _post_save_work(self):
         """After saving data, look for warnings, post-work, etc.
@@ -845,78 +708,21 @@
         # 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_unmatched:
-                self._warn_about_unmeasured_code(pkg)
+            self._inorout.warn_unimported_source()
 
         # Find out if we got any data.
-        if not self.data and self._warn_no_data:
+        if not self._data and self._warn_no_data:
             self._warn("No data was collected.", slug="no-data-collected")
 
-        # Find files that were never executed at all.
-        for pkg in self.source_pkgs:
-            if (not pkg in sys.modules or
-                not module_has_file(sys.modules[pkg])):
-                continue
-            pkg_file = source_for_file(sys.modules[pkg].__file__)
-            self._find_unexecuted_files(self._canonical_path(pkg_file))
-
-        for src in self.source:
-            self._find_unexecuted_files(src)
+        # Touch all the files that could have executed, so that we can
+        # mark completely unexecuted files as 0% covered.
+        if self._data is not None:
+            for file_path, plugin_name in self._inorout.find_possibly_unexecuted_files():
+                file_path = self._file_mapper(file_path)
+                self._data.touch_file(file_path, plugin_name)
 
         if self.config.note:
-            self.data.add_run_info(note=self.config.note)
-
-    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
-
-        if module_is_namespace(mod):
-            # A namespace package. It's OK for this not to have been traced,
-            # since there is no code directly in it.
-            return
-
-        if not module_has_file(mod):
-            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)
+            self._warn("The '[run] note' setting is no longer supported.")
 
     # Backward compatibility with version 1.
     def analysis(self, morf):
@@ -941,7 +747,6 @@
         coverage data.
 
         """
-        self._init()
         analysis = self._analyze(morf)
         return (
             analysis.filename,
@@ -957,11 +762,16 @@
         Returns an `Analysis` object.
 
         """
-        self.get_data()
+        # All reporting comes through here, so do reporting initialization.
+        self._init()
+        Numbers.set_precision(self.config.precision)
+        self._post_init()
+
+        data = self.get_data()
         if not isinstance(it, FileReporter):
             it = self._get_file_reporter(it)
 
-        return Analysis(self.data, it)
+        return Analysis(data, it, self._file_mapper)
 
     def _get_file_reporter(self, morf):
         """Get a FileReporter for a module or file name."""
@@ -969,19 +779,19 @@
         file_reporter = "python"
 
         if isinstance(morf, string_class):
-            abs_morf = abs_file(morf)
-            plugin_name = self.data.file_tracer(abs_morf)
+            mapped_morf = self._file_mapper(morf)
+            plugin_name = self._data.file_tracer(mapped_morf)
             if plugin_name:
-                plugin = self.plugins.get(plugin_name)
+                plugin = self._plugins.get(plugin_name)
 
-        if plugin:
-            file_reporter = plugin.file_reporter(abs_morf)
-            if file_reporter is None:
-                raise CoverageException(
-                    "Plugin %r did not provide a file reporter for %r." % (
-                        plugin._coverage_plugin_name, morf
-                    )
-                )
+                if plugin:
+                    file_reporter = plugin.file_reporter(mapped_morf)
+                    if file_reporter is None:
+                        raise CoverageException(
+                            "Plugin %r did not provide a file reporter for %r." % (
+                                plugin._coverage_plugin_name, morf
+                            )
+                        )
 
         if file_reporter == "python":
             file_reporter = PythonFileReporter(morf, self)
@@ -1000,49 +810,70 @@
 
         """
         if not morfs:
-            morfs = self.data.measured_files()
+            morfs = self._data.measured_files()
 
-        # Be sure we have a list.
-        if not isinstance(morfs, (list, tuple)):
+        # Be sure we have a collection.
+        if not isinstance(morfs, (list, tuple, set)):
             morfs = [morfs]
 
-        file_reporters = []
-        for morf in morfs:
-            file_reporter = self._get_file_reporter(morf)
-            file_reporters.append(file_reporter)
-
+        file_reporters = [self._get_file_reporter(morf) for morf in morfs]
         return file_reporters
 
     def report(
         self, morfs=None, show_missing=None, ignore_errors=None,
-        file=None,                  # pylint: disable=redefined-builtin
-        omit=None, include=None, skip_covered=None,
+        file=None, omit=None, include=None, skip_covered=None,
+        contexts=None, skip_empty=None,
     ):
-        """Write a summary report to `file`.
+        """Write a textual summary report to `file`.
 
         Each module in `morfs` is listed, with counts of statements, executed
         statements, missing statements, and a list of lines missed.
 
+        If `show_missing` is true, then details of which lines or branches are
+        missing will be included in the report.  If `ignore_errors` is true,
+        then a failure while reporting a single file will not stop the entire
+        report.
+
+        `file` is a file-like object, suitable for writing.
+
         `include` is a list of file name patterns.  Files that match will be
         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.
+        If `skip_covered` is true, don't report on files with 100% coverage.
+
+        If `skip_empty` is true, don't report on empty files (those that have
+        no statements).
+
+        `contexts` is a list of regular expressions.  Only data from
+        :ref:`dynamic contexts <dynamic_contexts>` that match one of those
+        expressions (using :func:`re.search <python:re.search>`) will be
+        included in the report.
+
+        All of the arguments default to the settings read from the
+        :ref:`configuration file <config>`.
 
         Returns a float, the total percentage covered.
 
+        .. versionadded:: 4.0
+            The `skip_covered` parameter.
+
+        .. versionadded:: 5.0
+            The `contexts` and `skip_empty` parameters.
+
         """
-        self.get_data()
-        self.config.from_args(
+        with override_config(
+            self,
             ignore_errors=ignore_errors, report_omit=omit, report_include=include,
             show_missing=show_missing, skip_covered=skip_covered,
-            )
-        reporter = SummaryReporter(self, self.config)
-        return reporter.report(morfs, outfile=file)
+            report_contexts=contexts, skip_empty=skip_empty,
+        ):
+            reporter = SummaryReporter(self)
+            return reporter.report(morfs, outfile=file)
 
     def annotate(
         self, morfs=None, directory=None, ignore_errors=None,
-        omit=None, include=None,
+        omit=None, include=None, contexts=None,
     ):
         """Annotate a list of modules.
 
@@ -1054,16 +885,17 @@
         See :meth:`report` for other arguments.
 
         """
-        self.get_data()
-        self.config.from_args(
-            ignore_errors=ignore_errors, report_omit=omit, report_include=include
-            )
-        reporter = AnnotateReporter(self, self.config)
-        reporter.report(morfs, directory=directory)
+        with override_config(self,
+            ignore_errors=ignore_errors, report_omit=omit,
+            report_include=include, report_contexts=contexts,
+        ):
+            reporter = AnnotateReporter(self)
+            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,
-                    skip_covered=None):
+                    skip_covered=None, show_contexts=None, contexts=None,
+                    skip_empty=None):
         """Generate an HTML report.
 
         The HTML is written to `directory`.  The file "index.html" is the
@@ -1080,19 +912,25 @@
 
         Returns a float, the total percentage covered.
 
+        .. note::
+            The HTML report files are generated incrementally based on the
+            source files and coverage results. If you modify the report files,
+            the changes will not be considered.  You should be careful about
+            changing the files in the report folder.
+
         """
-        self.get_data()
-        self.config.from_args(
+        with override_config(self,
             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)
+            skip_covered=skip_covered, show_contexts=show_contexts, report_contexts=contexts,
+            skip_empty=skip_empty,
+        ):
+            reporter = HtmlReporter(self)
+            return reporter.report(morfs)
 
     def xml_report(
         self, morfs=None, outfile=None, ignore_errors=None,
-        omit=None, include=None,
+        omit=None, include=None, contexts=None,
     ):
         """Generate an XML report of coverage results.
 
@@ -1106,40 +944,35 @@
         Returns a float, the total percentage covered.
 
         """
-        self.get_data()
-        self.config.from_args(
+        with override_config(self,
             ignore_errors=ignore_errors, report_omit=omit, report_include=include,
-            xml_output=outfile,
-            )
-        file_to_close = None
-        delete_file = False
-        if self.config.xml_output:
-            if self.config.xml_output == '-':
-                outfile = sys.stdout
-            else:
-                # Ensure that the output directory is created; done here
-                # because this report pre-opens the output file.
-                # HTMLReport does this using the Report plumbing because
-                # its task is more complex, being multiple files.
-                output_dir = os.path.dirname(self.config.xml_output)
-                if output_dir and not os.path.isdir(output_dir):
-                    os.makedirs(output_dir)
-                open_kwargs = {}
-                if env.PY3:
-                    open_kwargs['encoding'] = 'utf8'
-                outfile = open(self.config.xml_output, "w", **open_kwargs)
-                file_to_close = outfile
-        try:
-            reporter = XmlReporter(self, self.config)
-            return reporter.report(morfs, outfile=outfile)
-        except CoverageException:
-            delete_file = True
-            raise
-        finally:
-            if file_to_close:
-                file_to_close.close()
-                if delete_file:
-                    file_be_gone(self.config.xml_output)
+            xml_output=outfile, report_contexts=contexts,
+        ):
+            return render_report(self.config.xml_output, XmlReporter(self), morfs)
+
+    def json_report(
+        self, morfs=None, outfile=None, ignore_errors=None,
+        omit=None, include=None, contexts=None, pretty_print=None,
+        show_contexts=None
+    ):
+        """Generate a JSON report of coverage results.
+
+        Each module in `morfs` is included in the report.  `outfile` is the
+        path to write the file to, "-" will write to stdout.
+
+        See :meth:`report` for other arguments.
+
+        Returns a float, the total percentage covered.
+
+        .. versionadded:: 5.0
+
+        """
+        with override_config(self,
+            ignore_errors=ignore_errors, report_omit=omit, report_include=include,
+            json_output=outfile, report_contexts=contexts, json_pretty_print=pretty_print,
+            json_show_contexts=show_contexts
+        ):
+            return render_report(self.config.json_output, JsonReporter(self), morfs)
 
     def sys_info(self):
         """Return a list of (key, value) pairs showing internal information."""
@@ -1147,6 +980,7 @@
         import coverage as covmod
 
         self._init()
+        self._post_init()
 
         def plugin_info(plugins):
             """Make an entry for the sys_info from a list of plug-ins."""
@@ -1161,84 +995,51 @@
         info = [
             ('version', covmod.__version__),
             ('coverage', covmod.__file__),
-            ('cover_paths', self.cover_paths),
-            ('pylib_paths', self.pylib_paths),
-            ('tracer', self.collector.tracer_name()),
-            ('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),
+            ('tracer', self._collector.tracer_name() if self._collector else "-none-"),
+            ('CTracer', 'available' if CTracer else "unavailable"),
+            ('plugins.file_tracers', plugin_info(self._plugins.file_tracers)),
+            ('plugins.configurers', plugin_info(self._plugins.configurers)),
+            ('plugins.context_switchers', plugin_info(self._plugins.context_switchers)),
+            ('configs_attempted', self.config.attempted_config_files),
+            ('configs_read', self.config.config_files_read),
+            ('config_file', self.config.config_file),
+            ('config_contents',
+                repr(self.config._config_contents)
+                if self.config._config_contents
+                else '-none-'
+            ),
+            ('data_file', self._data.data_filename() if self._data is not None else "-none-"),
             ('python', sys.version.replace('\n', '')),
             ('platform', platform.platform()),
             ('implementation', platform.python_implementation()),
             ('executable', sys.executable),
+            ('def_encoding', sys.getdefaultencoding()),
+            ('fs_encoding', sys.getfilesystemencoding()),
+            ('pid', os.getpid()),
             ('cwd', os.getcwd()),
             ('path', sys.path),
             ('environment', sorted(
                 ("%s = %s" % (k, v))
                 for k, v in iitems(os.environ)
-                if k.startswith(("COV", "PY"))
+                if any(slug in k for slug in ("COV", "PY"))
             )),
-            ('command_line', " ".join(getattr(sys, 'argv', ['???']))),
+            ('command_line', " ".join(getattr(sys, 'argv', ['-none-']))),
             ]
 
-        matcher_names = [
-            'source_match', 'source_pkgs_match',
-            'include_match', 'omit_match',
-            'cover_match', 'pylib_match',
-            ]
+        if self._inorout:
+            info.extend(self._inorout.sys_info())
 
-        for matcher_name in matcher_names:
-            matcher = getattr(self, matcher_name)
-            if matcher:
-                matcher_info = matcher.info()
-            else:
-                matcher_info = '-none-'
-            info.append((matcher_name, matcher_info))
+        info.extend(CoverageData.sys_info())
 
         return info
 
 
-def module_is_namespace(mod):
-    """Is the module object `mod` a PEP420 namespace module?"""
-    return hasattr(mod, '__path__') and getattr(mod, '__file__', None) is None
-
-
-def module_has_file(mod):
-    """Does the module object `mod` have an existing __file__ ?"""
-    mod__file__ = getattr(mod, '__file__', None)
-    if mod__file__ is None:
-        return False
-    return os.path.exists(mod__file__)
-
-
-# FileDisposition "methods": FileDisposition is a pure value object, so it can
-# be implemented in either C or Python.  Acting on them is done with these
-# functions.
+# Mega debugging...
+# $set_env.py: COVERAGE_DEBUG_CALLS - Lots and lots of output about calls to Coverage.
+if int(os.environ.get("COVERAGE_DEBUG_CALLS", 0)):              # pragma: debugging
+    from coverage.debug import decorate_methods, show_calls
 
-def _disposition_init(cls, original_filename):
-    """Construct and initialize a new FileDisposition object."""
-    disp = cls()
-    disp.original_filename = original_filename
-    disp.canonical_filename = original_filename
-    disp.source_filename = None
-    disp.trace = False
-    disp.reason = ""
-    disp.file_tracer = None
-    disp.has_dynamic_filename = False
-    return disp
-
-
-def _disposition_debug_msg(disp):
-    """Make a nice debug message of what the FileDisposition is doing."""
-    if disp.trace:
-        msg = "Tracing %r" % (disp.original_filename,)
-        if disp.file_tracer:
-            msg += ": will be traced by %r" % disp.file_tracer
-    else:
-        msg = "Not tracing %r: %s" % (disp.original_filename, disp.reason)
-    return msg
+    Coverage = decorate_methods(show_calls(show_args=True), butnot=['get_data'])(Coverage)
 
 
 def process_startup():
@@ -1286,10 +1087,11 @@
 
     cov = Coverage(config_file=cps)
     process_startup.coverage = cov
-    cov.start()
     cov._warn_no_data = False
     cov._warn_unimported_source = False
+    cov._warn_preimported_source = False
     cov._auto_save = True
+    cov.start()
 
     return cov
 

eric ide

mercurial