summaryrefslogtreecommitdiff
path: root/coverage/control.py
diff options
context:
space:
mode:
Diffstat (limited to 'coverage/control.py')
-rw-r--r--coverage/control.py287
1 files changed, 150 insertions, 137 deletions
diff --git a/coverage/control.py b/coverage/control.py
index 351992f2..fb033610 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -3,39 +3,47 @@
"""Core control stuff for coverage.py."""
+
import atexit
import inspect
+import itertools
import os
import platform
import re
import sys
import traceback
-from coverage import env, files
+from coverage import env
from coverage.annotate import AnnotateReporter
from coverage.backward import string_class, iitems
from coverage.collector import Collector
-from coverage.config import CoverageConfig
+from coverage.config import read_coverage_config
from coverage.data import CoverageData, CoverageDataFiles
-from coverage.debug import DebugControl
+from coverage.debug import DebugControl, write_formatted_info
from coverage.files import TreeMatcher, FnmatchMatcher
from coverage.files import PathAliases, find_python_files, prep_patterns
+from coverage.files import canonical_filename, set_relative_directory
from coverage.files import ModuleMatcher, abs_file
from coverage.html import HtmlReporter
from coverage.misc import CoverageException, bool_or_none, join_regex
from coverage.misc import file_be_gone, isolate_module
-from coverage.multiproc import patch_multiprocessing
from coverage.plugin import FileReporter
from coverage.plugin_support import Plugins
-from coverage.python import PythonFileReporter
+from coverage.python import PythonFileReporter, source_for_file
from coverage.results import Analysis, Numbers
from coverage.summary import SummaryReporter
from coverage.xmlreport import XmlReporter
+try:
+ from coverage.multiproc import patch_multiprocessing
+except ImportError: # pragma: only jython
+ # Jython has no multiprocessing module.
+ patch_multiprocessing = None
+
os = isolate_module(os)
# Pypy has some unusual stuff in the "stdlib". Consider those locations
-# when deciding where the stdlib is. This modules are not used for anything,
+# 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
@@ -101,8 +109,8 @@ class Coverage(object):
file can't be read, it is an error.
* If it is True, then a few standard files names are tried
- (".coveragerc", "setup.cfg"). It is not an error for these files
- to not be found.
+ (".coveragerc", "setup.cfg", "tox.ini"). It is not an error for
+ these files to not be found.
* If it is False, then no configuration file is read.
@@ -130,49 +138,18 @@ class Coverage(object):
The `concurrency` parameter can now be a list of strings.
"""
- # Build our configuration from a number of sources:
- # 1: defaults:
- self.config = CoverageConfig()
-
- # 2: from the rcfile, .coveragerc or setup.cfg file:
- if config_file:
- # pylint: disable=redefined-variable-type
- did_read_rc = False
- # Some API users were specifying ".coveragerc" to mean the same as
- # True, so make it so.
- if config_file == ".coveragerc":
- config_file = True
- specified_file = (config_file is not True)
- if not specified_file:
- config_file = ".coveragerc"
- self.config_file = config_file
-
- did_read_rc = self.config.from_file(config_file)
-
- if not did_read_rc:
- if specified_file:
- raise CoverageException(
- "Couldn't read '%s' as a config file" % config_file
- )
- self.config.from_file("setup.cfg", section_prefix="coverage:")
-
- # 3: from environment variables:
- env_data_file = os.environ.get('COVERAGE_FILE')
- if env_data_file:
- self.config.data_file = env_data_file
- debugs = os.environ.get('COVERAGE_DEBUG')
- if debugs:
- self.config.debug.extend(debugs.split(","))
-
- # 4: from constructor arguments:
- self.config.from_args(
+ # Build our configuration from a number of sources.
+ self.config_file, self.config = read_coverage_config(
+ config_file=config_file,
data_file=data_file, cover_pylib=cover_pylib, timid=timid,
branch=branch, parallel=bool_or_none(data_suffix),
source=source, omit=omit, include=include, debug=debug,
concurrency=concurrency,
)
+ # This is injectable by tests.
self._debug_file = None
+
self._auto_load = self._auto_save = auto_data
self._data_suffix = data_suffix
@@ -191,10 +168,11 @@ class Coverage(object):
# Other instance attributes, set later.
self.omit = self.include = self.source = None
+ self.source_pkgs_unmatched = None
self.source_pkgs = None
self.data = self.data_files = self.collector = None
self.plugins = None
- self.pylib_dirs = self.cover_dirs = None
+ self.pylib_paths = self.cover_paths = None
self.data_suffix = self.run_suffix = None
self._exclude_re = None
self.debug = None
@@ -204,8 +182,6 @@ class Coverage(object):
self._inited = False
# Have we started collecting and not stopped it?
self._started = False
- # Have we measured some data and not harvested it?
- self._measured = False
# If we have sub-process measurement happening automatically, then we
# want any explicit creation of a Coverage object to mean, this process
@@ -226,6 +202,8 @@ class Coverage(object):
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.
@@ -245,22 +223,27 @@ class Coverage(object):
self._exclude_re = {}
self._exclude_regex_stale()
- files.set_relative_directory()
+ set_relative_directory()
# The source argument can be directories or package names.
self.source = []
self.source_pkgs = []
for src in self.config.source or []:
- if os.path.exists(src):
- self.source.append(files.canonical_filename(src))
+ if os.path.isdir(src):
+ self.source.append(canonical_filename(src))
else:
self.source_pkgs.append(src)
+ self.source_pkgs_unmatched = self.source_pkgs[:]
self.omit = prep_patterns(self.config.omit)
self.include = prep_patterns(self.config.include)
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.
@@ -306,10 +289,12 @@ class Coverage(object):
# data file will be written into the directory where the process
# started rather than wherever the process eventually chdir'd to.
self.data = CoverageData(debug=self.debug)
- self.data_files = CoverageDataFiles(basename=self.config.data_file, warn=self._warn)
+ self.data_files = CoverageDataFiles(
+ basename=self.config.data_file, warn=self._warn, debug=self.debug,
+ )
# The directories for files considered "installed with the interpreter".
- self.pylib_dirs = set()
+ self.pylib_paths = set()
if not self.config.cover_pylib:
# Look at where some standard modules are located. That's the
# indication for "installed with the interpreter". In some
@@ -318,7 +303,7 @@ class Coverage(object):
# 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_dirs.add(self._canonical_dir(m))
+ self.pylib_paths.add(self._canonical_path(m, directory=True))
if _structseq and not hasattr(_structseq, '__file__'):
# PyPy 2.4 has no __file__ in the builtin modules, but the code
@@ -329,96 +314,77 @@ class Coverage(object):
structseq_file = structseq_new.func_code.co_filename
except AttributeError:
structseq_file = structseq_new.__code__.co_filename
- self.pylib_dirs.add(self._canonical_dir(structseq_file))
+ self.pylib_paths.add(self._canonical_path(structseq_file))
# To avoid tracing the coverage.py code itself, we skip anything
# located where we are.
- self.cover_dirs = [self._canonical_dir(__file__)]
+ self.cover_paths = [self._canonical_path(__file__, directory=True)]
if env.TESTING:
+ # Don't include our own test code.
+ self.cover_paths.append(os.path.join(self.cover_paths[0], "tests"))
+
# When testing, we use PyContracts, which should be considered
# part of coverage.py, and it uses six. Exclude those directories
# just as we exclude ourselves.
import contracts
import six
for mod in [contracts, six]:
- self.cover_dirs.append(self._canonical_dir(mod))
+ self.cover_paths.append(self._canonical_path(mod))
# Set the reporting precision.
Numbers.set_precision(self.config.precision)
atexit.register(self._atexit)
- self._inited = True
-
# Create the matchers we need for _should_trace
if self.source or self.source_pkgs:
self.source_match = TreeMatcher(self.source)
self.source_pkgs_match = ModuleMatcher(self.source_pkgs)
else:
- if self.cover_dirs:
- self.cover_match = TreeMatcher(self.cover_dirs)
- if self.pylib_dirs:
- self.pylib_match = TreeMatcher(self.pylib_dirs)
+ if self.cover_paths:
+ self.cover_match = TreeMatcher(self.cover_paths)
+ if self.pylib_paths:
+ self.pylib_match = TreeMatcher(self.pylib_paths)
if self.include:
self.include_match = FnmatchMatcher(self.include)
if self.omit:
self.omit_match = FnmatchMatcher(self.omit)
# The user may want to debug things, show info if desired.
+ self._write_startup_debug()
+
+ 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())
- self.debug.write_formatted_info("config", config_info)
+ write_formatted_info(self.debug, "config", config_info)
wrote_any = True
if self.debug.should('sys'):
- self.debug.write_formatted_info("sys", self.sys_info())
+ write_formatted_info(self.debug, "sys", self.sys_info())
for plugin in self.plugins:
header = "sys: " + plugin._coverage_plugin_name
info = plugin.sys_info()
- self.debug.write_formatted_info(header, info)
+ write_formatted_info(self.debug, header, info)
wrote_any = True
if wrote_any:
- self.debug.write_formatted_info("end", ())
+ write_formatted_info(self.debug, "end", ())
- def _canonical_dir(self, morf):
- """Return the canonical directory of the module or file `morf`."""
- morf_filename = PythonFileReporter(morf, self).filename
- return os.path.split(morf_filename)[0]
+ def _canonical_path(self, morf, directory=False):
+ """Return the canonical path of the module or file `morf`.
- def _source_for_file(self, filename):
- """Return the source file for `filename`.
-
- Given a file name being traced, return the best guess as to the source
- file to attribute it to.
+ If the module is a package, then return its directory. If it is a
+ module, then return its file, unless `directory` is True, in which
+ case return its enclosing directory.
"""
- if filename.endswith(".py"):
- # .py files are themselves source files.
- return filename
-
- elif filename.endswith((".pyc", ".pyo")):
- # Bytecode files probably have source files near them.
- py_filename = filename[:-1]
- if os.path.exists(py_filename):
- # Found a .py file, use that.
- return py_filename
- if env.WINDOWS:
- # On Windows, it could be a .pyw file.
- pyw_filename = py_filename + "w"
- if os.path.exists(pyw_filename):
- return pyw_filename
- # Didn't find source, but it's probably the .py file we want.
- return py_filename
-
- elif filename.endswith("$py.class"):
- # Jython is easy to guess.
- return filename[:-9] + ".py"
-
- # No idea, just use the file name as-is.
- return filename
+ morf_path = PythonFileReporter(morf, self).filename
+ if morf_path.endswith("__init__.py") or directory:
+ morf_path = os.path.split(morf_path)[0]
+ return morf_path
def _name_for_module(self, module_globals, filename):
"""Get the name of the module for a set of globals and file name.
@@ -432,6 +398,10 @@ class Coverage(object):
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__':
@@ -480,9 +450,9 @@ class Coverage(object):
# .pyc files can be moved after compilation (for example, by being
# installed), we look for __file__ in the frame and prefer it to the
# co_filename value.
- dunder_file = frame.f_globals.get('__file__')
+ dunder_file = frame.f_globals and frame.f_globals.get('__file__')
if dunder_file:
- filename = self._source_for_file(dunder_file)
+ filename = source_for_file(dunder_file)
if original_filename and not original_filename.startswith('<'):
orig = os.path.basename(original_filename)
if orig != os.path.basename(filename):
@@ -514,7 +484,7 @@ class Coverage(object):
if filename.endswith("$py.class"):
filename = filename[:-9] + ".py"
- canonical = files.canonical_filename(filename)
+ canonical = canonical_filename(filename)
disp.canonical_filename = canonical
# Try the plugins, see if they have an opinion about the file.
@@ -532,7 +502,7 @@ class Coverage(object):
if file_tracer.has_dynamic_source_filename():
disp.has_dynamic_filename = True
else:
- disp.source_filename = files.canonical_filename(
+ disp.source_filename = canonical_filename(
file_tracer.source_filename()
)
break
@@ -579,8 +549,8 @@ class Coverage(object):
# stdlib and coverage.py directories.
if self.source_match:
if self.source_pkgs_match.match(modulename):
- if modulename in self.source_pkgs:
- self.source_pkgs.remove(modulename)
+ if modulename in self.source_pkgs_unmatched:
+ self.source_pkgs_unmatched.remove(modulename)
return None # There's no reason to skip this file.
if not self.source_match.match(filename):
@@ -633,9 +603,18 @@ class Coverage(object):
return not reason
- def _warn(self, msg):
- """Use `msg` as a warning."""
+ def _warn(self, msg, slug=None):
+ """Use `msg` as a warning.
+
+ For warning suppression, use `slug` as the shorthand.
+ """
+ if slug in self.config.disable_warnings:
+ # Don't issue the warning
+ return
+
self._warnings.append(msg)
+ if slug:
+ msg = "%s (%s)" % (msg, slug)
if self.debug.should('pid'):
msg = "[%d] %s" % (os.getpid(), msg)
sys.stderr.write("Coverage.py warning: %s\n" % msg)
@@ -694,7 +673,7 @@ class Coverage(object):
def start(self):
"""Start measuring code coverage.
- Coverage measurement actually occurs in functions called after
+ Coverage measurement only occurs in functions called after
:meth:`start` is invoked. Statements in the same scope as
:meth:`start` won't be measured.
@@ -712,7 +691,6 @@ class Coverage(object):
self.collector.start()
self._started = True
- self._measured = True
def stop(self):
"""Stop measuring code coverage."""
@@ -722,8 +700,8 @@ class Coverage(object):
def _atexit(self):
"""Clean up on process shutdown."""
- if self.debug and self.debug.should('dataio'):
- self.debug.write("Inside _atexit: self._auto_save = %r" % (self._auto_save,))
+ if self.debug.should("process"):
+ self.debug.write("atexit: {0!r}".format(self))
if self._started:
self.stop()
if self._auto_save:
@@ -832,7 +810,7 @@ class Coverage(object):
)
def get_data(self):
- """Get the collected data and reset the collector.
+ """Get the collected data.
Also warn about various problems collecting data.
@@ -842,46 +820,78 @@ class Coverage(object):
"""
self._init()
- if not self._measured:
- return self.data
- self.collector.save_data(self.data)
+ if self.collector.save_data(self.data):
+ self._post_save_work()
+
+ return self.data
+
+ def _post_save_work(self):
+ """After saving data, look for warnings, post-work, etc.
- # If there are still entries in the source_pkgs list, then we never
- # encountered those packages.
+ Warn about things that should have happened but didn't.
+ Look for unexecuted files.
+
+ """
+ # If there are still entries in the source_pkgs_unmatched list,
+ # then we never encountered those packages.
if self._warn_unimported_source:
- for pkg in self.source_pkgs:
+ for pkg in self.source_pkgs_unmatched:
if pkg not in sys.modules:
- self._warn("Module %s was never imported." % pkg)
+ self._warn("Module %s was never imported." % pkg, slug="module-not-imported")
elif not (
hasattr(sys.modules[pkg], '__file__') and
os.path.exists(sys.modules[pkg].__file__)
):
- self._warn("Module %s has no Python source." % pkg)
+ self._warn("Module %s has no Python source." % pkg, slug="module-not-python")
else:
- self._warn("Module %s was previously imported, but not measured." % pkg)
+ self._warn(
+ "Module %s was previously imported, but not measured." % pkg,
+ slug="module-not-measured",
+ )
# Find out if we got any data.
if not self.data and self._warn_no_data:
- self._warn("No data was collected.")
+ self._warn("No data was collected.", slug="no-data-collected")
# Find files that were never executed at all.
- for src in self.source:
- for py_file in find_python_files(src):
- py_file = files.canonical_filename(py_file)
-
- if self.omit_match and self.omit_match.match(py_file):
- # Turns out this file was omitted, so don't pull it back
- # in as unexecuted.
- continue
+ for pkg in self.source_pkgs:
+ if (not pkg in sys.modules or
+ not hasattr(sys.modules[pkg], '__file__') or
+ not os.path.exists(sys.modules[pkg].__file__)):
+ continue
+ pkg_file = source_for_file(sys.modules[pkg].__file__)
+ self._find_unexecuted_files(self._canonical_path(pkg_file))
- self.data.touch_file(py_file)
+ for src in self.source:
+ self._find_unexecuted_files(src)
if self.config.note:
self.data.add_run_info(note=self.config.note)
- self._measured = False
- return self.data
+ def _find_plugin_files(self, src_dir):
+ """Get executable files from the plugins."""
+ for plugin in self.plugins:
+ for x_file in plugin.find_executable_files(src_dir):
+ yield x_file, plugin._coverage_plugin_name
+
+ def _find_unexecuted_files(self, src_dir):
+ """Find unexecuted files in `src_dir`.
+
+ Search for files in `src_dir` that are probably importable,
+ and add them as unexecuted files in `self.data`.
+
+ """
+ py_files = ((py_file, None) for py_file in find_python_files(src_dir))
+ plugin_files = self._find_plugin_files(src_dir)
+
+ for file_path, plugin_name in itertools.chain(py_files, plugin_files):
+ file_path = canonical_filename(file_path)
+ if self.omit_match and self.omit_match.match(file_path):
+ # Turns out this file was omitted, so don't pull it back
+ # in as unexecuted.
+ continue
+ self.data.touch_file(file_path, plugin_name)
# Backward compatibility with version 1.
def analysis(self, morf):
@@ -949,7 +959,6 @@ class Coverage(object):
)
if file_reporter == "python":
- # pylint: disable=redefined-variable-type
file_reporter = PythonFileReporter(morf, self)
return file_reporter
@@ -993,6 +1002,8 @@ class Coverage(object):
included in the report. Files matching `omit` will not be included in
the report.
+ If `skip_covered` is True, don't report on files with 100% coverage.
+
Returns a float, the total percentage covered.
"""
@@ -1026,7 +1037,8 @@ class Coverage(object):
reporter.report(morfs, directory=directory)
def html_report(self, morfs=None, directory=None, ignore_errors=None,
- omit=None, include=None, extra_css=None, title=None):
+ omit=None, include=None, extra_css=None, title=None,
+ skip_covered=None):
"""Generate an HTML report.
The HTML is written to `directory`. The file "index.html" is the
@@ -1048,6 +1060,7 @@ class Coverage(object):
self.config.from_args(
ignore_errors=ignore_errors, omit=omit, 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)
@@ -1120,8 +1133,8 @@ class Coverage(object):
info = [
('version', covmod.__version__),
('coverage', covmod.__file__),
- ('cover_dirs', self.cover_dirs),
- ('pylib_dirs', self.pylib_dirs),
+ ('cover_paths', self.cover_paths),
+ ('pylib_paths', self.pylib_paths),
('tracer', self.collector.tracer_name()),
('plugins.file_tracers', ft_plugins),
('config_files', self.config.attempted_config_files),