summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--coverage/__init__.py2
-rw-r--r--coverage/codeunit.py22
-rw-r--r--coverage/control.py68
-rw-r--r--coverage/plugin.py84
-rw-r--r--coverage/report.py4
-rw-r--r--coverage/tracer.c37
-rw-r--r--tests/test_plugins.py127
7 files changed, 273 insertions, 71 deletions
diff --git a/coverage/__init__.py b/coverage/__init__.py
index 1097228f..5ae32aba 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, FileDisposition
+from coverage.control import Coverage, process_startup
from coverage.data import CoverageData
from coverage.cmdline import main, CoverageScript
from coverage.misc import CoverageException
diff --git a/coverage/codeunit.py b/coverage/codeunit.py
index 35167a72..3ec9c390 100644
--- a/coverage/codeunit.py
+++ b/coverage/codeunit.py
@@ -10,14 +10,16 @@ from coverage.phystokens import source_token_lines, source_encoding
from coverage.django import DjangoTracer
-def code_unit_factory(morfs, file_locator, get_ext=None):
+def code_unit_factory(morfs, file_locator, get_plugin=None):
"""Construct a list of CodeUnits from polymorphic inputs.
`morfs` is a module or a filename, or a list of same.
`file_locator` is a FileLocator that can help resolve filenames.
- `get_ext` TODO
+ `get_plugin` is a function taking a filename, and returning a plugin
+ responsible for the file. It can also return None if there is no plugin
+ claiming the file.
Returns a list of CodeUnit objects.
@@ -26,15 +28,14 @@ def code_unit_factory(morfs, file_locator, get_ext=None):
if not isinstance(morfs, (list, tuple)):
morfs = [morfs]
- django_tracer = DjangoTracer()
-
code_units = []
for morf in morfs:
- ext = None
- if isinstance(morf, string_class) and get_ext:
- ext = get_ext(morf)
- if ext:
- klass = DjangoTracer # NOT REALLY! TODO
+ 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!
@@ -162,6 +163,9 @@ class CodeUnit(object):
"""
return False
+ def get_parser(self, exclude=None):
+ raise NotImplementedError
+
class PythonCodeUnit(CodeUnit):
"""Represents a Python file."""
diff --git a/coverage/control.py b/coverage/control.py
index 42550508..86a2ae23 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -9,7 +9,7 @@ from coverage.collector import Collector
from coverage.config import CoverageConfig
from coverage.data import CoverageData
from coverage.debug import DebugControl
-from coverage.plugin import load_plugins
+from coverage.plugin import Plugins, plugin_implements
from coverage.files import FileLocator, TreeMatcher, FnmatchMatcher
from coverage.files import PathAliases, find_python_files, prep_patterns
from coverage.html import HtmlReporter
@@ -135,14 +135,13 @@ class Coverage(object):
self.debug = DebugControl(self.config.debug, debug_file or sys.stderr)
# Load plugins
- self.plugins = load_plugins(self.config.plugins, self.config)
+ self.plugins = 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.
+ if plugin_implements(plugin, "trace_judge"):
+ self.trace_judges.append(plugin)
+ self.trace_judges.append(None) # The Python case.
self.auto_data = auto_data
@@ -255,10 +254,10 @@ class Coverage(object):
Returns a FileDisposition object.
"""
- original_filename = filename
- def nope(reason):
- disp = FileDisposition(original_filename)
- disp.nope(reason)
+ disp = FileDisposition(filename)
+ def nope(disp, reason):
+ disp.trace = False
+ disp.reason = reason
return disp
self._check_for_packages()
@@ -275,44 +274,43 @@ class Coverage(object):
if not filename:
# Empty string is pretty useless
- return nope("empty string isn't a filename")
+ return nope(disp, "empty string isn't a filename")
if filename.startswith('memory:'):
- return nope("memory isn't traceable")
+ 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
# "<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")
+ return nope(disp, "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"
canonical = self.file_locator.canonical_filename(filename)
+ disp.canonical_filename = canonical
# Try the plugins, see if they have an opinion about the file.
- for plugin, judge in self.trace_judges:
+ for plugin in self.trace_judges:
if plugin:
- disp = judge(canonical)
+ plugin.trace_judge(disp)
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.trace = True
+ disp.source_filename = canonical
+ if disp.trace:
disp.plugin = plugin
- reason = self._check_include_omit_etc(disp.source_filename)
- if reason:
- disp.nope(reason)
+ if disp.check_filters:
+ reason = self._check_include_omit_etc(disp.source_filename)
+ if reason:
+ nope(disp, reason)
return disp
- return nope("no plugin found") # TODO: a test that causes this.
+ return nope(disp, "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.
@@ -661,10 +659,17 @@ class Coverage(object):
Returns an `Analysis` object.
"""
+ def get_plugin(filename):
+ """For code_unit_factory to use to find the plugin for a file."""
+ plugin = None
+ plugin_name = self.data.plugin_data().get(filename)
+ if plugin_name:
+ plugin = self.plugins.get(plugin_name)
+ return plugin
+
self._harvest_data()
if not isinstance(it, CodeUnit):
- get_ext = self.data.plugin_data().get
- it = code_unit_factory(it, self.file_locator, get_ext)[0]
+ it = code_unit_factory(it, self.file_locator, get_plugin)[0]
return Analysis(self, it)
@@ -833,16 +838,13 @@ 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 = True
+ self.check_filters = True
+ self.trace = False
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
-
def debug_message(self):
"""Produce a debugging message explaining the outcome."""
if self.trace:
diff --git a/coverage/plugin.py b/coverage/plugin.py
index 84d81a28..35be41a9 100644
--- a/coverage/plugin.py
+++ b/coverage/plugin.py
@@ -8,16 +8,15 @@ 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.
+ def trace_judge(self, disposition):
+ """Decide whether to trace this file with this plugin.
- The callable should take a filename, and return a coverage.TraceDisposition
- object.
+ Set disposition.trace to True if this plugin should trace this file.
+ May also set other attributes in `disposition`.
"""
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.
@@ -39,24 +38,71 @@ class CoveragePlugin(object):
"""
return None
+ def code_unit_class(self, morf):
+ """Return the CodeUnit class to use for a module or filename."""
+ return None
+
+
+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()
-def load_plugins(modules, config):
- """Load plugins from `modules`.
+ for module in modules:
+ __import__(module)
+ mod = sys.modules[module]
- Returns a list of loaded and configured plugins.
+ plugin_class = getattr(mod, "Plugin", None)
+ if plugin_class:
+ options = config.get_plugin_options(module)
+ plugin = plugin_class(options)
+ plugin.__name__ = module
+ plugins.order.append(plugin)
+ plugins.names[module] = plugin
+
+ return plugins
+
+ def __iter__(self):
+ return iter(self.order)
+
+ def get(self, module):
+ return self.names[module]
+
+
+def overrides(obj, method_name, base_class):
+ """Does `obj` override the `method_name` it got from `base_class`?
+
+ Determine if `obj` implements the method called `method_name`, which it
+ inherited from `base_class`.
+
+ Returns a boolean.
"""
- plugins = []
+ klass = obj.__class__
+ klass_func = getattr(klass, method_name)
+ base_func = getattr(base_class, method_name)
+
+ # Python 2/3 compatibility: Python 2 returns an instancemethod object, the
+ # function is the .im_func attribute. Python 3 returns a plain function
+ # object already.
+ if sys.version_info < (3, 0):
+ klass_func = klass_func.im_func
+ base_func = base_func.im_func
- for module in modules:
- __import__(module)
- mod = sys.modules[module]
+ return klass_func is not base_func
- plugin_class = getattr(mod, "Plugin", None)
- if plugin_class:
- options = config.get_plugin_options(module)
- plugin = plugin_class(options)
- plugin.__name__ = module
- plugins.append(plugin)
- return plugins
+def plugin_implements(obj, method_name):
+ """Does the plugin `obj` implement `method_name`?"""
+ return overrides(obj, method_name, CoveragePlugin)
diff --git a/coverage/report.py b/coverage/report.py
index c91c6e6d..b93749c8 100644
--- a/coverage/report.py
+++ b/coverage/report.py
@@ -33,8 +33,8 @@ class Reporter(object):
"""
morfs = morfs or self.coverage.data.measured_files()
file_locator = self.coverage.file_locator
- get_ext = self.coverage.data.plugin_data().get
- self.code_units = code_unit_factory(morfs, file_locator, get_ext)
+ get_plugin = self.coverage.data.plugin_data().get
+ self.code_units = code_unit_factory(morfs, file_locator, get_plugin)
if self.config.include:
patterns = prep_patterns(self.config.include)
diff --git a/coverage/tracer.c b/coverage/tracer.c
index c0066046..5370fef1 100644
--- a/coverage/tracer.c
+++ b/coverage/tracer.c
@@ -67,6 +67,7 @@ typedef struct {
PyObject * should_trace;
PyObject * warn;
PyObject * data;
+ PyObject * plugin_data;
PyObject * should_trace_cache;
PyObject * arcs;
@@ -139,6 +140,7 @@ CTracer_init(CTracer *self, PyObject *args_unused, PyObject *kwds_unused)
self->should_trace = NULL;
self->warn = NULL;
self->data = NULL;
+ self->plugin_data = NULL;
self->should_trace_cache = NULL;
self->arcs = NULL;
@@ -172,6 +174,7 @@ CTracer_dealloc(CTracer *self)
Py_XDECREF(self->should_trace);
Py_XDECREF(self->warn);
Py_XDECREF(self->data);
+ Py_XDECREF(self->plugin_data);
Py_XDECREF(self->should_trace_cache);
PyMem_Free(self->data_stack);
@@ -383,6 +386,9 @@ 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_plugin_name = NULL;
+
if (file_data == NULL) {
file_data = PyDict_New();
if (file_data == NULL) {
@@ -399,6 +405,34 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse
Py_DECREF(disposition);
return RET_ERROR;
}
+
+ if (self->plugin_data != NULL) {
+ /* If the disposition mentions a plugin, record that. */
+ disp_plugin = PyObject_GetAttrString(disposition, "plugin");
+ if (disp_plugin == 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_plugin_name == NULL) {
+ STATS( self->stats.errors++; )
+ Py_DECREF(tracename);
+ Py_DECREF(disposition);
+ return RET_ERROR;
+ }
+ ret = PyDict_SetItem(self->plugin_data, tracename, disp_plugin_name);
+ Py_DECREF(disp_plugin_name);
+ if (ret < 0) {
+ Py_DECREF(tracename);
+ Py_DECREF(disposition);
+ return RET_ERROR;
+ }
+ }
+ }
}
self->cur_file_data = file_data;
/* Make the frame right in case settrace(gettrace()) happens. */
@@ -629,6 +663,9 @@ CTracer_members[] = {
{ "data", T_OBJECT, offsetof(CTracer, data), 0,
PyDoc_STR("The raw dictionary of trace data.") },
+ { "plugin_data", T_OBJECT, offsetof(CTracer, plugin_data), 0,
+ PyDoc_STR("Mapping from filename to plugin name.") },
+
{ "should_trace_cache", T_OBJECT, offsetof(CTracer, should_trace_cache), 0,
PyDoc_STR("Dictionary caching should_trace results.") },
diff --git a/tests/test_plugins.py b/tests/test_plugins.py
index 154f1a7d..d60ce77b 100644
--- a/tests/test_plugins.py
+++ b/tests/test_plugins.py
@@ -1,7 +1,11 @@
"""Tests for plugins."""
+import os.path
+
import coverage
-from coverage.plugin import load_plugins
+from coverage.codeunit import CodeUnit
+from coverage.parser import CodeParser
+from coverage.plugin import Plugins, overrides
from tests.coveragetest import CoverageTest
@@ -24,7 +28,7 @@ class FakeConfig(object):
class PluginUnitTest(CoverageTest):
- """Test load_plugins directly."""
+ """Test Plugins.load_plugins directly."""
def test_importing_and_configuring(self):
self.make_file("plugin1.py", """\
@@ -37,7 +41,7 @@ class PluginUnitTest(CoverageTest):
""")
config = FakeConfig("plugin1", {'a':'hello'})
- plugins = load_plugins(["plugin1"], config)
+ plugins = list(Plugins.load_plugins(["plugin1"], config))
self.assertEqual(len(plugins), 1)
self.assertEqual(plugins[0].this_is, "me")
@@ -61,7 +65,7 @@ class PluginUnitTest(CoverageTest):
""")
config = FakeConfig("plugin1", {'a':'hello'})
- plugins = load_plugins(["plugin1", "plugin2"], config)
+ plugins = list(Plugins.load_plugins(["plugin1", "plugin2"], config))
self.assertEqual(len(plugins), 2)
self.assertEqual(plugins[0].this_is, "me")
@@ -71,7 +75,7 @@ class PluginUnitTest(CoverageTest):
def test_cant_import(self):
with self.assertRaises(ImportError):
- _ = load_plugins(["plugin_not_there"], None)
+ _ = Plugins.load_plugins(["plugin_not_there"], None)
def test_ok_to_not_define_plugin(self):
self.make_file("plugin2.py", """\
@@ -79,7 +83,7 @@ class PluginUnitTest(CoverageTest):
Nothing = 0
""")
- plugins = load_plugins(["plugin2"], None)
+ plugins = list(Plugins.load_plugins(["plugin2"], None))
self.assertEqual(plugins, [])
@@ -87,6 +91,7 @@ class PluginTest(CoverageTest):
"""Test plugins through the Coverage class."""
def test_plugin_imported(self):
+ # Prove that a plugin will be imported.
self.make_file("my_plugin.py", """\
with open("evidence.out", "w") as f:
f.write("we are here!")
@@ -98,7 +103,115 @@ class PluginTest(CoverageTest):
with open("evidence.out") as f:
self.assertEqual(f.read(), "we are here!")
- def test_bad_plugin_raises_import_error(self):
+ def test_missing_plugin_raises_import_error(self):
+ # Prove that a missing plugin will raise an ImportError.
with self.assertRaises(ImportError):
cov = coverage.Coverage(plugins=["foo"])
cov.start()
+
+ def test_bad_plugin_isnt_hidden(self):
+ # Prove that a plugin with an error in it will raise the error.
+ self.make_file("plugin_over_zero.py", """\
+ 1/0
+ """)
+ with self.assertRaises(ZeroDivisionError):
+ _ = coverage.Coverage(plugins=["plugin_over_zero"])
+
+ def test_importing_myself(self):
+ self.make_file("simple.py", """\
+ import try_xyz
+ a = 1
+ b = 2
+ """)
+ self.make_file("try_xyz.py", """\
+ c = 3
+ d = 4
+ """)
+
+ cov = coverage.Coverage(plugins=["tests.test_plugins"])
+
+ # Import the python file, executing it.
+ self.start_import_stop(cov, "simple")
+
+ _, statements, missing, _ = cov.analysis("simple.py")
+ self.assertEqual(statements, [1,2,3])
+ self.assertEqual(missing, [])
+ _, statements, _, _ = cov.analysis("/src/try_ABC.zz")
+ self.assertEqual(statements, [105, 106, 107, 205, 206, 207])
+
+
+class Plugin(coverage.CoveragePlugin):
+ def trace_judge(self, disp):
+ if "xyz.py" in disp.original_filename:
+ disp.trace = True
+ disp.source_filename = os.path.join(
+ "/src",
+ os.path.basename(
+ disp.original_filename.replace("xyz.py", "ABC.zz")
+ )
+ )
+
+ def line_number_range(self, frame):
+ lineno = frame.f_lineno
+ return lineno*100+5, lineno*100+7
+
+ def code_unit_class(self, filename):
+ return PluginCodeUnit
+
+class PluginCodeUnit(CodeUnit):
+ def get_parser(self, exclude=None):
+ return PluginParser()
+
+class PluginParser(CodeParser):
+ def parse_source(self):
+ return set([105, 106, 107, 205, 206, 207]), set([])
+
+
+class OverridesTest(CoverageTest):
+ """Test plugins.py:overrides."""
+
+ run_in_temp_dir = False
+
+ def test_overrides(self):
+ class SomeBase(object):
+ """Base class, two base methods."""
+ def method1(self):
+ pass
+
+ def method2(self):
+ pass
+
+ class Derived1(SomeBase):
+ """Simple single inheritance."""
+ def method1(self):
+ pass
+
+ self.assertTrue(overrides(Derived1(), "method1", SomeBase))
+ self.assertFalse(overrides(Derived1(), "method2", SomeBase))
+
+ class FurtherDerived1(Derived1):
+ """Derive again from Derived1, inherit its method1."""
+ pass
+
+ self.assertTrue(overrides(FurtherDerived1(), "method1", SomeBase))
+ self.assertFalse(overrides(FurtherDerived1(), "method2", SomeBase))
+
+ class FurtherDerived2(Derived1):
+ """Override the overridden method."""
+ def method1(self):
+ pass
+
+ self.assertTrue(overrides(FurtherDerived2(), "method1", SomeBase))
+ self.assertFalse(overrides(FurtherDerived2(), "method2", SomeBase))
+
+ class Mixin(object):
+ """A mixin that overrides method1."""
+ def method1(self):
+ pass
+
+ class Derived2(Mixin, SomeBase):
+ """A class that gets the method from the mixin."""
+ pass
+
+ self.assertTrue(overrides(Derived2(), "method1", SomeBase))
+ self.assertFalse(overrides(Derived2(), "method2", SomeBase))