summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2014-09-06 14:24:15 -0400
committerNed Batchelder <ned@nedbatchelder.com>2014-09-06 14:24:15 -0400
commit0891e02eb490494ee40a7f840e4ab9fd6b3d2d7b (patch)
tree394c5e68d986579d94a153b15dee8ac977c643bd
parent7862e1fee15b6ef8fcadfd112cf93121ed8f388e (diff)
downloadpython-coveragepy-git-0891e02eb490494ee40a7f840e4ab9fd6b3d2d7b.tar.gz
Move dispositions closer to useful plugins
-rw-r--r--coverage/__init__.py2
-rw-r--r--coverage/collector.py75
-rw-r--r--coverage/control.py133
-rw-r--r--coverage/plugin.py32
-rw-r--r--coverage/tracer.c22
5 files changed, 189 insertions, 75 deletions
diff --git a/coverage/__init__.py b/coverage/__init__.py
index 5ae32aba..1097228f 100644
--- a/coverage/__init__.py
+++ b/coverage/__init__.py
@@ -7,7 +7,7 @@ http://nedbatchelder.com/code/coverage
from coverage.version import __version__, __url__
-from coverage.control import Coverage, process_startup
+from coverage.control import Coverage, process_startup, FileDisposition
from coverage.data import CoverageData
from coverage.cmdline import main, CoverageScript
from coverage.misc import CoverageException
diff --git a/coverage/collector.py b/coverage/collector.py
index 675758d0..26de3727 100644
--- a/coverage/collector.py
+++ b/coverage/collector.py
@@ -47,12 +47,12 @@ class PyTracer(object):
self.should_trace = None
self.should_trace_cache = None
self.warn = None
- self.plugins = None
+ self.plugin_data = None
+
+ self.plugin = []
+ self.cur_file_dict = []
+ self.last_line = [0]
- self.plugin = None
- self.cur_tracename = None # TODO: This is only maintained for the if0 debugging output. Get rid of it eventually.
- self.cur_file_data = None
- self.last_line = 0
self.data_stack = []
self.data_stacks = collections.defaultdict(list)
self.last_exc_back = None
@@ -92,18 +92,18 @@ class PyTracer(object):
for ident, data_stacks in self.data_stacks.items()
)
, width=250)
- pprint.pprint(sorted((self.cur_file_data or {}).keys()), width=250)
+ pprint.pprint(sorted((self.cur_file_dict or {}).keys()), width=250)
print("TRYING: {}".format(sorted(next((v for k,v in self.data.items() if k.endswith("try_it.py")), {}).keys())))
- if self.last_exc_back:
+ if self.last_exc_back: # TODO: bring this up to speed
if frame == self.last_exc_back:
# Someone forgot a return event.
- if self.arcs and self.cur_file_data:
+ if self.arcs and self.cur_file_dict:
pair = (self.last_line, -self.last_exc_firstlineno)
- self.cur_file_data[pair] = None
+ self.cur_file_dict[pair] = None
if self.coroutine_id_func:
self.data_stack = self.data_stacks[self.coroutine_id_func()]
- self.handler, _, self.cur_file_data, self.last_line = self.data_stack.pop()
+ self.plugin, self.cur_file_dict, self.last_line = self.data_stack.pop()
self.last_exc_back = None
if event == 'call':
@@ -112,7 +112,7 @@ class PyTracer(object):
if self.coroutine_id_func:
self.data_stack = self.data_stacks[self.coroutine_id_func()]
self.last_coroutine = self.coroutine_id_func()
- self.data_stack.append((self.plugin, self.cur_tracename, self.cur_file_data, self.last_line))
+ self.data_stack.append((self.plugin, self.cur_file_dict, self.last_line))
filename = frame.f_code.co_filename
disp = self.should_trace_cache.get(filename)
if disp is None:
@@ -120,19 +120,26 @@ class PyTracer(object):
self.should_trace_cache[filename] = disp
#print("called, stack is %d deep, tracename is %r" % (
# len(self.data_stack), tracename))
- tracename = disp.filename
- if tracename and disp.plugin:
- tracename = disp.plugin.file_name(frame)
+ self.plugin = 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 dyn_func:
+ tracename = dyn_func(tracename, frame)
+ if tracename:
+ if not self.check_include(tracename):
+ tracename = None
+ else:
+ tracename = None
if tracename:
if tracename not in self.data:
self.data[tracename] = {}
if disp.plugin:
- self.plugins[tracename] = disp.plugin.__name__
- self.cur_tracename = tracename
- self.cur_file_data = self.data[tracename]
+ self.plugin_data[tracename] = disp.plugin.__name__
+ self.cur_file_dict = self.data[tracename]
self.plugin = disp.plugin
- else:
- self.cur_file_data = None
# Set the last_line to -1 because the next arc will be entering a
# code block, indicated by (-1, n).
self.last_line = -1
@@ -142,29 +149,30 @@ class PyTracer(object):
this_coroutine = self.coroutine_id_func()
if self.last_coroutine != this_coroutine:
print("mismatch: {0} != {1}".format(self.last_coroutine, this_coroutine))
+
if self.plugin:
lineno_from, lineno_to = self.plugin.line_number_range(frame)
else:
lineno_from, lineno_to = frame.f_lineno, frame.f_lineno
if lineno_from != -1:
- if self.cur_file_data is not None:
+ if self.cur_file_dict is not None:
if self.arcs:
#print("lin", self.last_line, frame.f_lineno)
- self.cur_file_data[(self.last_line, lineno_from)] = None
+ self.cur_file_dict[(self.last_line, lineno_from)] = None
else:
#print("lin", frame.f_lineno)
for lineno in range(lineno_from, lineno_to+1):
- self.cur_file_data[lineno] = None
+ self.cur_file_dict[lineno] = None
self.last_line = lineno_to
elif event == 'return':
- if self.arcs and self.cur_file_data:
+ if self.arcs and self.cur_file_dict:
first = frame.f_code.co_firstlineno
- self.cur_file_data[(self.last_line, -first)] = None
+ self.cur_file_dict[(self.last_line, -first)] = None
# Leaving this function, pop the filename stack.
if self.coroutine_id_func:
self.data_stack = self.data_stacks[self.coroutine_id_func()]
self.last_coroutine = self.coroutine_id_func()
- self.plugin, _, self.cur_file_data, self.last_line = self.data_stack.pop()
+ self.plugin, self.cur_file_dict, self.last_line = self.data_stack.pop()
#print("returned, stack is %d deep" % (len(self.data_stack)))
elif event == 'exception':
#print("exc", self.last_line, frame.f_lineno)
@@ -224,13 +232,15 @@ class Collector(object):
# the top, and resumed when they become the top again.
_collectors = []
- def __init__(self, should_trace, timid, branch, warn, coroutine):
+ def __init__(self, should_trace, check_include, timid, branch, warn, coroutine):
"""Create a collector.
`should_trace` is a function, taking a filename, and returning a
canonicalized filename, or None depending on whether the file should
be traced or not.
+ TODO: `check_include`
+
If `timid` is true, then a slower simpler trace function will be
used. This is important for some environments where manipulation of
tracing functions make the faster more sophisticated trace function not
@@ -243,10 +253,14 @@ 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`
+
"""
self.should_trace = should_trace
+ self.check_include = check_include
self.warn = warn
self.branch = branch
+
if coroutine == "greenlet":
import greenlet
self.coroutine_id_func = greenlet.getcurrent
@@ -258,6 +272,7 @@ class Collector(object):
self.coroutine_id_func = gevent.getcurrent
else:
self.coroutine_id_func = None
+
self.reset()
if timid:
@@ -281,7 +296,7 @@ class Collector(object):
# or mapping filenames to dicts with linenumber pairs as keys.
self.data = {}
- self.plugins = {}
+ self.plugin_data = {}
# 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
@@ -301,8 +316,8 @@ class Collector(object):
tracer.warn = self.warn
if hasattr(tracer, 'coroutine_id_func'):
tracer.coroutine_id_func = self.coroutine_id_func
- if hasattr(tracer, 'plugins'):
- tracer.plugins = self.plugins
+ if hasattr(tracer, 'plugin_data'):
+ tracer.plugin_data = self.plugin_data
fn = tracer.start()
self.tracers.append(tracer)
return fn
@@ -421,4 +436,4 @@ class Collector(object):
return {}
def get_plugin_data(self):
- return self.plugins
+ return self.plugin_data
diff --git a/coverage/control.py b/coverage/control.py
index fab2ea19..42550508 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -135,8 +135,14 @@ class Coverage(object):
self.debug = DebugControl(self.config.debug, debug_file or sys.stderr)
# Load plugins
- plugins = load_plugins(self.config.plugins, self.config)
- self.tracer_plugins = []#[cls() for cls in tracer_classes]
+ self.plugins = load_plugins(self.config.plugins, self.config)
+
+ self.trace_judges = []
+ for plugin in self.plugins:
+ trace_judge = plugin.trace_judge()
+ if trace_judge:
+ self.trace_judges.append((plugin, trace_judge))
+ self.trace_judges.append((None, None)) # The Python case.
self.auto_data = auto_data
@@ -160,8 +166,11 @@ class Coverage(object):
self.include = prep_patterns(self.config.include)
self.collector = Collector(
- self._should_trace, timid=self.config.timid,
- branch=self.config.branch, warn=self._warn,
+ should_trace=self._should_trace,
+ check_include=self._tracing_check_include_omit_etc,
+ timid=self.config.timid,
+ branch=self.config.branch,
+ warn=self._warn,
coroutine=self.config.coroutine,
)
@@ -246,21 +255,11 @@ class Coverage(object):
Returns a FileDisposition object.
"""
- disp = FileDisposition(filename)
-
- if not filename:
- # Empty string is pretty useless
- return disp.nope("empty string isn't a filename")
-
- if filename.startswith('memory:'):
- return disp.nope("memory isn't traceable")
-
- if filename.startswith('<'):
- # 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 disp.nope("not a real filename")
+ original_filename = filename
+ def nope(reason):
+ disp = FileDisposition(original_filename)
+ disp.nope(reason)
+ return disp
self._check_for_packages()
@@ -274,6 +273,20 @@ class Coverage(object):
if dunder_file:
filename = self._source_for_file(dunder_file)
+ if not filename:
+ # Empty string is pretty useless
+ return nope("empty string isn't a filename")
+
+ if filename.startswith('memory:'):
+ return nope("memory isn't traceable")
+
+ if filename.startswith('<'):
+ # 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 nope("not a real filename")
+
# Jython reports the .class file to the tracer, use the source file.
if filename.endswith("$py.class"):
filename = filename[:-9] + ".py"
@@ -281,39 +294,60 @@ class Coverage(object):
canonical = self.file_locator.canonical_filename(filename)
# Try the plugins, see if they have an opinion about the file.
- for tracer in self.tracer_plugins:
- plugin_disp = tracer.should_trace(canonical)
- if plugin_disp:
- plugin_disp.plugin = tracer
- return plugin_disp
+ for plugin, judge in self.trace_judges:
+ if plugin:
+ disp = judge(canonical)
+ else:
+ disp = FileDisposition(original_filename)
+ if disp is not None:
+ if plugin:
+ disp.source_filename = plugin.source_file_name(canonical)
+ else:
+ disp.source_filename = canonical
+ disp.plugin = plugin
+
+ reason = self._check_include_omit_etc(disp.source_filename)
+ if reason:
+ disp.nope(reason)
+
+ return disp
+
+ return nope("no plugin found") # TODO: a test that causes this.
+ def _check_include_omit_etc(self, filename):
+ """Check a filename 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.
+
+ """
# If the user specified source or include, then that's authoritative
# about the outer bound of what to measure and we don't have to apply
# any canned exclusions. If they didn't, then we have to exclude the
# stdlib and coverage.py directories.
if self.source_match:
- if not self.source_match.match(canonical):
- return disp.nope("falls outside the --source trees")
+ if not self.source_match.match(filename):
+ return "falls outside the --source trees"
elif self.include_match:
- if not self.include_match.match(canonical):
- return disp.nope("falls outside the --include trees")
+ if not self.include_match.match(filename):
+ return "falls outside the --include trees"
else:
# 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 self.pylib_match and self.pylib_match.match(canonical):
- return disp.nope("is in the stdlib")
+ 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.
- if self.cover_match and self.cover_match.match(canonical):
- return disp.nope("is part of coverage.py")
+ if self.cover_match and self.cover_match.match(filename):
+ return "is part of coverage.py"
# Check the file against the omit pattern.
- if self.omit_match and self.omit_match.match(canonical):
- return disp.nope("is inside an --omit pattern")
+ if self.omit_match and self.omit_match.match(filename):
+ return "is inside an --omit pattern"
- disp.filename = canonical
- return disp
+ # No reason found to skip this file.
+ return None
def _should_trace(self, filename, frame):
"""Decide whether to trace execution in `filename`.
@@ -326,6 +360,22 @@ class Coverage(object):
self.debug.write(disp.debug_message())
return disp
+ def _tracing_check_include_omit_etc(self, filename):
+ """Check a filename against the include, omit, etc, rules, and say so.
+
+ Returns a boolean: True if the file should be traced, False if not.
+
+ """
+ reason = self._check_include_omit_etc(filename)
+ if self.debug.should('trace'):
+ if not reason:
+ msg = "Tracing %r" % (filename,)
+ else:
+ msg = "Not tracing %r: %s" % (filename, reason)
+ self.debug.write(msg)
+
+ return not reason
+
def _warn(self, msg):
"""Use `msg` as a warning."""
self._warnings.append(msg)
@@ -783,21 +833,22 @@ 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.filename = None
+ self.source_filename = None
+ self.trace = True
self.reason = ""
self.plugin = None
def nope(self, reason):
"""A helper for returning a NO answer from should_trace."""
+ self.trace = False
self.reason = reason
- return self
def debug_message(self):
"""Produce a debugging message explaining the outcome."""
- if not self.filename:
- msg = "Not tracing %r: %s" % (self.original_filename, self.reason)
- else:
+ if self.trace:
msg = "Tracing %r" % (self.original_filename,)
+ else:
+ msg = "Not tracing %r: %s" % (self.original_filename, self.reason)
return msg
diff --git a/coverage/plugin.py b/coverage/plugin.py
index 8e26ae6b..84d81a28 100644
--- a/coverage/plugin.py
+++ b/coverage/plugin.py
@@ -8,6 +8,37 @@ class CoveragePlugin(object):
def __init__(self, options):
self.options = options
+ def trace_judge(self):
+ """Return a callable that can decide whether to trace a file or not.
+
+ The callable should take a filename, and return a coverage.TraceDisposition
+ object.
+
+ """
+ return None
+
+ # TODO: why does trace_judge return a callable, but source_file_name is callable?
+ def source_file_name(self, filename):
+ """Return the source name for a given Python filename.
+
+ Can return None if tracing shouldn't continue.
+
+ """
+ return filename
+
+ def dynamic_source_file_name(self):
+ """Returns a callable that can return a source name for a frame.
+
+ The callable should take a filename and a frame, and return either a
+ filename or None:
+
+ def dynamic_source_filename_func(filename, frame)
+
+ Can return None if dynamic filenames aren't needed.
+
+ """
+ return None
+
def load_plugins(modules, config):
"""Load plugins from `modules`.
@@ -25,6 +56,7 @@ def load_plugins(modules, config):
if plugin_class:
options = config.get_plugin_options(module)
plugin = plugin_class(options)
+ plugin.__name__ = module
plugins.append(plugin)
return plugins
diff --git a/coverage/tracer.c b/coverage/tracer.c
index ca8d61c1..c0066046 100644
--- a/coverage/tracer.c
+++ b/coverage/tracer.c
@@ -260,6 +260,7 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse
PyObject * filename = NULL;
PyObject * tracename = NULL;
PyObject * disposition = NULL;
+ PyObject * disp_trace = NULL;
#if WHAT_LOG || TRACE_LOG
PyObject * ascii = NULL;
#endif
@@ -358,13 +359,28 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse
Py_INCREF(disposition);
}
- /* If tracename is a string, then we're supposed to trace. */
- tracename = PyObject_GetAttrString(disposition, "filename");
- if (tracename == NULL) {
+ disp_trace = PyObject_GetAttrString(disposition, "trace");
+ if (disp_trace == NULL) {
STATS( self->stats.errors++; )
Py_DECREF(disposition);
return RET_ERROR;
}
+
+ tracename = Py_None;
+ Py_INCREF(tracename);
+
+ if (disp_trace == Py_True) {
+ /* If tracename is a string, then we're supposed to trace. */
+ tracename = PyObject_GetAttrString(disposition, "source_filename");
+ if (tracename == NULL) {
+ STATS( self->stats.errors++; )
+ Py_DECREF(disposition);
+ Py_DECREF(disp_trace);
+ return RET_ERROR;
+ }
+ }
+ Py_DECREF(disp_trace);
+
if (MyText_Check(tracename)) {
PyObject * file_data = PyDict_GetItem(self->data, tracename);
if (file_data == NULL) {