summaryrefslogtreecommitdiff
path: root/coverage
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2014-10-20 18:37:46 -0400
committerNed Batchelder <ned@nedbatchelder.com>2014-10-20 18:37:46 -0400
commit17c94a9f94916ba892f7ef0518881776d6b55d66 (patch)
tree51a7eda6cf8d9e61adcb3ca791f9917065125085 /coverage
parentad4c7f3a5194f6966454d534f02e6b02633fa370 (diff)
parentcd015c45c278aca757263746ed2e64c46d578ddd (diff)
downloadpython-coveragepy-git-17c94a9f94916ba892f7ef0518881776d6b55d66.tar.gz
Merged pull request #18 manually
Diffstat (limited to 'coverage')
-rw-r--r--coverage/__init__.py16
-rw-r--r--coverage/backward.py2
-rw-r--r--coverage/cmdline.py122
-rw-r--r--coverage/codeunit.py26
-rw-r--r--coverage/collector.py71
-rw-r--r--coverage/config.py49
-rw-r--r--coverage/control.py167
-rw-r--r--coverage/misc.py2
-rw-r--r--coverage/parser.py4
-rw-r--r--coverage/plugin.py40
-rw-r--r--coverage/pytracer.py24
-rw-r--r--coverage/results.py3
-rw-r--r--coverage/test_helpers.py40
-rw-r--r--coverage/tracer.c32
-rw-r--r--coverage/version.py2
15 files changed, 322 insertions, 278 deletions
diff --git a/coverage/__init__.py b/coverage/__init__.py
index 5ae32aba..67dd6e88 100644
--- a/coverage/__init__.py
+++ b/coverage/__init__.py
@@ -17,9 +17,9 @@ from coverage.plugin import CoveragePlugin
coverage = Coverage
# Module-level functions. The original API to this module was based on
-# functions defined directly in the module, with a singleton of the coverage()
+# functions defined directly in the module, with a singleton of the Coverage()
# class. That design hampered programmability, so the current api uses
-# explicitly-created coverage objects. But for backward compatibility, here we
+# explicitly-created Coverage objects. But for backward compatibility, here we
# define the top-level functions to create the singleton when they are first
# called.
@@ -28,7 +28,7 @@ coverage = Coverage
_the_coverage = None
def _singleton_method(name):
- """Return a function to the `name` method on a singleton `coverage` object.
+ """Return a function to the `name` method on a singleton `Coverage` object.
The singleton object is created the first time one of these functions is
called.
@@ -42,19 +42,19 @@ def _singleton_method(name):
"""Singleton wrapper around a coverage method."""
global _the_coverage
if not _the_coverage:
- _the_coverage = coverage(auto_data=True)
+ _the_coverage = Coverage(auto_data=True)
return getattr(_the_coverage, name)(*args, **kwargs)
import inspect
- meth = getattr(coverage, name)
+ meth = getattr(Coverage, name)
args, varargs, kw, defaults = inspect.getargspec(meth)
argspec = inspect.formatargspec(args[1:], varargs, kw, defaults)
docstring = meth.__doc__
wrapper.__doc__ = ("""\
- A first-use-singleton wrapper around coverage.%(name)s.
+ A first-use-singleton wrapper around Coverage.%(name)s.
This wrapper is provided for backward compatibility with legacy code.
- New code should use coverage.%(name)s directly.
+ New code should use Coverage.%(name)s directly.
%(name)s%(argspec)s:
@@ -96,7 +96,7 @@ except KeyError:
# COPYRIGHT AND LICENSE
#
# Copyright 2001 Gareth Rees. All rights reserved.
-# Copyright 2004-2013 Ned Batchelder. All rights reserved.
+# Copyright 2004-2014 Ned Batchelder. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
diff --git a/coverage/backward.py b/coverage/backward.py
index 9597449c..e839f6bd 100644
--- a/coverage/backward.py
+++ b/coverage/backward.py
@@ -148,7 +148,7 @@ def import_local_file(modname):
if SourceFileLoader:
mod = SourceFileLoader(modname, modfile).load_module()
else:
- for suff in imp.get_suffixes():
+ for suff in imp.get_suffixes(): # pragma: part covered
if suff[0] == '.py':
break
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index bd10d5a8..3d1f5f6a 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -19,9 +19,12 @@ class Opts(object):
'', '--branch', action='store_true',
help="Measure branch coverage in addition to statement coverage."
)
- coroutine = optparse.make_option(
- '', '--coroutine', action='store', metavar="LIB",
- help="Properly measure code using coroutines."
+ CONCURRENCY_CHOICES = ["thread", "gevent", "greenlet", "eventlet"]
+ concurrency = optparse.make_option(
+ '', '--concurrency', action='store', metavar="LIB",
+ choices=CONCURRENCY_CHOICES,
+ help="Properly measure code using a concurrency library. "
+ "Valid values are: %s." % ", ".join(CONCURRENCY_CHOICES)
)
debug = optparse.make_option(
'', '--debug', action='store', metavar="OPTS",
@@ -46,8 +49,8 @@ class Opts(object):
include = optparse.make_option(
'', '--include', action='store',
metavar="PAT1,PAT2,...",
- help="Include files only when their filename path matches one of "
- "these patterns. Usually needs quoting on the command line."
+ help="Include only files whose paths match one of these patterns."
+ "Accepts shell-style wildcards, which must be quoted."
)
pylib = optparse.make_option(
'-L', '--pylib', action='store_true',
@@ -59,17 +62,11 @@ class Opts(object):
help="Show line numbers of statements in each module that weren't "
"executed."
)
- old_omit = optparse.make_option(
- '-o', '--omit', action='store',
- metavar="PAT1,PAT2,...",
- help="Omit files when their filename matches one of these patterns. "
- "Usually needs quoting on the command line."
- )
omit = optparse.make_option(
'', '--omit', action='store',
metavar="PAT1,PAT2,...",
- help="Omit files when their filename matches one of these patterns. "
- "Usually needs quoting on the command line."
+ help="Omit files whose paths match one of these patterns. "
+ "Accepts shell-style wildcards, which must be quoted."
)
output_xml = optparse.make_option(
'-o', '', action='store', dest="outfile",
@@ -125,7 +122,7 @@ class CoverageOptionParser(optparse.OptionParser, object):
self.set_defaults(
actions=[],
branch=None,
- coroutine=None,
+ concurrency=None,
debug=None,
directory=None,
fail_under=None,
@@ -175,42 +172,17 @@ class CoverageOptionParser(optparse.OptionParser, object):
raise self.OptionParserError
-class ClassicOptionParser(CoverageOptionParser):
- """Command-line parser for coverage.py classic arguments."""
+class GlobalOptionParser(CoverageOptionParser):
+ """Command-line parser for coverage.py global option arguments."""
def __init__(self):
- super(ClassicOptionParser, self).__init__()
-
- self.add_action('-a', '--annotate', 'annotate')
- self.add_action('-b', '--html', 'html')
- self.add_action('-c', '--combine', 'combine')
- self.add_action('-e', '--erase', 'erase')
- self.add_action('-r', '--report', 'report')
- self.add_action('-x', '--execute', 'execute')
+ super(GlobalOptionParser, self).__init__()
self.add_options([
- Opts.directory,
Opts.help,
- Opts.ignore_errors,
- Opts.pylib,
- Opts.show_missing,
- Opts.old_omit,
- Opts.parallel_mode,
- Opts.timid,
Opts.version,
])
- def add_action(self, dash, dashdash, action_code):
- """Add a specialized option that is the action to execute."""
- option = self.add_option(dash, dashdash, action='callback',
- callback=self._append_action
- )
- option.action_code = action_code
-
- def _append_action(self, option, opt_unused, value_unused, parser):
- """Callback for an option that adds to the `actions` list."""
- parser.values.actions.append(option.action_code)
-
class CmdOptionParser(CoverageOptionParser):
"""Parse one of the new-style commands for coverage.py."""
@@ -320,7 +292,7 @@ CMDS = {
[
Opts.append,
Opts.branch,
- Opts.coroutine,
+ Opts.concurrency,
Opts.debug,
Opts.pylib,
Opts.parallel_mode,
@@ -370,7 +342,7 @@ class CoverageScript(object):
self.run_python_file = _run_python_file or run_python_file
self.run_python_module = _run_python_module or run_python_module
self.help_fn = _help_fn or self.help
- self.classic = False
+ self.global_option = False
self.coverage = None
@@ -387,11 +359,11 @@ class CoverageScript(object):
self.help_fn(topic='minimum_help')
return OK
- # The command syntax we parse depends on the first argument. Classic
- # syntax always starts with an option.
- self.classic = argv[0].startswith('-')
- if self.classic:
- parser = ClassicOptionParser()
+ # The command syntax we parse depends on the first argument. Global
+ # switch syntax always starts with an option.
+ self.global_option = argv[0].startswith('-')
+ if self.global_option:
+ parser = GlobalOptionParser()
else:
parser = CMDS.get(argv[0])
if not parser:
@@ -429,7 +401,7 @@ class CoverageScript(object):
omit = omit,
include = include,
debug = debug,
- coroutine = options.coroutine,
+ concurrency = options.concurrency,
)
if 'debug' in options.actions:
@@ -508,7 +480,7 @@ class CoverageScript(object):
"""
# Handle help.
if options.help:
- if self.classic:
+ if self.global_option:
self.help_fn(topic='help')
else:
self.help_fn(parser=parser)
@@ -656,53 +628,6 @@ def unglob_args(args):
HELP_TOPICS = {
# -------------------------
-'classic':
-r"""Coverage.py version %(__version__)s
-Measure, collect, and report on code coverage in Python programs.
-
-Usage:
-
-coverage -x [-p] [-L] [--timid] MODULE.py [ARG1 ARG2 ...]
- Execute the module, passing the given command-line arguments, collecting
- coverage data. With the -p option, include the machine name and process
- id in the .coverage file name. With -L, measure coverage even inside the
- Python installed library, which isn't done by default. With --timid, use a
- simpler but slower trace method.
-
-coverage -e
- Erase collected coverage data.
-
-coverage -c
- Combine data from multiple coverage files (as created by -p option above)
- and store it into a single file representing the union of the coverage.
-
-coverage -r [-m] [-i] [-o DIR,...] [FILE1 FILE2 ...]
- Report on the statement coverage for the given files. With the -m
- option, show line numbers of the statements that weren't executed.
-
-coverage -b -d DIR [-i] [-o DIR,...] [FILE1 FILE2 ...]
- Create an HTML report of the coverage of the given files. Each file gets
- its own page, with the file listing decorated to show executed, excluded,
- and missed lines.
-
-coverage -a [-d DIR] [-i] [-o DIR,...] [FILE1 FILE2 ...]
- Make annotated copies of the given files, marking statements that
- are executed with > and statements that are missed with !.
-
--d DIR
- Write output files for -b or -a to this directory.
-
--i Ignore errors while reporting or annotating.
-
--o DIR,...
- Omit reporting or annotating files when their filename path starts with
- a directory listed in the omit list.
- e.g. coverage -i -r -o c:\python25,lib\enthought\traits
-
-Coverage data is saved in the file .coverage by default. Set the
-COVERAGE_FILE environment variable to save it somewhere else.
-""",
-# -------------------------
'help': """\
Coverage.py, version %(__version__)s
Measure, collect, and report on code coverage in Python programs.
@@ -720,7 +645,6 @@ Commands:
xml Create an XML report of coverage results.
Use "coverage help <command>" for detailed help on any command.
-Use "coverage help classic" for help on older command syntax.
For more information, see %(__url__)s
""",
# -------------------------
diff --git a/coverage/codeunit.py b/coverage/codeunit.py
index c9ab2622..da617913 100644
--- a/coverage/codeunit.py
+++ b/coverage/codeunit.py
@@ -32,24 +32,16 @@ def code_unit_factory(morfs, file_locator, get_plugin=None):
if isinstance(morf, string_class) and get_plugin:
plugin = get_plugin(morf)
if plugin:
- klass = plugin.code_unit_class(morf)
- #klass = DjangoTracer # NOT REALLY! TODO
- # Hacked-in Mako support. Define COVERAGE_MAKO_PATH as a fragment of
- # the path that indicates the Python file is actually a compiled Mako
- # template. THIS IS TEMPORARY!
- #MAKO_PATH = os.environ.get('COVERAGE_MAKO_PATH')
- #if MAKO_PATH and isinstance(morf, string_class) and MAKO_PATH in morf:
- # # Super hack! Do mako both ways!
- # if 0:
- # cu = PythonCodeUnit(morf, file_locator)
- # cu.name += '_fako'
- # code_units.append(cu)
- # klass = MakoCodeUnit
- #elif isinstance(morf, string_class) and morf.endswith(".html"):
- # klass = DjangoCodeUnit
+ file_reporter = plugin.file_reporter(morf)
+ if file_reporter is None:
+ raise CoverageException(
+ "Plugin %r did not provide a file reporter for %r." % (
+ plugin.plugin_name, morf
+ )
+ )
else:
- klass = PythonCodeUnit
- code_units.append(klass(morf, file_locator))
+ file_reporter = PythonCodeUnit(morf, file_locator)
+ code_units.append(file_reporter)
return code_units
diff --git a/coverage/collector.py b/coverage/collector.py
index 85c8dc90..001bc3d3 100644
--- a/coverage/collector.py
+++ b/coverage/collector.py
@@ -23,6 +23,11 @@ except ImportError:
sys.exit(1)
CTracer = None
+try:
+ import __pypy__ # pylint: disable=import-error
+except ImportError:
+ __pypy__ = None
+
class Collector(object):
"""Collects trace data.
@@ -46,7 +51,7 @@ class Collector(object):
_collectors = []
def __init__(self,
- should_trace, check_include, timid, branch, warn, coroutine,
+ should_trace, check_include, timid, branch, warn, concurrency,
):
"""Create a collector.
@@ -68,7 +73,9 @@ class Collector(object):
`warn` is a warning function, taking a single string message argument,
to be used if a warning needs to be issued.
- TODO: `coroutine`
+ `concurrency` is a string indicating the concurrency library in use.
+ Valid values are "greenlet", "eventlet", "gevent", or "thread" (the
+ default).
"""
self.should_trace = should_trace
@@ -76,21 +83,21 @@ class Collector(object):
self.warn = warn
self.branch = branch
self.threading = None
- self.coroutine = coroutine
+ self.concurrency = concurrency
- self.coroutine_id_func = None
+ self.concur_id_func = None
try:
- if coroutine == "greenlet":
- import greenlet
- self.coroutine_id_func = greenlet.getcurrent
- elif coroutine == "eventlet":
- import eventlet.greenthread
- self.coroutine_id_func = eventlet.greenthread.getcurrent
- elif coroutine == "gevent":
- import gevent
- self.coroutine_id_func = gevent.getcurrent
- elif coroutine == "thread" or not coroutine:
+ if concurrency == "greenlet":
+ import greenlet # pylint: disable=import-error
+ self.concur_id_func = greenlet.getcurrent
+ elif concurrency == "eventlet":
+ import eventlet.greenthread # pylint: disable=import-error
+ self.concur_id_func = eventlet.greenthread.getcurrent
+ elif concurrency == "gevent":
+ import gevent # pylint: disable=import-error
+ self.concur_id_func = gevent.getcurrent
+ elif concurrency == "thread" or not concurrency:
# It's important to import threading only if we need it. If
# it's imported early, and the program being measured uses
# gevent, then gevent's monkey-patching won't work properly.
@@ -98,12 +105,12 @@ class Collector(object):
self.threading = threading
else:
raise CoverageException(
- "Don't understand coroutine=%s" % coroutine
+ "Don't understand concurrency=%s" % concurrency
)
except ImportError:
raise CoverageException(
- "Couldn't trace with coroutine=%s, "
- "the module isn't installed." % coroutine
+ "Couldn't trace with concurrency=%s, "
+ "the module isn't installed." % concurrency
)
self.reset()
@@ -134,7 +141,24 @@ class Collector(object):
# A cache of the results from should_trace, the decision about whether
# to trace execution in a file. A dict of filename to (filename or
# None).
- self.should_trace_cache = {}
+ if __pypy__ is not None:
+ # Alex Gaynor said:
+ # should_trace_cache is a strictly growing key: once a key is in
+ # it, it never changes. Further, the keys used to access it are
+ # generally constant, given sufficient context. That is to say, at
+ # any given point _trace() is called, pypy is able to know the key.
+ # This is because the key is determined by the physical source code
+ # line, and that's invariant with the call site.
+ #
+ # This property of a dict with immutable keys, combined with
+ # call-site-constant keys is a match for PyPy's module dict,
+ # which is optimized for such workloads.
+ #
+ # This gives a 20% benefit on the workload described at
+ # https://bitbucket.org/pypy/pypy/issue/1871/10x-slower-than-cpython-under-coverage
+ self.should_trace_cache = __pypy__.newdict("module")
+ else:
+ self.should_trace_cache = {}
# Our active Tracers.
self.tracers = []
@@ -148,13 +172,13 @@ class Collector(object):
tracer.should_trace_cache = self.should_trace_cache
tracer.warn = self.warn
- if hasattr(tracer, 'coroutine_id_func'):
- tracer.coroutine_id_func = self.coroutine_id_func
- elif self.coroutine_id_func:
+ if hasattr(tracer, 'concur_id_func'):
+ tracer.concur_id_func = self.concur_id_func
+ elif self.concur_id_func:
raise CoverageException(
- "Can't support coroutine=%s with %s, "
+ "Can't support concurrency=%s with %s, "
"only threads are supported" % (
- self.coroutine, self.tracer_name(),
+ self.concurrency, self.tracer_name(),
)
)
@@ -204,6 +228,7 @@ class Collector(object):
# Install the tracer on this thread.
fn = self._start_tracer()
+ # Replay all the events from fullcoverage into the new trace function.
for args in traces0:
(frame, event, arg), lineno = args
try:
diff --git a/coverage/config.py b/coverage/config.py
index c671ef75..4d599ee7 100644
--- a/coverage/config.py
+++ b/coverage/config.py
@@ -2,6 +2,8 @@
import os, re, sys
from coverage.backward import string_class, iitems
+from coverage.misc import CoverageException
+
# In py3, # ConfigParser was renamed to the more-standard configparser
try:
@@ -140,7 +142,7 @@ class CoverageConfig(object):
# Defaults for [run]
self.branch = False
- self.coroutine = None
+ self.concurrency = None
self.cover_pylib = False
self.data_file = ".coverage"
self.parallel = False
@@ -173,15 +175,6 @@ class CoverageConfig(object):
# Options for plugins
self.plugin_options = {}
- def from_environment(self, env_var):
- """Read configuration from the `env_var` environment variable."""
- # 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.
- env = os.environ.get(env_var, '')
- if env:
- self.timid = ('--timid' in env)
-
MUST_BE_LIST = ["omit", "include", "debug", "plugins"]
def from_args(self, **kwargs):
@@ -235,7 +228,7 @@ class CoverageConfig(object):
# [run]
('branch', 'run:branch', 'boolean'),
- ('coroutine', 'run:coroutine'),
+ ('concurrency', 'run:concurrency'),
('cover_pylib', 'run:cover_pylib', 'boolean'),
('data_file', 'run:data_file'),
('debug', 'run:debug', 'list'),
@@ -275,3 +268,37 @@ class CoverageConfig(object):
def get_plugin_options(self, plugin):
"""Get a dictionary of options for the plugin named `plugin`."""
return self.plugin_options.get(plugin, {})
+
+ # TODO: docs for this.
+ def __setitem__(self, option_name, value):
+ # Check all the hard-coded options.
+ for option_spec in self.CONFIG_FILE_OPTIONS:
+ attr, where = option_spec[:2]
+ if where == option_name:
+ setattr(self, attr, value)
+ return
+
+ # See if it's a plugin option.
+ plugin_name, _, key = option_name.partition(":")
+ if key and plugin_name in self.plugins:
+ self.plugin_options.setdefault(plugin_name, {})[key] = value
+ return
+
+ # If we get here, we didn't find the option.
+ raise CoverageException("No such option: %r" % option_name)
+
+ # TODO: docs for this.
+ def __getitem__(self, option_name):
+ # Check all the hard-coded options.
+ for option_spec in self.CONFIG_FILE_OPTIONS:
+ attr, where = option_spec[:2]
+ if where == option_name:
+ return getattr(self, attr)
+
+ # See if it's a plugin option.
+ plugin_name, _, key = option_name.partition(":")
+ if key and plugin_name in self.plugins:
+ return self.plugin_options.get(plugin_name, {}).get(key)
+
+ # If we get here, we didn't find the option.
+ raise CoverageException("No such option: %r" % option_name)
diff --git a/coverage/control.py b/coverage/control.py
index 86a2ae23..66979c33 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -33,7 +33,7 @@ class Coverage(object):
To use::
- from coverage import coverage
+ from coverage import Coverage
cov = Coverage()
cov.start()
@@ -45,7 +45,7 @@ class Coverage(object):
def __init__(self, data_file=None, data_suffix=None, cover_pylib=None,
auto_data=False, timid=None, branch=None, config_file=True,
source=None, omit=None, include=None, debug=None,
- debug_file=None, coroutine=None, plugins=None):
+ concurrency=None):
"""
`data_file` is the base name of the data file to use, defaulting to
".coverage". `data_suffix` is appended (with a dot) to `data_file` to
@@ -81,22 +81,14 @@ class Coverage(object):
will also accept a single string argument.
`debug` is a list of strings indicating what debugging information is
- desired. `debug_file` is the file to write debug messages to,
- defaulting to stderr.
+ desired.
- `coroutine` is a string indicating the coroutining library being used
+ `concurrency` is a string indicating the concurrency library being used
in the measured code. Without this, coverage.py will get incorrect
- results. Valid strings are "greenlet", "eventlet", or "gevent", which
- are all equivalent. TODO: really?
-
- `plugins` TODO.
+ results. Valid strings are "greenlet", "eventlet", "gevent", or
+ "thread" (the default).
"""
- from coverage import __version__
-
- # A record of all the warnings that have been issued.
- self._warnings = []
-
# Build our configuration from a number of sources:
# 1: defaults:
self.config = CoverageConfig()
@@ -118,7 +110,6 @@ class Coverage(object):
self.config.from_file("setup.cfg", section_prefix="coverage:")
# 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
@@ -128,22 +119,69 @@ class Coverage(object):
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,
- coroutine=coroutine, plugins=plugins,
+ concurrency=concurrency,
)
+ self._debug_file = None
+ self._auto_data = auto_data
+ self._data_suffix = data_suffix
+
+ # The matchers for _should_trace.
+ self.source_match = None
+ self.pylib_match = self.cover_match = None
+ self.include_match = self.omit_match = None
+
+ # Is it ok for no data to be collected?
+ self._warn_no_data = True
+ self._warn_unimported_source = True
+
+ # A record of all the warnings that have been issued.
+ self._warnings = []
+
+ # 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_tracers = None
+ self.pylib_dirs = self.cover_dir = None
+ self.data_suffix = self.run_suffix = None
+ self._exclude_re = None
+ self.debug = None
+
+ # State machine variables:
+ # Have we initialized everything?
+ 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
+
+ def _init(self):
+ """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.
+
+ """
+ from coverage import __version__
+
+ if self._inited:
+ return
+
# Create and configure the debugging controller.
- self.debug = DebugControl(self.config.debug, debug_file or sys.stderr)
+ if self._debug_file is None:
+ 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.trace_judges = []
+ self.file_tracers = []
for plugin in self.plugins:
- if plugin_implements(plugin, "trace_judge"):
- self.trace_judges.append(plugin)
- self.trace_judges.append(None) # The Python case.
-
- self.auto_data = auto_data
+ if plugin_implements(plugin, "file_tracer"):
+ self.file_tracers.append(plugin)
+ self.file_tracers.append(None) # The Python case.
# _exclude_re is a dict mapping exclusion list names to compiled
# regexes.
@@ -170,21 +208,21 @@ class Coverage(object):
timid=self.config.timid,
branch=self.config.branch,
warn=self._warn,
- coroutine=self.config.coroutine,
+ concurrency=self.config.concurrency,
)
# 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 self._data_suffix or self.config.parallel:
+ if not isinstance(self._data_suffix, string_class):
# if data_suffix=True, use .machinename.pid.random
- data_suffix = True
+ self._data_suffix = True
else:
- data_suffix = None
+ self._data_suffix = None
self.data_suffix = None
- self.run_suffix = data_suffix
+ self.run_suffix = self._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
@@ -206,31 +244,24 @@ class Coverage(object):
for m in (atexit, os, platform, random, socket, _structseq):
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
+ # the path to exclude.
+ structseq_file = _structseq.structseq_new.func_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__)
- # The matchers for _should_trace.
- self.source_match = None
- self.pylib_match = self.cover_match = None
- self.include_match = self.omit_match = None
-
# Set the reporting precision.
Numbers.set_precision(self.config.precision)
- # Is it ok for no data to be collected?
- self._warn_no_data = True
- self._warn_unimported_source = True
-
- # State machine variables:
- # Have we started collecting and not stopped it?
- self._started = False
- # Have we measured some data and not harvested it?
- self._measured = False
-
atexit.register(self._atexit)
+ self._inited = True
+
def _canonical_dir(self, morf):
"""Return the canonical directory of the module or file `morf`."""
morf_filename = PythonCodeUnit(morf, self.file_locator).filename
@@ -294,15 +325,27 @@ class Coverage(object):
disp.canonical_filename = canonical
# Try the plugins, see if they have an opinion about the file.
- for plugin in self.trace_judges:
+ for plugin in self.file_tracers:
if plugin:
- plugin.trace_judge(disp)
+ #plugin.trace_judge(disp)
+ file_tracer = plugin.file_tracer(canonical)
+ if file_tracer is not None:
+ file_tracer.plugin_name = plugin.plugin_name
+ disp.trace = True
+ disp.file_tracer = file_tracer
+ disp.source_filename = self.file_locator.canonical_filename(file_tracer.source_filename())
else:
disp.trace = True
disp.source_filename = canonical
+ file_tracer = None
if disp.trace:
- disp.plugin = plugin
-
+ if file_tracer:
+ disp.file_tracer = file_tracer
+ if disp.source_filename is None:
+ raise CoverageException(
+ "Plugin %r didn't set source_filename for %r" %
+ (plugin, disp.original_filename)
+ )
if disp.check_filters:
reason = self._check_include_omit_etc(disp.source_filename)
if reason:
@@ -425,10 +468,12 @@ class Coverage(object):
`usecache` is true or false, whether to read and write data on disk.
"""
+ self._init()
self.data.usefile(usecache)
def load(self):
"""Load previously-collected coverage data from the data file."""
+ self._init()
self.collector.reset()
self.data.read()
@@ -442,11 +487,12 @@ class Coverage(object):
process might not shut down cleanly.
"""
+ self._init()
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:
+ if self._auto_data:
self.load()
# Create the matchers we need for _should_trace
@@ -478,14 +524,15 @@ class Coverage(object):
def stop(self):
"""Stop measuring code coverage."""
+ if self._started:
+ self.collector.stop()
self._started = False
- self.collector.stop()
def _atexit(self):
"""Clean up on process shutdown."""
if self._started:
self.stop()
- if self.auto_data:
+ if self._auto_data:
self.save()
def erase(self):
@@ -495,11 +542,13 @@ class Coverage(object):
discarding the data file.
"""
+ self._init()
self.collector.reset()
self.data.erase()
def clear_exclude(self, which='exclude'):
"""Clear the exclude list."""
+ self._init()
setattr(self.config, which + "_list", [])
self._exclude_regex_stale()
@@ -518,6 +567,7 @@ class Coverage(object):
is marked for special treatment during reporting.
"""
+ self._init()
excl_list = getattr(self.config, which + "_list")
excl_list.append(regex)
self._exclude_regex_stale()
@@ -540,10 +590,12 @@ class Coverage(object):
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."""
+ 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
@@ -551,10 +603,9 @@ class Coverage(object):
# `save()` at the last minute so that the pid will be correct even
# if the process forks.
extra = ""
- if _TEST_NAME_FILE:
- f = open(_TEST_NAME_FILE)
- test_name = f.read()
- f.close()
+ 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(),
@@ -572,6 +623,7 @@ class Coverage(object):
current measurements.
"""
+ self._init()
aliases = None
if self.config.paths:
aliases = PathAliases(self.file_locator)
@@ -587,6 +639,7 @@ class Coverage(object):
Also warn about various problems collecting data.
"""
+ self._init()
if not self._measured:
return
@@ -644,6 +697,7 @@ class Coverage(object):
coverage data.
"""
+ self._init()
analysis = self._analyze(morf)
return (
analysis.filename,
@@ -794,6 +848,7 @@ class Coverage(object):
import coverage as covmod
+ self._init()
try:
implementation = platform.python_implementation()
except AttributeError:
@@ -843,7 +898,7 @@ class FileDisposition(object):
self.check_filters = True
self.trace = False
self.reason = ""
- self.plugin = None
+ self.file_tracer = None
def debug_message(self):
"""Produce a debugging message explaining the outcome."""
diff --git a/coverage/misc.py b/coverage/misc.py
index 6962ae32..a653bb62 100644
--- a/coverage/misc.py
+++ b/coverage/misc.py
@@ -55,7 +55,7 @@ def format_lines(statements, lines):
return ret
-def short_stack():
+def short_stack(): # pragma: debugging
"""Return a string summarizing the call stack."""
stack = inspect.stack()[:0:-1]
return "\n".join("%30s : %s @%d" % (t[3],t[1],t[2]) for t in stack)
diff --git a/coverage/parser.py b/coverage/parser.py
index c5e95baa..e7b9c029 100644
--- a/coverage/parser.py
+++ b/coverage/parser.py
@@ -413,7 +413,7 @@ class ByteParser(object):
for _, l in bp._bytes_lines():
yield l
- def _block_stack_repr(self, block_stack):
+ def _block_stack_repr(self, block_stack): # pragma: debugging
"""Get a string version of `block_stack`, for debugging."""
blocks = ", ".join(
"(%s, %r)" % (dis.opname[b[0]], b[1]) for b in block_stack
@@ -552,7 +552,7 @@ class ByteParser(object):
#self.validate_chunks(chunks)
return chunks
- def validate_chunks(self, chunks):
+ def validate_chunks(self, chunks): # pragma: debugging
"""Validate the rule that chunks have a single entrance."""
# starts is the entrances to the chunks
starts = set(ch.byte for ch in chunks)
diff --git a/coverage/plugin.py b/coverage/plugin.py
index 35be41a9..5d1c5306 100644
--- a/coverage/plugin.py
+++ b/coverage/plugin.py
@@ -2,28 +2,33 @@
import sys
+from coverage.misc import CoverageException
+
class CoveragePlugin(object):
"""Base class for coverage.py plugins."""
def __init__(self, options):
self.options = options
- def trace_judge(self, disposition):
- """Decide whether to trace this file with this plugin.
+ def file_tracer(self, filename):
+ """Return a FileTracer object for this file."""
+ return None
+
+ def file_reporter(self, filename):
+ """Return the FileReporter class to use for filename.
- Set disposition.trace to True if this plugin should trace this file.
- May also set other attributes in `disposition`.
+ This will only be invoked if `filename` returns non-None from
+ `file_tracer`. It's an error to return None.
"""
- return None
+ raise Exception("Plugin %r needs to implement file_reporter" % self.plugin_name)
- def source_file_name(self, filename):
- """Return the source name for a given Python filename.
- Can return None if tracing shouldn't continue.
+class FileTracer(object):
+ """Support needed for files during the tracing phase."""
- """
- return filename
+ def source_filename(self):
+ return "xyzzy"
def dynamic_source_file_name(self):
"""Returns a callable that can return a source name for a frame.
@@ -38,9 +43,16 @@ class CoveragePlugin(object):
"""
return None
- def code_unit_class(self, morf):
- """Return the CodeUnit class to use for a module or filename."""
- return None
+ def line_number_range(self, frame):
+ """Given a call frame, return the range of source line numbers."""
+ lineno = frame.f_lineno
+ return lineno, lineno
+
+
+class FileReporter(object):
+ """Support needed for files during the reporting phase."""
+ def __init__(self, filename):
+ self.filename = filename
class Plugins(object):
@@ -67,7 +79,7 @@ class Plugins(object):
if plugin_class:
options = config.get_plugin_options(module)
plugin = plugin_class(options)
- plugin.__name__ = module
+ plugin.plugin_name = module
plugins.order.append(plugin)
plugins.names[module] = plugin
diff --git a/coverage/pytracer.py b/coverage/pytracer.py
index 7563ae11..84071bb1 100644
--- a/coverage/pytracer.py
+++ b/coverage/pytracer.py
@@ -33,7 +33,7 @@ class PyTracer(object):
# The threading module to use, if any.
self.threading = None
- self.plugin = []
+ self.file_tracer = []
self.cur_file_dict = []
self.last_line = [0]
@@ -62,7 +62,7 @@ class PyTracer(object):
if self.arcs and self.cur_file_dict:
pair = (self.last_line, -self.last_exc_firstlineno)
self.cur_file_dict[pair] = None
- self.plugin, self.cur_file_dict, self.last_line = (
+ self.file_tracer, self.cur_file_dict, self.last_line = (
self.data_stack.pop()
)
self.last_exc_back = None
@@ -71,7 +71,7 @@ class PyTracer(object):
# Entering a new function context. Decide if we should trace
# in this file.
self.data_stack.append(
- (self.plugin, self.cur_file_dict, self.last_line)
+ (self.file_tracer, self.cur_file_dict, self.last_line)
)
filename = frame.f_code.co_filename
disp = self.should_trace_cache.get(filename)
@@ -79,12 +79,12 @@ class PyTracer(object):
disp = self.should_trace(filename, frame)
self.should_trace_cache[filename] = disp
- self.plugin = None
+ self.file_tracer = None
self.cur_file_dict = None
if disp.trace:
tracename = disp.source_filename
- if disp.plugin:
- dyn_func = disp.plugin.dynamic_source_file_name()
+ if disp.file_tracer:
+ dyn_func = disp.file_tracer.dynamic_source_file_name()
if dyn_func:
tracename = dyn_func(tracename, frame)
if tracename:
@@ -95,17 +95,17 @@ class PyTracer(object):
if tracename:
if tracename not in self.data:
self.data[tracename] = {}
- if disp.plugin:
- self.plugin_data[tracename] = disp.plugin.__name__
+ if disp.file_tracer:
+ self.plugin_data[tracename] = disp.file_tracer.plugin_name
self.cur_file_dict = self.data[tracename]
- self.plugin = disp.plugin
+ self.file_tracer = disp.file_tracer
# Set the last_line to -1 because the next arc will be entering a
# code block, indicated by (-1, n).
self.last_line = -1
elif event == 'line':
# Record an executed line.
- if self.plugin:
- lineno_from, lineno_to = self.plugin.line_number_range(frame)
+ if self.file_tracer:
+ lineno_from, lineno_to = self.file_tracer.line_number_range(frame)
else:
lineno_from, lineno_to = frame.f_lineno, frame.f_lineno
if lineno_from != -1:
@@ -123,7 +123,7 @@ class PyTracer(object):
first = frame.f_code.co_firstlineno
self.cur_file_dict[(self.last_line, -first)] = None
# Leaving this function, pop the filename stack.
- self.plugin, self.cur_file_dict, self.last_line = (
+ self.file_tracer, self.cur_file_dict, self.last_line = (
self.data_stack.pop()
)
elif event == 'exception':
diff --git a/coverage/results.py b/coverage/results.py
index 6cbcbfc8..4449de56 100644
--- a/coverage/results.py
+++ b/coverage/results.py
@@ -11,9 +11,8 @@ class Analysis(object):
def __init__(self, cov, code_unit):
self.coverage = cov
- self.code_unit = code_unit
- self.filename = self.code_unit.filename
+ self.filename = code_unit.filename
self.parser = code_unit.get_parser(
exclude=self.coverage._exclude_regex('exclude')
)
diff --git a/coverage/test_helpers.py b/coverage/test_helpers.py
index efe68dcd..665593a2 100644
--- a/coverage/test_helpers.py
+++ b/coverage/test_helpers.py
@@ -132,8 +132,13 @@ class StdStreamCapturingMixin(TestCase):
return self.captured_stderr.getvalue()
-class TempDirMixin(TestCase):
- """A test case mixin that creates a temp directory and files in it."""
+class TempDirMixin(SysPathAwareMixin, ModuleAwareMixin, TestCase):
+ """A test case mixin that creates a temp directory and files in it.
+
+ Includes SysPathAwareMixin and ModuleAwareMixin, because making and using
+ temp dirs like this will also need that kind of isolation.
+
+ """
# Our own setting: most of these tests run in their own temp directory.
run_in_temp_dir = True
@@ -143,12 +148,8 @@ class TempDirMixin(TestCase):
if self.run_in_temp_dir:
# Create a temporary directory.
- noise = str(random.random())[2:]
- self.temp_root = os.path.join(tempfile.gettempdir(), 'test_cover')
- self.temp_dir = os.path.join(self.temp_root, noise)
- os.makedirs(self.temp_dir)
- self.old_dir = os.getcwd()
- os.chdir(self.temp_dir)
+ self.temp_dir = self.make_temp_dir("test_cover")
+ self.chdir(self.temp_dir)
# Modules should be importable from this temp directory. We don't
# use '' because we make lots of different temp directories and
@@ -161,15 +162,24 @@ class TempDirMixin(TestCase):
class_behavior.test_method_made_any_files = False
class_behavior.temp_dir = self.run_in_temp_dir
- self.addCleanup(self.cleanup_temp_dir)
+ self.addCleanup(self.check_behavior)
- def cleanup_temp_dir(self):
- """Clean up the temp directories we made."""
+ def make_temp_dir(self, slug="test_cover"):
+ """Make a temp directory that is cleaned up when the test is done."""
+ name = "%s_%08d" % (slug, random.randint(0, 99999999))
+ temp_dir = os.path.join(tempfile.gettempdir(), name)
+ os.makedirs(temp_dir)
+ self.addCleanup(shutil.rmtree, temp_dir)
+ return temp_dir
- if self.run_in_temp_dir:
- # Get rid of the temporary directory.
- os.chdir(self.old_dir)
- shutil.rmtree(self.temp_root)
+ def chdir(self, new_dir):
+ """Change directory, and change back when the test is done."""
+ old_dir = os.getcwd()
+ os.chdir(new_dir)
+ self.addCleanup(os.chdir, old_dir)
+
+ def check_behavior(self):
+ """Check that we did the right things."""
class_behavior = self.class_behavior()
if class_behavior.test_method_made_any_files:
diff --git a/coverage/tracer.c b/coverage/tracer.c
index 5bf5c462..5a463fcb 100644
--- a/coverage/tracer.c
+++ b/coverage/tracer.c
@@ -81,7 +81,7 @@ typedef struct {
/* Python objects manipulated directly by the Collector class. */
PyObject * should_trace;
PyObject * warn;
- PyObject * coroutine_id_func;
+ PyObject * concur_id_func;
PyObject * data;
PyObject * plugin_data;
PyObject * should_trace_cache;
@@ -104,8 +104,8 @@ typedef struct {
(None).
*/
- DataStack data_stack; /* Used if we aren't doing coroutines. */
- PyObject * data_stack_index; /* Used if we are doing coroutines. */
+ DataStack data_stack; /* Used if we aren't doing concurrency. */
+ PyObject * data_stack_index; /* Used if we are doing concurrency. */
DataStack * data_stacks;
int data_stacks_alloc;
int data_stacks_used;
@@ -191,7 +191,7 @@ CTracer_init(CTracer *self, PyObject *args_unused, PyObject *kwds_unused)
self->should_trace = NULL;
self->warn = NULL;
- self->coroutine_id_func = NULL;
+ self->concur_id_func = NULL;
self->data = NULL;
self->plugin_data = NULL;
self->should_trace_cache = NULL;
@@ -234,7 +234,7 @@ CTracer_dealloc(CTracer *self)
Py_XDECREF(self->should_trace);
Py_XDECREF(self->warn);
- Py_XDECREF(self->coroutine_id_func);
+ Py_XDECREF(self->concur_id_func);
Py_XDECREF(self->data);
Py_XDECREF(self->plugin_data);
Py_XDECREF(self->should_trace_cache);
@@ -327,18 +327,18 @@ CTracer_record_pair(CTracer *self, int l1, int l2)
static int
CTracer_set_pdata_stack(CTracer *self)
{
- if (self->coroutine_id_func != Py_None) {
+ if (self->concur_id_func != Py_None) {
PyObject * co_obj = NULL;
PyObject * stack_index = NULL;
long the_index = 0;
- co_obj = PyObject_CallObject(self->coroutine_id_func, NULL);
+ co_obj = PyObject_CallObject(self->concur_id_func, NULL);
if (co_obj == NULL) {
return RET_ERROR;
}
stack_index = PyDict_GetItem(self->data_stack_index, co_obj);
if (stack_index == NULL) {
- /* A new coroutine object. Make a new data stack. */
+ /* A new concurrency object. Make a new data stack. */
the_index = self->data_stacks_used;
stack_index = MyInt_FromLong(the_index);
if (PyDict_SetItem(self->data_stack_index, co_obj, stack_index) < 0) {
@@ -505,7 +505,7 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse
if (MyText_Check(tracename)) {
PyObject * file_data = PyDict_GetItem(self->data, tracename);
- PyObject * disp_plugin = NULL;
+ PyObject * disp_file_tracer = NULL;
PyObject * disp_plugin_name = NULL;
if (file_data == NULL) {
@@ -527,16 +527,16 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse
if (self->plugin_data != NULL) {
/* If the disposition mentions a plugin, record that. */
- disp_plugin = PyObject_GetAttrString(disposition, "plugin");
- if (disp_plugin == NULL) {
+ disp_file_tracer = PyObject_GetAttrString(disposition, "file_tracer");
+ if (disp_file_tracer == NULL) {
STATS( self->stats.errors++; )
Py_DECREF(tracename);
Py_DECREF(disposition);
return RET_ERROR;
}
- if (disp_plugin != Py_None) {
- disp_plugin_name = PyObject_GetAttrString(disp_plugin, "__name__");
- Py_DECREF(disp_plugin);
+ if (disp_file_tracer != Py_None) {
+ disp_plugin_name = PyObject_GetAttrString(disp_file_tracer, "plugin_name");
+ Py_DECREF(disp_file_tracer);
if (disp_plugin_name == NULL) {
STATS( self->stats.errors++; )
Py_DECREF(tracename);
@@ -781,8 +781,8 @@ CTracer_members[] = {
{ "warn", T_OBJECT, offsetof(CTracer, warn), 0,
PyDoc_STR("Function for issuing warnings.") },
- { "coroutine_id_func", T_OBJECT, offsetof(CTracer, coroutine_id_func), 0,
- PyDoc_STR("Function for determining coroutine context") },
+ { "concur_id_func", T_OBJECT, offsetof(CTracer, concur_id_func), 0,
+ PyDoc_STR("Function for determining concurrency context") },
{ "data", T_OBJECT, offsetof(CTracer, data), 0,
PyDoc_STR("The raw dictionary of trace data.") },
diff --git a/coverage/version.py b/coverage/version.py
index 27c2f6b1..fe3f44fa 100644
--- a/coverage/version.py
+++ b/coverage/version.py
@@ -1,7 +1,7 @@
"""The version and URL for coverage.py"""
# This file is exec'ed in setup.py, don't import anything!
-__version__ = "4.0a0" # see detailed history in CHANGES.txt
+__version__ = "4.0a1" # see detailed history in CHANGES.txt
__url__ = "http://nedbatchelder.com/code/coverage"
if max(__version__).isalpha():