summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt3
-rw-r--r--coverage/control.py23
-rw-r--r--coverage/misc.py23
-rw-r--r--coverage/plugin_support.py70
-rw-r--r--tests/modules/plugins/a_plugin.py3
-rw-r--r--tests/modules/plugins/another.py3
-rw-r--r--tests/plugin1.py5
-rw-r--r--tests/plugin2.py9
-rw-r--r--tests/test_misc.py51
-rw-r--r--tests/test_plugins.py86
10 files changed, 154 insertions, 122 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index ed208e2f..d6c8498f 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -10,6 +10,9 @@ Latest
collected, exiting with a status code of 1. Fixed ``fail_under`` to be
applied even when the report is empty. Thanks, Ionel Cristian Mărieș.
+- Plugins are now initialized differently. Instead of looking for a class
+ called ``Plugin``, coverage looks for a function called ``coverage_init``.
+
Version 4.0a6 --- 21 June 2015
------------------------------
diff --git a/coverage/control.py b/coverage/control.py
index e4e67d3a..1dbf0672 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -21,9 +21,9 @@ from coverage.files import PathAliases, find_python_files, prep_patterns
from coverage.files import ModuleMatcher, abs_file
from coverage.html import HtmlReporter
from coverage.misc import CoverageException, bool_or_none, join_regex
-from coverage.misc import file_be_gone, overrides
+from coverage.misc import file_be_gone
from coverage.monkey import patch_multiprocessing
-from coverage.plugin import CoveragePlugin, FileReporter
+from coverage.plugin import FileReporter
from coverage.plugin_support import Plugins
from coverage.python import PythonFileReporter
from coverage.results import Analysis, Numbers
@@ -172,7 +172,7 @@ class Coverage(object):
self.omit = self.include = self.source = None
self.source_pkgs = None
self.data = self.collector = None
- self.plugins = self.file_tracing_plugins = None
+ self.plugins = None
self.pylib_dirs = self.cover_dirs = None
self.data_suffix = self.run_suffix = None
self._exclude_re = None
@@ -207,11 +207,6 @@ class Coverage(object):
# Load plugins
self.plugins = Plugins.load_plugins(self.config.plugins, self.config, self.debug)
- self.file_tracing_plugins = []
- for plugin in self.plugins:
- if overrides(plugin, "file_tracer", CoveragePlugin):
- self.file_tracing_plugins.append(plugin)
-
# _exclude_re is a dict that maps exclusion list names to compiled
# regexes.
self._exclude_re = {}
@@ -246,17 +241,17 @@ class Coverage(object):
)
# Early warning if we aren't going to be able to support plugins.
- if self.file_tracing_plugins and not self.collector.supports_plugins:
+ if self.plugins.file_tracers and not self.collector.supports_plugins:
self._warn(
"Plugin file tracers (%s) aren't supported with %s" % (
", ".join(
plugin._coverage_plugin_name
- for plugin in self.file_tracing_plugins
+ for plugin in self.plugins.file_tracers
),
self.collector.tracer_name(),
)
)
- for plugin in self.file_tracing_plugins:
+ for plugin in self.plugins.file_tracers:
plugin._coverage_enabled = False
# Suffixes are a bit tricky. We want to use the data suffix only when
@@ -482,7 +477,7 @@ class Coverage(object):
# Try the plugins, see if they have an opinion about the file.
plugin = None
- for plugin in self.file_tracing_plugins:
+ for plugin in self.plugins.file_tracers:
if not plugin._coverage_enabled:
continue
@@ -1037,7 +1032,7 @@ class Coverage(object):
implementation = "unknown"
ft_plugins = []
- for ft in self.file_tracing_plugins:
+ for ft in self.plugins.file_tracers:
ft_name = ft._coverage_plugin_name
if not ft._coverage_enabled:
ft_name += " (disabled)"
@@ -1049,7 +1044,7 @@ class Coverage(object):
('cover_dirs', self.cover_dirs),
('pylib_dirs', self.pylib_dirs),
('tracer', self.collector.tracer_name()),
- ('file_tracing_plugins', ft_plugins),
+ ('plugins.file_tracers', ft_plugins),
('config_files', self.config.attempted_config_files),
('configs_read', self.config.config_files),
('data_path', self.data.filename),
diff --git a/coverage/misc.py b/coverage/misc.py
index 0dad0559..21b7333c 100644
--- a/coverage/misc.py
+++ b/coverage/misc.py
@@ -158,29 +158,6 @@ class Hasher(object):
return self.md5.hexdigest()
-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.
-
- """
- 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 env.PY2:
- klass_func = klass_func.im_func
- base_func = base_func.im_func
-
- return klass_func is not base_func
-
-
# TODO: abc?
def _needs_to_implement(that, func_name):
"""Helper to raise NotImplementedError in interface stubs."""
diff --git a/coverage/plugin_support.py b/coverage/plugin_support.py
index 4f22c137..ae72f797 100644
--- a/coverage/plugin_support.py
+++ b/coverage/plugin_support.py
@@ -13,6 +13,10 @@ class Plugins(object):
def __init__(self):
self.order = []
self.names = {}
+ self.file_tracers = []
+
+ self.current_module = None
+ self.debug = None
@classmethod
def load_plugins(cls, modules, config, debug=None):
@@ -22,30 +26,68 @@ class Plugins(object):
"""
plugins = cls()
+ plugins.debug = debug
for module in modules:
+ plugins.current_module = module
__import__(module)
mod = sys.modules[module]
- plugin_class = getattr(mod, "Plugin", None)
- if not plugin_class:
- raise CoverageException("Plugin module %r didn't define a Plugin class" % module)
+ coverage_init = getattr(mod, "coverage_init", None)
+ if not coverage_init:
+ raise CoverageException(
+ "Plugin module %r didn't define a coverage_init function" % module
+ )
options = config.get_plugin_options(module)
- plugin = plugin_class(options)
- if debug and debug.should('plugin'):
- debug.write("Loaded plugin %r: %r" % (module, plugin))
- labelled = LabelledDebug("plugin %r" % (module,), debug)
- plugin = DebugPluginWrapper(plugin, labelled)
-
- # pylint: disable=attribute-defined-outside-init
- plugin._coverage_plugin_name = module
- plugin._coverage_enabled = True
- plugins.order.append(plugin)
- plugins.names[module] = plugin
+ coverage_init(plugins, options)
+ plugins.current_module = None
return plugins
+ def add_file_tracer(self, plugin):
+ """Add a file tracer plugin.
+
+ ``plugin`` must implement the :meth:`CoveragePlugin.file_tracer` method.
+
+ """
+ self._add_plugin(plugin, self.file_tracers)
+
+ def add_noop(self, plugin):
+ """Add a plugin that does nothing.
+
+ This is only useful for testing the plugin support.
+
+ """
+ self._add_plugin(plugin, None)
+
+ def _add_plugin(self, plugin, specialized):
+ """Add a plugin object.
+
+ Arguments:
+ plugin (CoveragePlugin): the plugin to add.
+
+ specialized (list): the list of plugins to add this to.
+
+ Returns:
+ plugin: may be a different object than passed in.
+
+ """
+ plugin_name = "%s.%s" % (self.current_module, plugin.__class__.__name__)
+ if self.debug and self.debug.should('plugin'):
+ self.debug.write("Loaded plugin %r: %r" % (self.current_module, plugin))
+ labelled = LabelledDebug("plugin %r" % (self.current_module,), self.debug)
+ plugin = DebugPluginWrapper(plugin, labelled)
+
+ # pylint: disable=attribute-defined-outside-init
+ plugin._coverage_plugin_name = plugin_name
+ plugin._coverage_enabled = True
+ self.order.append(plugin)
+ self.names[plugin_name] = plugin
+ if specialized is not None:
+ specialized.append(plugin)
+ return plugin
+
def __nonzero__(self):
return bool(self.order)
diff --git a/tests/modules/plugins/a_plugin.py b/tests/modules/plugins/a_plugin.py
index 2ff84dac..65627515 100644
--- a/tests/modules/plugins/a_plugin.py
+++ b/tests/modules/plugins/a_plugin.py
@@ -4,3 +4,6 @@ from coverage import CoveragePlugin
class Plugin(CoveragePlugin):
pass
+
+def coverage_init(reg, options):
+ reg.add_file_tracer(Plugin(options))
diff --git a/tests/modules/plugins/another.py b/tests/modules/plugins/another.py
index 2ff84dac..65627515 100644
--- a/tests/modules/plugins/another.py
+++ b/tests/modules/plugins/another.py
@@ -4,3 +4,6 @@ from coverage import CoveragePlugin
class Plugin(CoveragePlugin):
pass
+
+def coverage_init(reg, options):
+ reg.add_file_tracer(Plugin(options))
diff --git a/tests/plugin1.py b/tests/plugin1.py
index f9da35c8..5d3db856 100644
--- a/tests/plugin1.py
+++ b/tests/plugin1.py
@@ -44,3 +44,8 @@ class FileReporter(coverage.plugin.FileReporter):
def excluded_statements(self):
return set([])
+
+
+def coverage_init(reg, options):
+ """Called by coverage to initialize the plugins here."""
+ reg.add_file_tracer(Plugin(options))
diff --git a/tests/plugin2.py b/tests/plugin2.py
index 70d25249..9c3cb1d4 100644
--- a/tests/plugin2.py
+++ b/tests/plugin2.py
@@ -4,10 +4,9 @@ import os.path
import coverage
-# pylint: disable=missing-docstring
-
class Plugin(coverage.CoveragePlugin):
+ """A plugin for testing."""
def file_tracer(self, filename):
if "render.py" in filename:
return RenderFileTracer()
@@ -34,8 +33,14 @@ class RenderFileTracer(coverage.plugin.FileTracer):
class FileReporter(coverage.plugin.FileReporter):
+ """A goofy file reporter."""
def statements(self):
# Goofy test arrangement: claim that the file has as many lines as the
# number in its name.
num = os.path.basename(self.filename).split(".")[0].split("_")[1]
return set(range(1, int(num)+1))
+
+
+def coverage_init(reg, options):
+ """Called by coverage to initialize the plugins here."""
+ reg.add_file_tracer(Plugin(options))
diff --git a/tests/test_misc.py b/tests/test_misc.py
index 152207b5..c5c03b71 100644
--- a/tests/test_misc.py
+++ b/tests/test_misc.py
@@ -2,7 +2,7 @@
import sys
-from coverage.misc import Hasher, file_be_gone, overrides
+from coverage.misc import Hasher, file_be_gone
from coverage import __version__, __url__
from tests.coveragetest import CoverageTest
@@ -87,52 +87,3 @@ class SetupPyTest(CoverageTest):
self.assertGreater(len(long_description), 7)
self.assertNotEqual(long_description[0].strip(), "")
self.assertNotEqual(long_description[-1].strip(), "")
-
-
-class OverridesTest(CoverageTest):
- """Test plugins.py:overrides."""
-
- run_in_temp_dir = False
-
- def test_overrides(self):
- # pylint: disable=missing-docstring
- class SomeBase(object):
- def method1(self):
- pass
-
- def method2(self):
- pass
-
- class Derived1(SomeBase):
- 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))
diff --git a/tests/test_plugins.py b/tests/test_plugins.py
index 1ffb73bf..6e43ff14 100644
--- a/tests/test_plugins.py
+++ b/tests/test_plugins.py
@@ -40,6 +40,9 @@ class LoadPluginsTest(CoverageTest):
class Plugin(CoveragePlugin):
pass
+
+ def coverage_init(reg, options):
+ reg.add_file_tracer(Plugin(options))
""")
config = FakeConfig("plugin1", {})
@@ -57,6 +60,9 @@ class LoadPluginsTest(CoverageTest):
def __init__(self, options):
super(Plugin, self).__init__(options)
self.this_is = "me"
+
+ def coverage_init(reg, options):
+ reg.add_file_tracer(Plugin(options))
""")
config = FakeConfig("plugin1", {'a': 'hello'})
@@ -75,12 +81,18 @@ class LoadPluginsTest(CoverageTest):
def __init__(self, options):
super(Plugin, self).__init__(options)
self.this_is = "me"
+
+ def coverage_init(reg, options):
+ reg.add_file_tracer(Plugin(options))
""")
self.make_file("plugin2.py", """\
from coverage import CoveragePlugin
class Plugin(CoveragePlugin):
pass
+
+ def coverage_init(reg, options):
+ reg.add_file_tracer(Plugin(options))
""")
config = FakeConfig("plugin1", {'a': 'hello'})
@@ -105,12 +117,12 @@ class LoadPluginsTest(CoverageTest):
with self.assertRaises(ImportError):
_ = Plugins.load_plugins(["plugin_not_there"], None)
- def test_plugin_must_define_plugin_class(self):
+ def test_plugin_must_define_coverage_init(self):
self.make_file("no_plugin.py", """\
from coverage import CoveragePlugin
Nothing = 0
""")
- msg_pat = "Plugin module 'no_plugin' didn't define a Plugin class"
+ msg_pat = "Plugin module 'no_plugin' didn't define a coverage_init function"
with self.assertRaisesRegex(CoverageException, msg_pat):
list(Plugins.load_plugins(["no_plugin"], None))
@@ -124,6 +136,8 @@ class PluginTest(CoverageTest):
from coverage import CoveragePlugin
class Plugin(CoveragePlugin):
pass
+ def coverage_init(reg, options):
+ reg.add_noop(Plugin(options))
with open("evidence.out", "w") as f:
f.write("we are here!")
""")
@@ -163,6 +177,9 @@ class PluginTest(CoverageTest):
class Plugin(coverage.CoveragePlugin):
def sys_info(self):
return [("hello", "world")]
+
+ def coverage_init(reg, options):
+ reg.add_noop(Plugin(options))
""")
debug_out = StringIO()
cov = coverage.Coverage(debug=["sys"])
@@ -172,7 +189,7 @@ class PluginTest(CoverageTest):
out_lines = debug_out.getvalue().splitlines()
expected_end = [
- "-- sys: plugin_sys_info --------------------------------------",
+ "-- sys: plugin_sys_info.Plugin -------------------------------",
" hello: world",
"-- end -------------------------------------------------------",
]
@@ -184,6 +201,9 @@ class PluginTest(CoverageTest):
class Plugin(coverage.CoveragePlugin):
pass
+
+ def coverage_init(reg, options):
+ reg.add_noop(Plugin(options))
""")
debug_out = StringIO()
cov = coverage.Coverage(debug=["sys"])
@@ -193,7 +213,7 @@ class PluginTest(CoverageTest):
out_lines = debug_out.getvalue().splitlines()
expected_end = [
- "-- sys: plugin_no_sys_info -----------------------------------",
+ "-- sys: plugin_no_sys_info.Plugin ----------------------------",
"-- end -------------------------------------------------------",
]
self.assertEqual(expected_end, out_lines[-len(expected_end):])
@@ -202,8 +222,10 @@ class PluginTest(CoverageTest):
self.make_file("importing_plugin.py", """\
from coverage import CoveragePlugin
import local_module
- class Plugin(CoveragePlugin):
+ class MyPlugin(CoveragePlugin):
pass
+ def coverage_init(reg, options):
+ reg.add_noop(MyPlugin(options))
""")
self.make_file("local_module.py", "CONST = 1")
self.make_file(".coveragerc", """\
@@ -237,8 +259,7 @@ class PluginWarningOnPyTracer(CoverageTest):
self.start_import_stop(cov, "simple")
self.assertIn(
- "Plugin file tracers (tests.plugin1) "
- "aren't supported with PyTracer",
+ "Plugin file tracers (tests.plugin1.Plugin) aren't supported with PyTracer",
warnings
)
@@ -441,7 +462,7 @@ class GoodPluginTest(FileTracerTest):
class BadPluginTest(FileTracerTest):
"""Test error handling around plugins."""
- def run_bad_plugin(self, plugin_name, our_error=True):
+ def run_bad_plugin(self, module_name, plugin_name, our_error=True):
"""Run a file, and see that the plugin failed.
`plugin_name` is the name of the plugin to use.
@@ -469,7 +490,7 @@ class BadPluginTest(FileTracerTest):
""")
cov = coverage.Coverage()
- cov.config["run:plugins"] = [plugin_name]
+ cov.config["run:plugins"] = [module_name]
self.start_import_stop(cov, "simple")
stderr = self.stderr()
@@ -485,7 +506,7 @@ class BadPluginTest(FileTracerTest):
# Disabling plugin '...' due to previous exception
# or:
# Disabling plugin '...' due to an excepton:
- msg = "Disabling plugin %r due to " % plugin_name
+ msg = "Disabling plugin '%s.%s' due to " % (module_name, plugin_name)
warnings = stderr.count(msg)
self.assertEqual(warnings, 1)
@@ -495,8 +516,11 @@ class BadPluginTest(FileTracerTest):
class Plugin(coverage.plugin.CoveragePlugin):
def file_tracer(self, filename):
17/0 # Oh noes!
+
+ def coverage_init(reg, options):
+ reg.add_file_tracer(Plugin(options))
""")
- self.run_bad_plugin("bad_plugin")
+ self.run_bad_plugin("bad_plugin", "Plugin")
def test_file_tracer_returns_wrong(self):
self.make_file("bad_plugin.py", """\
@@ -504,8 +528,11 @@ class BadPluginTest(FileTracerTest):
class Plugin(coverage.plugin.CoveragePlugin):
def file_tracer(self, filename):
return 3.14159
+
+ def coverage_init(reg, options):
+ reg.add_file_tracer(Plugin(options))
""")
- self.run_bad_plugin("bad_plugin", our_error=False)
+ self.run_bad_plugin("bad_plugin", "Plugin", our_error=False)
def test_has_dynamic_source_filename_fails(self):
self.make_file("bad_plugin.py", """\
@@ -517,8 +544,11 @@ class BadPluginTest(FileTracerTest):
class BadFileTracer(coverage.plugin.FileTracer):
def has_dynamic_source_filename(self):
23/0 # Oh noes!
+
+ def coverage_init(reg, options):
+ reg.add_file_tracer(Plugin(options))
""")
- self.run_bad_plugin("bad_plugin")
+ self.run_bad_plugin("bad_plugin", "Plugin")
def test_source_filename_fails(self):
self.make_file("bad_plugin.py", """\
@@ -530,8 +560,11 @@ class BadPluginTest(FileTracerTest):
class BadFileTracer(coverage.plugin.FileTracer):
def source_filename(self):
42/0 # Oh noes!
+
+ def coverage_init(reg, options):
+ reg.add_file_tracer(Plugin(options))
""")
- self.run_bad_plugin("bad_plugin")
+ self.run_bad_plugin("bad_plugin", "Plugin")
def test_source_filename_returns_wrong(self):
self.make_file("bad_plugin.py", """\
@@ -543,8 +576,11 @@ class BadPluginTest(FileTracerTest):
class BadFileTracer(coverage.plugin.FileTracer):
def source_filename(self):
return 17.3
+
+ def coverage_init(reg, options):
+ reg.add_file_tracer(Plugin(options))
""")
- self.run_bad_plugin("bad_plugin", our_error=False)
+ self.run_bad_plugin("bad_plugin", "Plugin", our_error=False)
def test_dynamic_source_filename_fails(self):
self.make_file("bad_plugin.py", """\
@@ -559,8 +595,11 @@ class BadPluginTest(FileTracerTest):
return True
def dynamic_source_filename(self, filename, frame):
101/0 # Oh noes!
+
+ def coverage_init(reg, options):
+ reg.add_file_tracer(Plugin(options))
""")
- self.run_bad_plugin("bad_plugin")
+ self.run_bad_plugin("bad_plugin", "Plugin")
def test_line_number_range_returns_non_tuple(self):
self.make_file("bad_plugin.py", """\
@@ -576,8 +615,11 @@ class BadPluginTest(FileTracerTest):
def line_number_range(self, frame):
return 42.23
+
+ def coverage_init(reg, options):
+ reg.add_file_tracer(Plugin(options))
""")
- self.run_bad_plugin("bad_plugin", our_error=False)
+ self.run_bad_plugin("bad_plugin", "Plugin", our_error=False)
def test_line_number_range_returns_triple(self):
self.make_file("bad_plugin.py", """\
@@ -593,8 +635,11 @@ class BadPluginTest(FileTracerTest):
def line_number_range(self, frame):
return (1, 2, 3)
+
+ def coverage_init(reg, options):
+ reg.add_file_tracer(Plugin(options))
""")
- self.run_bad_plugin("bad_plugin", our_error=False)
+ self.run_bad_plugin("bad_plugin", "Plugin", our_error=False)
def test_line_number_range_returns_pair_of_strings(self):
self.make_file("bad_plugin.py", """\
@@ -610,5 +655,8 @@ class BadPluginTest(FileTracerTest):
def line_number_range(self, frame):
return ("5", "7")
+
+ def coverage_init(reg, options):
+ reg.add_file_tracer(Plugin(options))
""")
- self.run_bad_plugin("bad_plugin", our_error=False)
+ self.run_bad_plugin("bad_plugin", "Plugin", our_error=False)