diff options
-rw-r--r-- | CHANGES.txt | 3 | ||||
-rw-r--r-- | coverage/control.py | 23 | ||||
-rw-r--r-- | coverage/misc.py | 23 | ||||
-rw-r--r-- | coverage/plugin_support.py | 70 | ||||
-rw-r--r-- | tests/modules/plugins/a_plugin.py | 3 | ||||
-rw-r--r-- | tests/modules/plugins/another.py | 3 | ||||
-rw-r--r-- | tests/plugin1.py | 5 | ||||
-rw-r--r-- | tests/plugin2.py | 9 | ||||
-rw-r--r-- | tests/test_misc.py | 51 | ||||
-rw-r--r-- | tests/test_plugins.py | 86 |
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) |