eric7/DebugClients/Python/coverage/control.py

branch
eric7
changeset 8312
800c432b34c8
parent 7975
7d493839a8fc
child 8527
2bd1325d727e
diff -r 4e8b98454baa -r 800c432b34c8 eric7/DebugClients/Python/coverage/control.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/DebugClients/Python/coverage/control.py	Sat May 15 18:45:04 2021 +0200
@@ -0,0 +1,1135 @@
+# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
+
+"""Core control stuff for coverage.py."""
+
+import atexit
+import collections
+import contextlib
+import os
+import os.path
+import platform
+import sys
+import time
+
+from coverage import env
+from coverage.annotate import AnnotateReporter
+from coverage.backward import string_class, iitems
+from coverage.collector import Collector, CTracer
+from coverage.config import read_coverage_config
+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 DefaultValue, ensure_dir_for_file, isolate_module
+from coverage.plugin import FileReporter
+from coverage.plugin_support import Plugins
+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
+
+try:
+    from coverage.multiproc import patch_multiprocessing
+except ImportError:                                         # pragma: only jython
+    # Jython has no multiprocessing module.
+    patch_multiprocessing = None
+
+os = isolate_module(os)
+
+@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:
+        cov.config.from_args(**kwargs)
+        yield
+    finally:
+        cov.config = original_config
+
+
+_DEFAULT_DATAFILE = DefaultValue("MISSING")
+
+class Coverage(object):
+    """Programmatic access to coverage.py.
+
+    To use::
+
+        from coverage import Coverage
+
+        cov = Coverage()
+        cov.start()
+        #.. call your code ..
+        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=_DEFAULT_DATAFILE, data_suffix=None, cover_pylib=None,
+        auto_data=False, timid=None, branch=None, config_file=True,
+        source=None, source_pkgs=None, omit=None, include=None, debug=None,
+        concurrency=None, check_preimported=False, context=None,
+    ):  # pylint: disable=too-many-arguments
+        """
+        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
+        standard library and any packages installed with the interpreter.
+
+        If `auto_data` is true, then any existing data file will be read when
+        coverage measurement starts, and data will be saved automatically when
+        measurement stops.
+
+        If `timid` is true, then a slower and simpler trace function will be
+        used.  This is important for some environments where manipulation of
+        tracing functions breaks the faster trace function.
+
+        If `branch` is true, then branch coverage will be measured in addition
+        to the usual statement coverage.
+
+        `config_file` determines what configuration file to read:
+
+            * If it is ".coveragerc", it is interpreted as if it were True,
+              for backward compatibility.
+
+            * If it is a string, it is the name of the file to read.  If the
+              file can't be read, it is an error.
+
+            * If it is True, then a few standard files names are tried
+              (".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.
+
+        `source` is a list of file paths or package names.  Only code located
+        in the trees indicated by the file paths or package names will be
+        measured.
+
+        `source_pkgs` is a list of package names. It works the same as
+        `source`, but can be used to name packages where the name can also be
+        interpreted as a file path.
+
+        `include` and `omit` are lists of file name patterns. Files that match
+        `include` will be measured, files that match `omit` will not.  Each
+        will also accept a single string argument.
+
+        `debug` is a list of strings indicating what debugging information is
+        desired.
+
+        `concurrency` is a string indicating the concurrency library being used
+        in the measured code.  Without this, coverage.py will get incorrect
+        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.
+
+        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.
+
+        .. versionadded:: 5.3
+            The `source_pkgs` parameter.
+
+        """
+        # 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 = 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, source_pkgs=source_pkgs, run_omit=omit, run_include=include, debug=debug,
+            report_omit=omit, report_include=include,
+            concurrency=concurrency, context=context,
+            )
+
+        # This is injectable by tests.
+        self._debug_file = None
+
+        self._auto_load = self._auto_save = auto_data
+        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._data = self._collector = None
+        self._plugins = None
+        self._inorout = None
+        self._data_suffix = self._run_suffix = None
+        self._exclude_re = 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
+        # 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.
+
+        This is called by the public methods to initialize state. This lets us
+        construct a :class:`Coverage` object, then tweak its state before this
+        function is called.
+
+        """
+        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.
+        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)
+
+        # 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])
+
+    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()
+
+        # '[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'):
+                config_info = sorted(self.config.__dict__.items())
+                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:
+                    header = "sys: " + plugin._coverage_plugin_name
+                    info = plugin.sys_info()
+                    write_formatted_info(self._debug, header, info)
+                wrote_any = True
+
+        if wrote_any:
+            write_formatted_info(self._debug, "end", ())
+
+    def _should_trace(self, filename, frame):
+        """Decide whether to trace execution in `filename`.
+
+        Calls `_should_trace_internal`, and returns the FileDisposition.
+
+        """
+        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):
+        """Check a file name against the include/omit/etc, rules, verbosely.
+
+        Returns a boolean: True if the file should be traced, False if not.
+
+        """
+        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)
+
+        return not reason
+
+    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 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'):
+            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.
+
+        `option_name` is a colon-separated string indicating the section and
+        option name.  For example, the ``branch`` option in the ``[run]``
+        section of the config file would be indicated with `"run:branch"`.
+
+        Returns the value of the option.  The type depends on the option
+        selected.
+
+        As a special case, an `option_name` of ``"paths"`` will return an
+        OrderedDict with the entire ``[paths]`` section value.
+
+        .. versionadded:: 4.0
+
+        """
+        return self.config.get_option(option_name)
+
+    def set_option(self, option_name, value):
+        """Set an option in the configuration.
+
+        `option_name` is a colon-separated string indicating the section and
+        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 an
+        appropriate Python value.  For example, use True for booleans, not the
+        string ``"True"``.
+
+        As an example, calling::
+
+            cov.set_option("run:branch", True)
+
+        has the same effect as this configuration file::
+
+            [run]
+            branch = True
+
+        As a special case, an `option_name` of ``"paths"`` will replace the
+        entire ``[paths]`` section.  The value should be an OrderedDict.
+
+        .. versionadded:: 4.0
+
+        """
+        self.config.set_option(option_name, value)
+
+    def load(self):
+        """Load previously-collected coverage data from the data file."""
+        self._init()
+        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 = InOrOut(
+            warn=self._warn,
+            debug=(self._debug if self._debug.should('trace') else None),
+        )
+        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.
+
+        Coverage measurement only occurs in functions called after
+        :meth:`start` is invoked.  Statements in the same scope as
+        :meth:`start` won't be measured.
+
+        Once you invoke :meth:`start`, you must also call :meth:`stop`
+        eventually, or your process might not shut down cleanly.
+
+        """
+        self._init()
+        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._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._started = False
+
+    def _atexit(self):
+        """Clean up on process shutdown."""
+        if self._debug.should("process"):
+            self._debug.write("atexit: pid: {}, instance: {!r}".format(os.getpid(), self))
+        if self._started:
+            self.stop()
+        if self._auto_save:
+            self.save()
+
+    def erase(self):
+        """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._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."""
+        self._init()
+        setattr(self.config, which + "_list", [])
+        self._exclude_regex_stale()
+
+    def exclude(self, regex, which='exclude'):
+        """Exclude source lines from execution consideration.
+
+        A number of lists of regular expressions are maintained.  Each list
+        selects lines that are treated differently during reporting.
+
+        `which` determines which list is modified.  The "exclude" list selects
+        lines that are not considered executable at all.  The "partial" list
+        indicates lines with branches that are not taken.
+
+        `regex` is a regular expression.  The regex is added to the specified
+        list.  If any of the regexes in the list is found in a line, the line
+        is marked for special treatment during reporting.
+
+        """
+        self._init()
+        excl_list = getattr(self.config, which + "_list")
+        excl_list.append(regex)
+        self._exclude_regex_stale()
+
+    def _exclude_regex_stale(self):
+        """Drop all the compiled exclusion regexes, a list was modified."""
+        self._exclude_re.clear()
+
+    def _exclude_regex(self, which):
+        """Return a compiled regex for the given exclusion list."""
+        if which not in self._exclude_re:
+            excl_list = getattr(self.config, which + "_list")
+            self._exclude_re[which] = join_regex(excl_list)
+        return self._exclude_re[which]
+
+    def get_exclude_list(self, which='exclude'):
+        """Return a list of excluded regex patterns.
+
+        `which` indicates which list is desired.  See :meth:`exclude` for the
+        lists that are available, and their meaning.
+
+        """
+        self._init()
+        return getattr(self.config, which + "_list")
+
+    def save(self):
+        """Save the collected coverage data to the data file."""
+        data = self.get_data()
+        data.write()
+
+    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
+        coverage() constructor) will be read, and combined together into the
+        current measurements.
+
+        `data_paths` is a list of files or directories from which data should
+        be combined. If no list is passed, then the data files from the
+        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._init_data(suffix=None)
+        self._post_init()
+        self.get_data()
+
+        aliases = None
+        if self.config.paths:
+            aliases = PathAliases()
+            for paths in self.config.paths.values():
+                result = paths[0]
+                for pattern in paths[1:]:
+                    aliases.add(pattern, result)
+
+        combine_parallel_data(self._data, aliases=aliases, data_paths=data_paths, strict=strict)
+
+    def get_data(self):
+        """Get the collected data.
+
+        Also warn about various problems collecting data.
+
+        Returns a :class:`coverage.CoverageData`, the collected coverage data.
+
+        .. versionadded:: 4.0
+
+        """
+        self._init()
+        self._init_data(suffix=None)
+        self._post_init()
+
+        for plugin in self._plugins:
+            if not plugin._coverage_enabled:
+                self._collector.plugin_was_disabled(plugin)
+
+        if self._collector and self._collector.flush_data():
+            self._post_save_work()
+
+        return self._data
+
+    def _post_save_work(self):
+        """After saving data, look for warnings, post-work, etc.
+
+        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:
+            self._inorout.warn_unimported_source()
+
+        # Find out if we got any data.
+        if not self._data and self._warn_no_data:
+            self._warn("No data was collected.", slug="no-data-collected")
+
+        # 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:
+            file_paths = collections.defaultdict(list)
+            for file_path, plugin_name in self._inorout.find_possibly_unexecuted_files():
+                file_path = self._file_mapper(file_path)
+                file_paths[plugin_name].append(file_path)
+            for plugin_name, paths in file_paths.items():
+                self._data.touch_files(paths, plugin_name)
+
+        if self.config.note:
+            self._warn("The '[run] note' setting is no longer supported.")
+
+    # Backward compatibility with version 1.
+    def analysis(self, morf):
+        """Like `analysis2` but doesn't return excluded line numbers."""
+        f, s, _, m, mf = self.analysis2(morf)
+        return f, s, m, mf
+
+    def analysis2(self, morf):
+        """Analyze a module.
+
+        `morf` is a module or a file name.  It will be analyzed to determine
+        its coverage statistics.  The return value is a 5-tuple:
+
+        * The file name for the module.
+        * A list of line numbers of executable statements.
+        * A list of line numbers of excluded statements.
+        * A list of line numbers of statements not run (missing from
+          execution).
+        * A readable formatted string of the missing line numbers.
+
+        The analysis uses the source file itself and the current measured
+        coverage data.
+
+        """
+        analysis = self._analyze(morf)
+        return (
+            analysis.filename,
+            sorted(analysis.statements),
+            sorted(analysis.excluded),
+            sorted(analysis.missing),
+            analysis.missing_formatted(),
+            )
+
+    def _analyze(self, it):
+        """Analyze a single morf or code unit.
+
+        Returns an `Analysis` object.
+
+        """
+        # 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(data, it, self._file_mapper)
+
+    def _get_file_reporter(self, morf):
+        """Get a FileReporter for a module or file name."""
+        plugin = None
+        file_reporter = "python"
+
+        if isinstance(morf, string_class):
+            mapped_morf = self._file_mapper(morf)
+            plugin_name = self._data.file_tracer(mapped_morf)
+            if plugin_name:
+                plugin = self._plugins.get(plugin_name)
+
+                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)
+
+        return file_reporter
+
+    def _get_file_reporters(self, morfs=None):
+        """Get a list of FileReporters for a list of modules or file names.
+
+        For each module or file name in `morfs`, find a FileReporter.  Return
+        the list of FileReporters.
+
+        If `morfs` is a single module or file name, this returns a list of one
+        FileReporter.  If `morfs` is empty or None, then the list of all files
+        measured is used to find the FileReporters.
+
+        """
+        if not morfs:
+            morfs = self._data.measured_files()
+
+        # Be sure we have a collection.
+        if not isinstance(morfs, (list, tuple, set)):
+            morfs = [morfs]
+
+        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, omit=None, include=None, skip_covered=None,
+        contexts=None, skip_empty=None, precision=None, sort=None
+    ):
+        """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_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.
+
+        `precision` is the number of digits to display after the decimal
+        point for percentages.
+
+        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.
+
+        .. versionadded:: 5.2
+            The `precision` parameter.
+
+        """
+        with override_config(
+            self,
+            ignore_errors=ignore_errors, report_omit=omit, report_include=include,
+            show_missing=show_missing, skip_covered=skip_covered,
+            report_contexts=contexts, skip_empty=skip_empty, precision=precision,
+            sort=sort
+        ):
+            reporter = SummaryReporter(self)
+            return reporter.report(morfs, outfile=file)
+
+    def annotate(
+        self, morfs=None, directory=None, ignore_errors=None,
+        omit=None, include=None, contexts=None,
+    ):
+        """Annotate a list of modules.
+
+        Each module in `morfs` is annotated.  The source is written to a new
+        file, named with a ",cover" suffix, with each line prefixed with a
+        marker to indicate the coverage of the line.  Covered lines have ">",
+        excluded lines have "-", and missing lines have "!".
+
+        See :meth:`report` for other arguments.
+
+        """
+        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, show_contexts=None, contexts=None,
+        skip_empty=None, precision=None,
+    ):
+        """Generate an HTML report.
+
+        The HTML is written to `directory`.  The file "index.html" is the
+        overview starting point, with links to more detailed pages for
+        individual modules.
+
+        `extra_css` is a path to a file of other CSS to apply on the page.
+        It will be copied into the HTML directory.
+
+        `title` is a text string (not HTML) to use as the title of the HTML
+        report.
+
+        See :meth:`report` for other arguments.
+
+        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.
+
+        """
+        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, show_contexts=show_contexts, report_contexts=contexts,
+            skip_empty=skip_empty, precision=precision,
+        ):
+            reporter = HtmlReporter(self)
+            return reporter.report(morfs)
+
+    def xml_report(
+        self, morfs=None, outfile=None, ignore_errors=None,
+        omit=None, include=None, contexts=None, skip_empty=None,
+    ):
+        """Generate an XML report of coverage results.
+
+        The report is compatible with Cobertura reports.
+
+        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.
+
+        """
+        with override_config(self,
+            ignore_errors=ignore_errors, report_omit=omit, report_include=include,
+            xml_output=outfile, report_contexts=contexts, skip_empty=skip_empty,
+        ):
+            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."""
+
+        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."""
+            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__),
+            ('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 any(slug in k for slug in ("COV", "PY"))
+            )),
+            ('command_line', " ".join(getattr(sys, 'argv', ['-none-']))),
+            ]
+
+        if self._inorout:
+            info.extend(self._inorout.sys_info())
+
+        info.extend(CoverageData.sys_info())
+
+        return info
+
+
+# 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
+
+    Coverage = decorate_methods(show_calls(show_args=True), butnot=['get_data'])(Coverage)
+
+
+def process_startup():
+    """Call this at Python start-up to perhaps measure coverage.
+
+    If the environment variable COVERAGE_PROCESS_START is defined, coverage
+    measurement is started.  The value of the variable is the config file
+    to use.
+
+    There are two ways to configure your Python installation to invoke this
+    function when Python starts:
+
+    #. Create or append to sitecustomize.py to add these lines::
+
+        import coverage
+        coverage.process_startup()
+
+    #. Create a .pth file in your Python installation containing::
+
+        import coverage; coverage.process_startup()
+
+    Returns the :class:`Coverage` instance that was started, or None if it was
+    not started by this call.
+
+    """
+    cps = os.environ.get("COVERAGE_PROCESS_START")
+    if not cps:
+        # No request for coverage, nothing to do.
+        return None
+
+    # This function can be called more than once in a process. This happens
+    # because some virtualenv configurations make the same directory visible
+    # twice in sys.path.  This means that the .pth file will be found twice,
+    # and executed twice, executing this function twice.  We set a global
+    # flag (an attribute on this function) to indicate that coverage.py has
+    # already been started, so we can avoid doing it twice.
+    #
+    # https://github.com/nedbat/coveragepy/issues/340 has more details.
+
+    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
+
+    cov = Coverage(config_file=cps)
+    process_startup.coverage = cov
+    cov._warn_no_data = False
+    cov._warn_unimported_source = False
+    cov._warn_preimported_source = False
+    cov._auto_save = True
+    cov.start()
+
+    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