summaryrefslogtreecommitdiff
path: root/coverage/control.py
diff options
context:
space:
mode:
Diffstat (limited to 'coverage/control.py')
-rw-r--r--coverage/control.py328
1 files changed, 136 insertions, 192 deletions
diff --git a/coverage/control.py b/coverage/control.py
index 563925ef..4837356d 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -1,29 +1,31 @@
-"""Core control stuff for Coverage."""
+# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
+
+"""Core control stuff for coverage.py."""
import atexit
import inspect
import os
import platform
-import random
-import socket
import sys
import traceback
-from coverage import env
+from coverage import env, files
from coverage.annotate import AnnotateReporter
from coverage.backward import string_class, iitems
from coverage.collector import Collector
from coverage.config import CoverageConfig
-from coverage.data import CoverageData
+from coverage.data import CoverageData, CoverageDataFiles
from coverage.debug import DebugControl
-from coverage.files import FileLocator, TreeMatcher, FnmatchMatcher
+from coverage.files import TreeMatcher, FnmatchMatcher
from coverage.files import PathAliases, find_python_files, prep_patterns
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, overrides
+from coverage.misc import file_be_gone
from coverage.monkey import patch_multiprocessing
-from coverage.plugin import CoveragePlugin, FileReporter
+from coverage.plugin import FileReporter
+from coverage.plugin_support import Plugins
from coverage.python import PythonFileReporter
from coverage.results import Analysis, Numbers
from coverage.summary import SummaryReporter
@@ -138,6 +140,9 @@ class Coverage(object):
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(
@@ -166,10 +171,10 @@ class Coverage(object):
# Other instance attributes, set later.
self.omit = self.include = self.source = None
- self.source_pkgs = self.file_locator = None
- self.data = self.collector = None
- self.plugins = self.file_tracing_plugins = None
- self.pylib_dirs = self.cover_dir = None
+ self.source_pkgs = None
+ self.data = self.data_files = self.collector = None
+ self.plugins = None
+ self.pylib_dirs = self.cover_dirs = None
self.data_suffix = self.run_suffix = None
self._exclude_re = None
self.debug = None
@@ -190,8 +195,6 @@ class Coverage(object):
is called.
"""
- from coverage import __version__
-
if self._inited:
return
@@ -201,26 +204,21 @@ class Coverage(object):
self.debug = DebugControl(self.config.debug, self._debug_file)
# Load plugins
- self.plugins = Plugins.load_plugins(self.config.plugins, self.config)
-
- self.file_tracing_plugins = []
- for plugin in self.plugins:
- if overrides(plugin, "file_tracer", CoveragePlugin):
- self.file_tracing_plugins.append(plugin)
+ self.plugins = Plugins.load_plugins(self.config.plugins, self.config, self.debug)
# _exclude_re is a dict that maps exclusion list names to compiled
# regexes.
self._exclude_re = {}
self._exclude_regex_stale()
- self.file_locator = FileLocator()
+ files.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(self.file_locator.canonical_filename(src))
+ self.source.append(files.canonical_filename(src))
else:
self.source_pkgs.append(src)
@@ -242,17 +240,17 @@ class Coverage(object):
)
# Early warning if we aren't going to be able to support plugins.
- if self.file_tracing_plugins and not self.collector.supports_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.file_tracing_plugins
+ for plugin in self.plugins.file_tracers
),
self.collector.tracer_name(),
)
)
- for plugin in self.file_tracing_plugins:
+ 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
@@ -271,11 +269,8 @@ class Coverage(object):
# 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(
- basename=self.config.data_file,
- collector="coverage v%s" % __version__,
- debug=self.debug,
- )
+ self.data = CoverageData(debug=self.debug)
+ self.data_files = CoverageDataFiles(basename=self.config.data_file)
# The dirs for files considered "installed with the interpreter".
self.pylib_dirs = set()
@@ -285,7 +280,7 @@ class Coverage(object):
# 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, os, platform, random, socket, _structseq):
+ for m in (atexit, inspect, os, platform, _structseq, traceback):
if m is not None and hasattr(m, "__file__"):
self.pylib_dirs.add(self._canonical_dir(m))
if _structseq and not hasattr(_structseq, '__file__'):
@@ -299,9 +294,16 @@ class Coverage(object):
structseq_file = structseq_new.__code__.co_filename
self.pylib_dirs.add(self._canonical_dir(structseq_file))
- # To avoid tracing the coverage code itself, we skip anything located
- # where we are.
- self.cover_dir = self._canonical_dir(__file__)
+ # To avoid tracing the coverage.py code itself, we skip anything
+ # located where we are.
+ self.cover_dirs = [self._canonical_dir(__file__)]
+ if env.TESTING:
+ # When testing, we use PyContracts, which should be considered
+ # part of coverage.py, and it uses six. Exclude those directories
+ # just as we exclude ourselves.
+ import contracts, six
+ for mod in [contracts, six]:
+ self.cover_dirs.append(self._canonical_dir(mod))
# Set the reporting precision.
Numbers.set_precision(self.config.precision)
@@ -315,8 +317,8 @@ class Coverage(object):
self.source_match = TreeMatcher(self.source)
self.source_pkgs_match = ModuleMatcher(self.source_pkgs)
else:
- if self.cover_dir:
- self.cover_match = TreeMatcher([self.cover_dir])
+ if self.cover_dirs:
+ self.cover_match = TreeMatcher(self.cover_dirs)
if self.pylib_dirs:
self.pylib_match = TreeMatcher(self.pylib_dirs)
if self.include:
@@ -424,7 +426,8 @@ class Coverage(object):
Returns a FileDisposition object.
"""
- disp = FileDisposition(filename)
+ 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."""
@@ -441,6 +444,13 @@ class Coverage(object):
dunder_file = frame.f_globals.get('__file__')
if dunder_file:
filename = self._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 filename, then just use the original.
+ filename = original_filename
if not filename:
# Empty string is pretty useless.
@@ -460,12 +470,12 @@ class Coverage(object):
if filename.endswith("$py.class"):
filename = filename[:-9] + ".py"
- canonical = self.file_locator.canonical_filename(filename)
+ canonical = files.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.file_tracing_plugins:
+ for plugin in self.plugins.file_tracers:
if not plugin._coverage_enabled:
continue
@@ -478,10 +488,9 @@ class Coverage(object):
if file_tracer.has_dynamic_source_filename():
disp.has_dynamic_filename = True
else:
- disp.source_filename = \
- self.file_locator.canonical_filename(
- file_tracer.source_filename()
- )
+ disp.source_filename = files.canonical_filename(
+ file_tracer.source_filename()
+ )
break
except Exception:
self._warn(
@@ -541,8 +550,8 @@ class Coverage(object):
if self.pylib_match and self.pylib_match.match(filename):
return "is in the stdlib"
- # We exclude the coverage code itself, since a little of it will be
- # measured otherwise.
+ # 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"
@@ -561,7 +570,7 @@ class Coverage(object):
"""
disp = self._should_trace_internal(filename, frame)
if self.debug.should('trace'):
- self.debug.write(disp.debug_message())
+ self.debug.write(_disposition_debug_msg(disp))
return disp
def _check_include_omit_etc(self, filename, frame):
@@ -588,19 +597,16 @@ class Coverage(object):
sys.stderr.write("Coverage.py warning: %s\n" % msg)
def use_cache(self, usecache):
- """Control the use of a data file (incorrectly called a cache).
-
- `usecache` is true or false, whether to read and write data on disk.
-
- """
+ """Obsolete method."""
self._init()
- self.data.usefile(usecache)
+ 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.read()
+ self.data_files.read(self.data)
def start(self):
"""Start measuring code coverage.
@@ -647,6 +653,7 @@ class Coverage(object):
self._init()
self.collector.reset()
self.data.erase()
+ self.data_files.erase(parallel=self.config.parallel)
def clear_exclude(self, which='exclude'):
"""Clear the exclude list."""
@@ -698,58 +705,48 @@ class Coverage(object):
def save(self):
"""Save the collected coverage data to the data file."""
self._init()
- data_suffix = self.data_suffix
- if data_suffix is True:
- # If data_suffix was a simple true value, then make a suffix with
- # plenty of distinguishing information. We do this here in
- # `save()` at the last minute so that the pid will be correct even
- # if the process forks.
- extra = ""
- if _TEST_NAME_FILE: # pragma: debugging
- with open(_TEST_NAME_FILE) as f:
- test_name = f.read()
- extra = "." + test_name
- data_suffix = "%s%s.%s.%06d" % (
- socket.gethostname(), extra, os.getpid(),
- random.randint(0, 999999)
- )
+ self.get_data()
+ self.data_files.write(self.data, suffix=self.data_suffix)
- self._harvest_data()
- self.data.write(suffix=data_suffix)
-
- def combine(self):
+ def combine(self, data_paths=None):
"""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.
+
"""
self._init()
+ self.get_data()
+
aliases = None
if self.config.paths:
- aliases = PathAliases(self.file_locator)
+ aliases = PathAliases()
for paths in self.config.paths.values():
result = paths[0]
for pattern in paths[1:]:
aliases.add(pattern, result)
- self.data.combine_parallel_data(aliases=aliases)
- def _harvest_data(self):
+ self.data_files.combine_parallel_data(self.data, aliases=aliases, data_paths=data_paths)
+
+ def get_data(self):
"""Get the collected data and reset the collector.
Also warn about various problems collecting data.
+ Returns a :class:`CoverageData`, the collected coverage data.
+
"""
self._init()
if not self._measured:
return
- # TODO: seems like this parallel structure is getting kinda old...
- self.data.add_line_data(self.collector.get_line_data())
- self.data.add_arc_data(self.collector.get_arc_data())
- self.data.add_plugin_data(self.collector.get_plugin_data())
- self.collector.reset()
+ self.collector.save_data(self.data)
# If there are still entries in the source_pkgs list, then we never
# encountered those packages.
@@ -769,14 +766,13 @@ class Coverage(object):
)
# Find out if we got any data.
- summary = self.data.summary()
- if not summary and self._warn_no_data:
+ if not self.data and self._warn_no_data:
self._warn("No data was collected.")
# Find files that were never executed at all.
for src in self.source:
for py_file in find_python_files(src):
- py_file = self.file_locator.canonical_filename(py_file)
+ 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
@@ -785,7 +781,20 @@ class Coverage(object):
self.data.touch_file(py_file)
+ # Add run information.
+ self.data.add_run_info(
+ brief_sys=" ".join([
+ platform.python_implementation(),
+ platform.python_version(),
+ platform.system(),
+ ])
+ )
+
+ if self.config.note:
+ self.data.add_run_info(note=self.config.note)
+
self._measured = False
+ return self.data
# Backward compatibility with version 1.
def analysis(self, morf):
@@ -826,19 +835,20 @@ class Coverage(object):
Returns an `Analysis` object.
"""
- self._harvest_data()
+ self.get_data()
if not isinstance(it, FileReporter):
it = self._get_file_reporter(it)
- return Analysis(self, it)
+ return Analysis(self.data, it)
def _get_file_reporter(self, morf):
"""Get a FileReporter for a module or filename."""
plugin = None
+ file_reporter = "python"
if isinstance(morf, string_class):
abs_morf = abs_file(morf)
- plugin_name = self.data.plugin_data().get(abs_morf)
+ plugin_name = self.data.file_tracer(abs_morf)
if plugin_name:
plugin = self.plugins.get(plugin_name)
@@ -850,15 +860,9 @@ class Coverage(object):
plugin._coverage_plugin_name, morf
)
)
- else:
- file_reporter = PythonFileReporter(morf, self)
- # The FileReporter can have a name attribute, but if it doesn't, we'll
- # supply it as the relative path to self.filename.
- if not hasattr(file_reporter, "name"):
- file_reporter.name = self.file_locator.relative_filename(
- file_reporter.filename
- )
+ if file_reporter == "python":
+ file_reporter = PythonFileReporter(morf, self)
return file_reporter
@@ -904,7 +908,7 @@ class Coverage(object):
Returns a float, the total percentage covered.
"""
- self._harvest_data()
+ self.get_data()
self.config.from_args(
ignore_errors=ignore_errors, omit=omit, include=include,
show_missing=show_missing, skip_covered=skip_covered,
@@ -926,7 +930,7 @@ class Coverage(object):
See `coverage.report()` for other arguments.
"""
- self._harvest_data()
+ self.get_data()
self.config.from_args(
ignore_errors=ignore_errors, omit=omit, include=include
)
@@ -952,7 +956,7 @@ class Coverage(object):
Returns a float, the total percentage covered.
"""
- self._harvest_data()
+ self.get_data()
self.config.from_args(
ignore_errors=ignore_errors, omit=omit, include=include,
html_dir=directory, extra_css=extra_css, html_title=title,
@@ -976,7 +980,7 @@ class Coverage(object):
Returns a float, the total percentage covered.
"""
- self._harvest_data()
+ self.get_data()
self.config.from_args(
ignore_errors=ignore_errors, omit=omit, include=include,
xml_output=outfile,
@@ -997,7 +1001,7 @@ class Coverage(object):
outfile = open(self.config.xml_output, "w")
file_to_close = outfile
try:
- reporter = XmlReporter(self, self.config, self.file_locator)
+ reporter = XmlReporter(self, self.config)
return reporter.report(morfs, outfile=outfile)
except CoverageException:
delete_file = True
@@ -1014,13 +1018,9 @@ class Coverage(object):
import coverage as covmod
self._init()
- try:
- implementation = platform.python_implementation()
- except AttributeError:
- implementation = "unknown"
ft_plugins = []
- for ft in self.file_tracing_plugins:
+ for ft in self.plugins.file_tracers:
ft_name = ft._coverage_plugin_name
if not ft._coverage_enabled:
ft_name += " (disabled)"
@@ -1029,16 +1029,16 @@ class Coverage(object):
info = [
('version', covmod.__version__),
('coverage', covmod.__file__),
- ('cover_dir', self.cover_dir),
+ ('cover_dirs', self.cover_dirs),
('pylib_dirs', self.pylib_dirs),
('tracer', self.collector.tracer_name()),
- ('file_tracing_plugins', ft_plugins),
+ ('plugins.file_tracers', ft_plugins),
('config_files', self.config.attempted_config_files),
('configs_read', self.config.config_files),
- ('data_path', self.data.filename),
+ ('data_path', self.data_files.filename),
('python', sys.version.replace('\n', '')),
('platform', platform.platform()),
- ('implementation', implementation),
+ ('implementation', platform.python_implementation()),
('executable', sys.executable),
('cwd', os.getcwd()),
('path', sys.path),
@@ -1067,36 +1067,32 @@ class Coverage(object):
return info
-class FileDisposition(object):
- """A simple object for noting a number of details of files to trace."""
- def __init__(self, original_filename):
- self.original_filename = original_filename
- self.canonical_filename = original_filename
- self.source_filename = None
- self.trace = False
- self.reason = ""
- self.file_tracer = None
- self.has_dynamic_filename = False
-
- def __repr__(self):
- ret = "FileDisposition %r" % (self.original_filename,)
- if self.trace:
- ret += " trace"
- else:
- ret += " notrace=%r" % (self.reason,)
- if self.file_tracer:
- ret += " file_tracer=%r" % (self.file_tracer,)
- return "<" + ret + ">"
-
- def debug_message(self):
- """Produce a debugging message explaining the outcome."""
- if self.trace:
- msg = "Tracing %r" % (self.original_filename,)
- if self.file_tracer:
- msg += ": will be traced by %r" % self.file_tracer
- else:
- msg = "Not tracing %r: %s" % (self.original_filename, self.reason)
- return msg
+# 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.
+
+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
def process_startup():
@@ -1128,7 +1124,7 @@ def process_startup():
# 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 has
+ # flag (an attribute on this function) to indicate that coverage.py has
# already been started, so we can avoid doing it twice.
#
# https://bitbucket.org/ned/coveragepy/issue/340/keyerror-subpy has more
@@ -1136,7 +1132,7 @@ def process_startup():
if hasattr(process_startup, "done"):
# We've annotated this function before, so we must have already
- # started coverage in this process. Nothing to do.
+ # started coverage.py in this process. Nothing to do.
return
process_startup.done = True
@@ -1144,55 +1140,3 @@ def process_startup():
cov.start()
cov._warn_no_data = False
cov._warn_unimported_source = False
-
-
-# A hack for debugging testing in sub-processes.
-_TEST_NAME_FILE = "" # "/tmp/covtest.txt"
-
-
-class Plugins(object):
- """The currently loaded collection of coverage.py plugins."""
-
- def __init__(self):
- self.order = []
- self.names = {}
-
- @classmethod
- def load_plugins(cls, modules, config):
- """Load plugins from `modules`.
-
- Returns a list of loaded and configured plugins.
-
- """
- plugins = cls()
-
- for module in modules:
- __import__(module)
- mod = sys.modules[module]
-
- plugin_class = getattr(mod, "Plugin", None)
- if plugin_class:
- options = config.get_plugin_options(module)
- plugin = plugin_class(options)
- plugin._coverage_plugin_name = module
- plugin._coverage_enabled = True
- plugins.order.append(plugin)
- plugins.names[module] = plugin
- else:
- raise CoverageException(
- "Plugin module %r didn't define a Plugin class" % module
- )
-
- return plugins
-
- def __nonzero__(self):
- return bool(self.order)
-
- __bool__ = __nonzero__
-
- def __iter__(self):
- return iter(self.order)
-
- def get(self, plugin_name):
- """Return a plugin by name."""
- return self.names[plugin_name]