diff options
author | Justas Sadzevičius <justas.sadzevicius@gmail.com> | 2019-04-21 06:02:09 +0300 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2019-04-20 23:02:09 -0400 |
commit | 4a5ad41f106d6d9fff97e10cae3571b7b67823d5 (patch) | |
tree | 1c1555ea6cb2c86f15dc528ce65d0e34e62e4e32 /coverage | |
parent | a92f0023e015a82f8c875e7a90210e22aaf0174b (diff) | |
download | python-coveragepy-git-4a5ad41f106d6d9fff97e10cae3571b7b67823d5.tar.gz |
Plugin support for dynamic context (#783)
* Introduce a new plugin type: dynamic context labels.
* Test dynamic context plugins
* Helper method to get full paths to measured files
* Get correct filenames on all OS
* Improve wording
Diffstat (limited to 'coverage')
-rw-r--r-- | coverage/control.py | 39 | ||||
-rw-r--r-- | coverage/plugin.py | 31 | ||||
-rw-r--r-- | coverage/plugin_support.py | 10 |
3 files changed, 78 insertions, 2 deletions
diff --git a/coverage/control.py b/coverage/control.py index ea6698d4..ae5f0442 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -335,6 +335,34 @@ class Coverage(object): if not should_skip: self._data.read() + def _combine_context_switchers(self, context_switchers): + """Create a single context switcher from multiple switchers. + + `context_switchers` is a list of methods that take a frame + as an argument and return a string to use as the new context label. + + Returns a method that composits `context_switchers` methods, or None + if `context_switchers` is an empty list. + + When invoked, the combined switcher calls `context_switchers` one-by-one + until a string is returned. Combined switcher returns None if all + `context_switchers` return None. + """ + if not context_switchers: + return None + + if len(context_switchers) == 1: + return context_switchers[0] + + def should_start_context(frame): + for switcher in context_switchers: + new_context = switcher(frame) + if new_context is not None: + return new_context + return None + + return should_start_context + def _init_for_start(self): """Initialization for start()""" # Construct the collector. @@ -350,14 +378,21 @@ class Coverage(object): self.config.parallel = True if self.config.dynamic_context is None: - should_start_context = None + context_switchers = [] elif self.config.dynamic_context == "test_function": - should_start_context = should_start_context_test_function + context_switchers = [should_start_context_test_function] else: raise CoverageException( "Don't understand dynamic_context setting: {!r}".format(self.config.dynamic_context) ) + context_switchers.extend([ + plugin.dynamic_context + for plugin in self._plugins.context_switchers + ]) + + should_start_context = self._combine_context_switchers(context_switchers) + self._collector = Collector( should_trace=self._should_trace, check_include=self._check_include_omit_etc, diff --git a/coverage/plugin.py b/coverage/plugin.py index f65d419c..e817cf2f 100644 --- a/coverage/plugin.py +++ b/coverage/plugin.py @@ -78,6 +78,26 @@ change the configuration. In your ``coverage_init`` function, use the ``add_configurer`` method to register your configurer. +Dynamic Contexts +================ + +.. versionadded:: 5.0 + +Context plugins implement the :meth:`~coverage.CoveragePlugin.dynamic_context` method +to dynamically compute the context label for each measured frame. + +Computed context labels are useful when you want to group measured data without +modifying the source code. + +For example, you could write a plugin that check `frame.f_code` to inspect +the currently executed method, and set label to a fully qualified method +name if it's an instance method of `unittest.TestCase` and the method name +starts with 'test'. Such plugin would provide basic coverage grouping by test +and could be used with test runners that have no built-in coveragepy support. + +In your ``coverage_init`` function, use the ``add_dynamic_context`` method to +register your file tracer. + """ from coverage import files @@ -140,6 +160,17 @@ class CoveragePlugin(object): """ _needs_to_implement(self, "file_reporter") + def dynamic_context(self, frame): # pylint: disable=unused-argument + """Get dynamically computed context label for collected data. + + Plug-in type: dynamic context. + + This method is invoked for each frame. If it returns a string, + a new context label is set for this and deeper frames. + + """ + return None + def find_executable_files(self, src_dir): # pylint: disable=unused-argument """Yield all of the executable files in `src_dir`, recursively. diff --git a/coverage/plugin_support.py b/coverage/plugin_support.py index 0727a3b0..7c25a5f1 100644 --- a/coverage/plugin_support.py +++ b/coverage/plugin_support.py @@ -21,6 +21,7 @@ class Plugins(object): self.names = {} self.file_tracers = [] self.configurers = [] + self.context_switchers = [] self.current_module = None self.debug = None @@ -70,6 +71,15 @@ class Plugins(object): """ self._add_plugin(plugin, self.configurers) + def add_dynamic_context(self, plugin): + """Add a dynamic context plugin. + + `plugin` is an instance of a third-party plugin class. It must + implement the :meth:`CoveragePlugin.dynamic_context` method. + + """ + self._add_plugin(plugin, self.context_switchers) + def add_noop(self, plugin): """Add a plugin that does nothing. |