diff options
author | Ned Batchelder <ned@nedbatchelder.com> | 2014-09-14 16:44:21 -0400 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2014-09-14 16:44:21 -0400 |
commit | 941eeb588459098a85266b4c5d176d6deed2eb53 (patch) | |
tree | 49d410b2b46d5d7a56faee81e6ed4a8fdcdd47e3 | |
parent | 0891e02eb490494ee40a7f840e4ab9fd6b3d2d7b (diff) | |
download | python-coveragepy-git-941eeb588459098a85266b4c5d176d6deed2eb53.tar.gz |
Progress on plugins
-rw-r--r-- | coverage/__init__.py | 2 | ||||
-rw-r--r-- | coverage/codeunit.py | 22 | ||||
-rw-r--r-- | coverage/control.py | 68 | ||||
-rw-r--r-- | coverage/plugin.py | 84 | ||||
-rw-r--r-- | coverage/report.py | 4 | ||||
-rw-r--r-- | coverage/tracer.c | 37 | ||||
-rw-r--r-- | tests/test_plugins.py | 127 |
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)) |