summaryrefslogtreecommitdiff
path: root/coverage/control.py
diff options
context:
space:
mode:
Diffstat (limited to 'coverage/control.py')
-rw-r--r--coverage/control.py361
1 files changed, 268 insertions, 93 deletions
diff --git a/coverage/control.py b/coverage/control.py
index 23740ca4..d07abaf3 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -1,14 +1,16 @@
"""Core control stuff for Coverage."""
-import os, socket
+import atexit, os, random, socket, sys
from coverage.annotate import AnnotateReporter
-from coverage.backward import string_class # pylint: disable-msg=W0622
+from coverage.backward import string_class
from coverage.codeunit import code_unit_factory, CodeUnit
from coverage.collector import Collector
+from coverage.config import CoverageConfig
from coverage.data import CoverageData
from coverage.files import FileLocator
from coverage.html import HtmlReporter
+from coverage.misc import bool_or_none
from coverage.results import Analysis
from coverage.summary import SummaryReporter
from coverage.xmlreport import XmlReporter
@@ -17,9 +19,9 @@ class coverage(object):
"""Programmatic access to Coverage.
To use::
-
+
from coverage import coverage
-
+
cov = coverage()
cov.start()
#.. blah blah (run your code) blah blah ..
@@ -28,82 +30,130 @@ class coverage(object):
"""
- def __init__(self, data_file=None, data_suffix=False, cover_pylib=False,
- auto_data=False, timid=False, branch=False):
- """Create a new coverage measurement context.
-
+ def __init__(self, data_file=None, data_suffix=None, cover_pylib=None,
+ auto_data=False, timid=None, branch=None, config_file=True,
+ omit_prefixes=None, include_prefixes=None):
+ """
`data_file` is the base name of the data file to use, defaulting to
- ".coverage". `data_suffix` is appended 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.
-
+ ".coverage". `data_suffix` is appended (with a dot) to `data_file` to
+ create the final file name. If `data_suffix` is simply True, then a
+ suffix is created with the machine and process identity included.
+
`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 measure branch execution.
+
+ If `branch` is true, then branch coverage will be measured in addition
+ to the usual statement coverage.
+
+ `config_file` determines what config file to read. If it is a string,
+ it is the name of the config file to read. If it is True, then a
+ standard file is read (".coveragerc"). If it is False, then no file is
+ read.
+
+ `omit_prefixes` and `include_prefixes` are lists of filename prefixes.
+ Files that match `include_prefixes` will be measured, files that match
+ `omit_prefixes` will not.
"""
from coverage import __version__
-
- self.cover_pylib = cover_pylib
+
+ # Build our configuration from a number of sources:
+ # 1: defaults:
+ self.config = CoverageConfig()
+
+ # 2: from the coveragerc file:
+ if config_file:
+ if config_file is True:
+ config_file = ".coveragerc"
+ self.config.from_file(config_file)
+
+ # 3: from environment variables:
+ self.config.from_environment('COVERAGE_OPTIONS')
+ env_data_file = os.environ.get('COVERAGE_FILE')
+ if env_data_file:
+ self.config.data_file = env_data_file
+
+ # 4: from constructor arguments:
+ self.config.from_args(
+ data_file=data_file, cover_pylib=cover_pylib, timid=timid,
+ branch=branch, parallel=bool_or_none(data_suffix),
+ omit_prefixes=omit_prefixes,
+ include_prefixes=include_prefixes
+ )
+
self.auto_data = auto_data
-
+ self.atexit_registered = False
+
self.exclude_re = ""
- self.exclude_list = []
-
+ self._compile_exclude()
+
self.file_locator = FileLocator()
-
- # Timidity: for nose users, read an environment variable. This is a
- # cheap hack, since the rest of the command line arguments aren't
- # recognized, but it solves some users' problems.
- timid = timid or ('--timid' in os.environ.get('COVERAGE_OPTIONS', ''))
+
+ self.omit_prefixes = self._abs_files(self.config.omit_prefixes)
+ self.include_prefixes = self._abs_files(self.config.include_prefixes)
+
self.collector = Collector(
- self._should_trace, timid=timid, branch=branch
+ self._should_trace, timid=self.config.timid,
+ branch=self.config.branch
)
- # Create the data file.
- if data_suffix:
+ # Suffixes are a bit tricky. We want to use the data suffix only when
+ # collecting data, not when combining data. So we save it as
+ # `self.run_suffix` now, and promote it to `self.data_suffix` if we
+ # find that we are collecting data later.
+ if data_suffix or self.config.parallel:
if not isinstance(data_suffix, string_class):
- # if data_suffix=True, use .machinename.pid
- data_suffix = ".%s.%s" % (socket.gethostname(), os.getpid())
+ # if data_suffix=True, use .machinename.pid.random
+ data_suffix = True
else:
data_suffix = None
+ self.data_suffix = None
+ self.run_suffix = data_suffix
+ # 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=data_file, suffix=data_suffix,
+ basename=self.config.data_file,
collector="coverage v%s" % __version__
)
- # The default exclude pattern.
- self.exclude('# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]')
-
# The prefix for files considered "installed with the interpreter".
- if not self.cover_pylib:
+ if not self.config.cover_pylib:
+ # Look at where the "os" module is located. That's the indication
+ # for "installed with the interpreter".
os_file = self.file_locator.canonical_filename(os.__file__)
self.pylib_prefix = os.path.split(os_file)[0]
+ # To avoid tracing the coverage code itself, we skip anything located
+ # where we are.
here = self.file_locator.canonical_filename(__file__)
self.cover_prefix = os.path.split(here)[0]
def _should_trace(self, filename, frame):
"""Decide whether to trace execution in `filename`
-
+
+ This function is called from the trace function. As each new file name
+ is encountered, this function determines whether it is traced or not.
+
Returns a canonicalized filename if it should be traced, False if it
should not.
-
+
"""
- if filename == '<string>':
- # There's no point in ever tracing string executions, we can't do
- # anything with the data later anyway.
+ if filename[0] == '<':
+ # Lots of non-file execution is represented with artificial
+ # filenames 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 False
# Compiled Python files have two filenames: frame.f_code.co_filename is
@@ -123,7 +173,7 @@ class coverage(object):
# If we aren't supposed to trace installed code, then check if this is
# near the Python standard library and skip it if so.
- if not self.cover_pylib:
+ if not self.config.cover_pylib:
if canonical.startswith(self.pylib_prefix):
return False
@@ -132,6 +182,17 @@ class coverage(object):
if canonical.startswith(self.cover_prefix):
return False
+ # Check the file against the include and omit prefixes.
+ if self.include_prefixes:
+ for prefix in self.include_prefixes:
+ if canonical.startswith(prefix):
+ break
+ else:
+ return False
+ for prefix in self.omit_prefixes:
+ if canonical.startswith(prefix):
+ return False
+
return canonical
# To log what should_trace returns, change this to "if 1:"
@@ -143,11 +204,16 @@ class coverage(object):
print("should_trace: %r -> %r" % (filename, ret))
return ret
+ def _abs_files(self, files):
+ """Return a list of absolute file names for the names in `files`."""
+ files = files or []
+ return [self.file_locator.abs_file(f) for f in files]
+
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.
-
+
"""
self.data.usefile(usecache)
@@ -155,16 +221,21 @@ class coverage(object):
"""Load previously-collected coverage data from the data file."""
self.collector.reset()
self.data.read()
-
+
def start(self):
"""Start measuring code coverage."""
+ if self.run_suffix:
+ # Calling start() means we're running code, so use the run_suffix
+ # as the data_suffix when we eventually save the data.
+ self.data_suffix = self.run_suffix
if self.auto_data:
self.load()
# Save coverage data when Python exits.
- import atexit
- atexit.register(self.save)
+ if not self.atexit_registered:
+ atexit.register(self.save)
+ self.atexit_registered = True
self.collector.start()
-
+
def stop(self):
"""Stop measuring code coverage."""
self.collector.stop()
@@ -172,47 +243,61 @@ class coverage(object):
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.collector.reset()
self.data.erase()
def clear_exclude(self):
"""Clear the exclude list."""
- self.exclude_list = []
+ self.config.exclude_list = []
self.exclude_re = ""
def exclude(self, regex):
"""Exclude source lines from execution consideration.
-
+
`regex` is a regular expression. Lines matching this expression are
not considered executable when reporting code coverage. A list of
regexes is maintained; this function adds a new regex to the list.
Matching any of the regexes excludes a source line.
-
+
"""
- self.exclude_list.append(regex)
- self.exclude_re = "(" + ")|(".join(self.exclude_list) + ")"
+ self.config.exclude_list.append(regex)
+ self._compile_exclude()
+
+ def _compile_exclude(self):
+ """Build the internal usable form of the exclude list."""
+ self.exclude_re = "(" + ")|(".join(self.config.exclude_list) + ")"
def get_exclude_list(self):
"""Return the list of excluded regex patterns."""
- return self.exclude_list
+ return self.config.exclude_list
def save(self):
"""Save the collected coverage data to the data file."""
+ data_suffix = self.data_suffix
+ if data_suffix and not isinstance(data_suffix, string_class):
+ # 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.
+ data_suffix = "%s.%s.%06d" % (
+ socket.gethostname(), os.getpid(), random.randint(0, 99999)
+ )
+
self._harvest_data()
- self.data.write()
+ self.data.write(suffix=data_suffix)
def combine(self):
"""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.
-
+
"""
self.data.combine_parallel_data()
@@ -230,14 +315,15 @@ class coverage(object):
def analysis2(self, morf):
"""Analyze a module.
-
+
`morf` is a module or a filename. It will be analyzed to determine
its coverage statistics. The return value is a 5-tuple:
-
+
* The filename 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 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
@@ -252,66 +338,126 @@ class coverage(object):
def _analyze(self, it):
"""Analyze a single morf or code unit.
-
+
Returns an `Analysis` object.
"""
if not isinstance(it, CodeUnit):
it = code_unit_factory(it, self.file_locator)[0]
-
+
return Analysis(self, it)
- def report(self, morfs=None, show_missing=True, ignore_errors=False,
- file=None, omit_prefixes=None): # pylint: disable-msg=W0622
+ def report(self, morfs=None, show_missing=True, ignore_errors=None,
+ file=None, # pylint: disable-msg=W0622
+ omit_prefixes=None, include_prefixes=None
+ ):
"""Write a summary report to `file`.
-
+
Each module in `morfs` is listed, with counts of statements, executed
statements, missing statements, and a list of lines missed.
-
+
+ `include_prefixes` is a list of filename prefixes. Modules that match
+ those prefixes will be included in the report. Modules that match
+ `omit_prefixes` will not be included in the report.
+
"""
- reporter = SummaryReporter(self, show_missing, ignore_errors)
- reporter.report(morfs, outfile=file, omit_prefixes=omit_prefixes)
+ self.config.from_args(
+ ignore_errors=ignore_errors,
+ omit_prefixes=omit_prefixes,
+ include_prefixes=include_prefixes
+ )
+ reporter = SummaryReporter(
+ self, show_missing, self.config.ignore_errors
+ )
+ reporter.report(
+ morfs, outfile=file, omit_prefixes=self.config.omit_prefixes,
+ include_prefixes=self.config.include_prefixes
+ )
- def annotate(self, morfs=None, directory=None, ignore_errors=False,
- omit_prefixes=None):
+ def annotate(self, morfs=None, directory=None, ignore_errors=None,
+ omit_prefixes=None, include_prefixes=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 `coverage.report()` for other arguments.
+
"""
- reporter = AnnotateReporter(self, ignore_errors)
+ self.config.from_args(
+ ignore_errors=ignore_errors,
+ omit_prefixes=omit_prefixes,
+ include_prefixes=include_prefixes
+ )
+ reporter = AnnotateReporter(self, self.config.ignore_errors)
reporter.report(
- morfs, directory=directory, omit_prefixes=omit_prefixes)
+ morfs, directory=directory,
+ omit_prefixes=self.config.omit_prefixes,
+ include_prefixes=self.config.include_prefixes
+ )
- def html_report(self, morfs=None, directory=None, ignore_errors=False,
- omit_prefixes=None):
+ def html_report(self, morfs=None, directory=None, ignore_errors=None,
+ omit_prefixes=None, include_prefixes=None):
"""Generate an HTML report.
-
+
+ See `coverage.report()` for other arguments.
+
"""
- reporter = HtmlReporter(self, ignore_errors)
+ self.config.from_args(
+ ignore_errors=ignore_errors,
+ omit_prefixes=omit_prefixes,
+ include_prefixes=include_prefixes,
+ html_dir=directory,
+ )
+ reporter = HtmlReporter(self, self.config.ignore_errors)
reporter.report(
- morfs, directory=directory, omit_prefixes=omit_prefixes)
+ morfs, directory=self.config.html_dir,
+ omit_prefixes=self.config.omit_prefixes,
+ include_prefixes=self.config.include_prefixes
+ )
- def xml_report(self, morfs=None, outfile=None, ignore_errors=False,
- omit_prefixes=None):
+ def xml_report(self, morfs=None, outfile=None, ignore_errors=None,
+ omit_prefixes=None, include_prefixes=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 `coverage.report()` for other arguments.
+
"""
- if outfile:
- outfile = open(outfile, "w")
- reporter = XmlReporter(self, ignore_errors)
- reporter.report(morfs, omit_prefixes=omit_prefixes, outfile=outfile)
+ self.config.from_args(
+ ignore_errors=ignore_errors,
+ omit_prefixes=omit_prefixes,
+ include_prefixes=include_prefixes,
+ xml_output=outfile,
+ )
+ file_to_close = None
+ if self.config.xml_output:
+ if self.config.xml_output == '-':
+ outfile = sys.stdout
+ else:
+ outfile = open(self.config.xml_output, "w")
+ file_to_close = outfile
+ try:
+ reporter = XmlReporter(self, self.config.ignore_errors)
+ reporter.report(
+ morfs, omit_prefixes=self.config.omit_prefixes,
+ include_prefixes=self.config.include_prefixes, outfile=outfile
+ )
+ finally:
+ if file_to_close:
+ file_to_close.close()
def sysinfo(self):
- """Return a list of key,value pairs showing internal information."""
-
+ """Return a list of (key, value) pairs showing internal information."""
+
import coverage as covmod
- import platform, re, sys
+ import platform, re
info = [
('version', covmod.__version__),
@@ -319,7 +465,7 @@ class coverage(object):
('cover_prefix', self.cover_prefix),
('pylib_prefix', self.pylib_prefix),
('tracer', self.collector.tracer_name()),
- ('data_file', self.data.filename),
+ ('data_path', self.data.filename),
('python', sys.version.replace('\n', '')),
('platform', platform.platform()),
('cwd', os.getcwd()),
@@ -330,3 +476,32 @@ class coverage(object):
]),
]
return info
+
+
+def process_startup():
+ """Call this at Python startup 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()
+
+ """
+ cps = os.environ.get("COVERAGE_PROCESS_START")
+ if cps:
+ cov = coverage(config_file=cps, auto_data=True)
+ if os.environ.get("COVERAGE_COVERAGE"):
+ # Measuring coverage within coverage.py takes yet more trickery.
+ cov.cover_prefix = "Please measure coverage.py!"
+ cov.start()