summaryrefslogtreecommitdiff
path: root/coverage/control.py
diff options
context:
space:
mode:
Diffstat (limited to 'coverage/control.py')
-rw-r--r--coverage/control.py460
1 files changed, 226 insertions, 234 deletions
diff --git a/coverage/control.py b/coverage/control.py
index 2c8d384e..0a5ccae6 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -1,34 +1,38 @@
-"""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 re
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, isolate_module
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
from coverage.xmlreport import XmlReporter
+os = isolate_module(os)
# Pypy has some unusual stuff in the "stdlib". Consider those locations
# when deciding where the stdlib is.
@@ -97,7 +101,7 @@ class Coverage(object):
in the trees indicated by the file paths or package names will be
measured.
- `include` and `omit` are lists of filename patterns. Files that match
+ `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.
@@ -109,6 +113,9 @@ class Coverage(object):
results. Valid strings are "greenlet", "eventlet", "gevent", or
"thread" (the default).
+ .. versionadded:: 4.0
+ The `concurrency` parameter.
+
"""
# Build our configuration from a number of sources:
# 1: defaults:
@@ -138,6 +145,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 +176,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
@@ -186,41 +196,40 @@ class Coverage(object):
"""Set all the initial state.
This is called by the public methods to initialize state. This lets us
- construct a Coverage object, then tweak its state before this function
- is called.
+ construct a :class:`Coverage` object, then tweak its state before this
+ function is called.
"""
- from coverage import __version__
-
if self._inited:
return
- # Create and configure the debugging controller.
+ # 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:
- self._debug_file = sys.stderr
+ 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)
# 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 +251,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,13 +280,10 @@ 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, warn=self._warn)
- # The dirs for files considered "installed with the interpreter".
+ # The directories for files considered "installed with the interpreter".
self.pylib_dirs = set()
if not self.config.cover_pylib:
# Look at where some standard modules are located. That's the
@@ -285,12 +291,12 @@ 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, re, _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__'):
# PyPy 2.4 has no __file__ in the builtin modules, but the code
- # objects still have the filenames. So dig into one to find
+ # objects still have the file names. So dig into one to find
# the path to exclude.
structseq_new = _structseq.structseq_new
try:
@@ -299,9 +305,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 +328,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:
@@ -350,7 +363,7 @@ class Coverage(object):
def _source_for_file(self, filename):
"""Return the source file for `filename`.
- Given a filename being traced, return the best guess as to the source
+ Given a file name being traced, return the best guess as to the source
file to attribute it to.
"""
@@ -376,18 +389,18 @@ class Coverage(object):
# Jython is easy to guess.
return filename[:-9] + ".py"
- # No idea, just use the filename as-is.
+ # No idea, just use the file name as-is.
return filename
def _name_for_module(self, module_globals, filename):
- """Get the name of the module for a set of globals and 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
- filename to get the module's name. In the case that the module name
+ file name to get the module's name. In the case that the module name
can't be determined, None is returned.
"""
@@ -424,7 +437,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."""
@@ -432,8 +446,8 @@ class Coverage(object):
disp.reason = reason
return disp
- # Compiled Python files have two filenames: frame.f_code.co_filename is
- # the filename at the time the .pyc was compiled. The second name is
+ # 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
@@ -441,31 +455,43 @@ 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 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 filename")
+ 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
- # filenames like "<string>", "<doctest readme.txt[0]>", or
+ # 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 filename")
+ 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 = 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 +504,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(
@@ -512,7 +537,7 @@ class Coverage(object):
return disp
def _check_include_omit_etc_internal(self, filename, frame):
- """Check a filename against the include, omit, etc, rules.
+ """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.
@@ -541,8 +566,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,11 +586,11 @@ 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):
- """Check a filename against the include/omit/etc, rules, verbosely.
+ """Check a file name against the include/omit/etc, rules, verbosely.
Returns a boolean: True if the file should be traced, False if not.
@@ -583,33 +608,70 @@ class Coverage(object):
def _warn(self, msg):
"""Use `msg` as a warning."""
self._warnings.append(msg)
- 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)
- def use_cache(self, usecache):
- """Control the use of a data file (incorrectly called a cache).
+ 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"`.
- `usecache` is true or false, whether to read and write data on disk.
+ Returns the value of the option.
+
+ .. 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 a Python
+ value where appropriate. 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
+
+ .. versionadded:: 4.0
+
+ """
+ self.config.set_option(option_name, value)
+
+ def use_cache(self, usecache):
+ """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.
- Coverage measurement actually occurs in functions called after `start`
- is invoked. Statements in the same scope as `start` won't be measured.
+ Coverage measurement actually occurs in functions called after
+ :meth:`start` is invoked. Statements in the same scope as
+ :meth:`start` won't be measured.
- Once you invoke `start`, you must also call `stop` eventually, or your
- process might not shut down cleanly.
+ Once you invoke :meth:`start`, you must also call :meth:`stop`
+ eventually, or your process might not shut down cleanly.
"""
self._init()
@@ -647,6 +709,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."""
@@ -688,8 +751,8 @@ class Coverage(object):
def get_exclude_list(self, which='exclude'):
"""Return a list of excluded regex patterns.
- `which` indicates which list is desired. See `exclude` for the lists
- that are available, and their meaning.
+ `which` indicates which list is desired. See :meth:`exclude` for the
+ lists that are available, and their meaning.
"""
self._init()
@@ -698,62 +761,53 @@ 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, data_dirs=None):
+ 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_dirs` is a list of directories from which data files should be
- combined. If no list is passed, then the data files from the current
- directory will be combined.
+ `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.
+
+ .. versionadded:: 4.0
+ The `data_paths` parameter.
"""
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, data_dirs=data_dirs)
- 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:`coverage.CoverageData`, the collected coverage data.
+
+ .. versionadded:: 4.0
+
"""
self._init()
if not self._measured:
- return
+ return self.data
- # 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.
@@ -767,20 +821,16 @@ class Coverage(object):
):
self._warn("Module %s has no Python source." % pkg)
else:
- self._warn(
- "Module %s was previously imported, "
- "but not measured." % pkg
- )
+ self._warn("Module %s was previously imported, but not measured." % pkg)
# 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
@@ -789,7 +839,11 @@ class Coverage(object):
self.data.touch_file(py_file)
+ 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):
@@ -800,10 +854,10 @@ class Coverage(object):
def analysis2(self, morf):
"""Analyze a module.
- `morf` is a module or a filename. It will be analyzed to determine
+ `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 filename for the module.
+ * 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
@@ -830,19 +884,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."""
+ """Get a FileReporter for a module or file name."""
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)
@@ -854,25 +909,19 @@ 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
def _get_file_reporters(self, morfs=None):
- """Get a list of FileReporters for a list of modules or filenames.
+ """Get a list of FileReporters for a list of modules or file names.
- For each module or filename in `morfs`, find a FileReporter. Return
+ For each module or file name in `morfs`, find a FileReporter. Return
the list of FileReporters.
- If `morfs` is a single module or filename, this returns a list of one
+ 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.
@@ -901,14 +950,14 @@ class Coverage(object):
Each module in `morfs` is listed, with counts of statements, executed
statements, missing statements, and a list of lines missed.
- `include` is a list of filename patterns. Files that match will be
+ `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.
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,
@@ -927,10 +976,10 @@ class Coverage(object):
marker to indicate the coverage of the line. Covered lines have ">",
excluded lines have "-", and missing lines have "!".
- See `coverage.report()` for other arguments.
+ See :meth:`report` for other arguments.
"""
- self._harvest_data()
+ self.get_data()
self.config.from_args(
ignore_errors=ignore_errors, omit=omit, include=include
)
@@ -951,12 +1000,12 @@ class Coverage(object):
`title` is a text string (not HTML) to use as the title of the HTML
report.
- See `coverage.report()` for other arguments.
+ See :meth:`report` for other arguments.
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,
@@ -975,12 +1024,12 @@ class Coverage(object):
Each module in `morfs` is included in the report. `outfile` is the
path to write the file to, "-" will write to stdout.
- See `coverage.report()` for other arguments.
+ See :meth:`report` for other arguments.
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,
@@ -998,10 +1047,13 @@ class Coverage(object):
output_dir = os.path.dirname(self.config.xml_output)
if output_dir and not os.path.isdir(output_dir):
os.makedirs(output_dir)
- outfile = open(self.config.xml_output, "w")
+ 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, self.file_locator)
+ reporter = XmlReporter(self, self.config)
return reporter.report(morfs, outfile=outfile)
except CoverageException:
delete_file = True
@@ -1018,13 +1070,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)"
@@ -1033,16 +1081,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),
@@ -1071,36 +1119,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():
@@ -1132,7 +1176,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
@@ -1140,7 +1184,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
@@ -1148,55 +1192,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]