summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2018-01-06 17:06:59 -0500
committerNed Batchelder <ned@nedbatchelder.com>2018-01-06 17:06:59 -0500
commitdb260dc3feb931947309e88fa93063a0a37b6eda (patch)
tree50867f9cd23e2c6196f145eca32e39cd0ffdbe19
parent66dc96690399b6db97c638fefa8dcff626844900 (diff)
downloadpython-coveragepy-db260dc3feb931947309e88fa93063a0a37b6eda.tar.gz
A new kind of plug-in: configurers. #563
-rw-r--r--CHANGES.rst9
-rw-r--r--coverage/control.py21
-rw-r--r--coverage/ctracer/tracer.c4
-rw-r--r--coverage/plugin.py168
-rw-r--r--coverage/plugin_support.py10
-rw-r--r--doc/api_plugin.rst9
-rw-r--r--doc/plugins.rst52
-rw-r--r--tests/plugin_config.py22
-rw-r--r--tests/test_plugins.py19
9 files changed, 216 insertions, 98 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index b14f1ed..9885167 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,10 +5,15 @@
Change history for Coverage.py
==============================
-Unreleases
+Unreleased
----------
-None yet.
+- A new kind of plugin is supported: configurators are invoked at start-up to
+ allow more complex configuration than the .coveragerc file can easily do.
+ See :ref:`api_plugin` for details. This solves the complex configuration
+ problem described in `issue 563`_.
+
+.. _issue 563: https://bitbucket.org/ned/coveragepy/issues/563/platform-specific-configuration
.. _changes_442:
diff --git a/coverage/control.py b/coverage/control.py
index fdcb544..f1db1ef 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -11,6 +11,7 @@ import os
import platform
import re
import sys
+import time
import traceback
from coverage import env
@@ -216,14 +217,22 @@ class Coverage(object):
self._debug_file = sys.stderr
self.debug = DebugControl(self.config.debug, self._debug_file)
- # Load plugins
- self.plugins = Plugins.load_plugins(self.config.plugins, self.config, self.debug)
-
# _exclude_re is a dict that maps exclusion list names to compiled regexes.
self._exclude_re = {}
set_relative_directory()
+ # Load plugins
+ self.plugins = Plugins.load_plugins(self.config.plugins, self.config, self.debug)
+
+ # Run configuring plugins.
+ for plugin in self.plugins.configurers:
+ # We need an object with set_option and get_option. Either self or
+ # self.config will do. Choosing randomly stops people from doing
+ # other things with those objects, against the public API. Yes,
+ # this is a bit childish. :)
+ plugin.configure([self, self.config][int(time.time()) % 2])
+
# The source argument can be directories or package names.
self.source = []
self.source_pkgs = []
@@ -507,7 +516,7 @@ class Coverage(object):
break
except Exception:
self._warn(
- "Disabling plugin %r due to an exception:" % (
+ "Disabling plug-in %r due to an exception:" % (
plugin._coverage_plugin_name
)
)
@@ -639,8 +648,8 @@ class Coverage(object):
option name. For example, the ``branch`` option in the ``[run]``
section of the config file would be indicated with ``"run:branch"``.
- `value` is the new value for the option. This should be a Python
- value where appropriate. For example, use True for booleans, not the
+ `value` is the new value for the option. This should be an
+ appropriate Python value. For example, use True for booleans, not the
string ``"True"``.
As an example, calling::
diff --git a/coverage/ctracer/tracer.c b/coverage/ctracer/tracer.c
index 0ade741..6dcdc57 100644
--- a/coverage/ctracer/tracer.c
+++ b/coverage/ctracer/tracer.c
@@ -596,7 +596,7 @@ CTracer_disable_plugin(CTracer *self, PyObject * disposition)
goto error;
}
msg = MyText_FromFormat(
- "Disabling plugin '%s' due to previous exception",
+ "Disabling plug-in '%s' due to previous exception",
MyText_AsString(plugin_name)
);
if (msg == NULL) {
@@ -622,7 +622,7 @@ error:
/* This function doesn't return a status, so if an error happens, print it,
* but don't interrupt the flow. */
/* PySys_WriteStderr is nicer, but is not in the public API. */
- fprintf(stderr, "Error occurred while disabling plugin:\n");
+ fprintf(stderr, "Error occurred while disabling plug-in:\n");
PyErr_Print();
ok:
diff --git a/coverage/plugin.py b/coverage/plugin.py
index 3e0e483..b11aa56 100644
--- a/coverage/plugin.py
+++ b/coverage/plugin.py
@@ -1,70 +1,105 @@
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
-"""Plugin interfaces for coverage.py"""
+"""
+Plug-in interfaces for coverage.py.
-from coverage import files
-from coverage.misc import contract, _needs_to_implement
+Coverage.py supports a few different kinds of plug-ins that change its
+behavior:
+* File tracers implement tracing of non-Python file types.
-class CoveragePlugin(object):
- """Base class for coverage.py plugins.
+* Configurers add custom configuration, using Python code to change the
+ configuration.
- To write a coverage.py plugin, create a module with a subclass of
- :class:`CoveragePlugin`. You will override methods in your class to
- participate in various aspects of coverage.py's processing.
+To write a coverage.py plug-in, create a module with a subclass of
+:class:`~coverage.CoveragePlugin`. You will override methods in your class to
+participate in various aspects of coverage.py's processing.
+Different types of plug-ins have to override different methods.
- Currently the only plugin type is a file tracer, for implementing
- measurement support for non-Python files. File tracer plugins implement
- the :meth:`file_tracer` method to claim files and the :meth:`file_reporter`
- method to report on those files.
+Any plug-in can optionally implement :meth:`~coverage.CoveragePlugin.sys_info`
+to provide debugging information about their operation.
- Any plugin can optionally implement :meth:`sys_info` to provide debugging
- information about their operation.
+Your module must also contain a ``coverage_init`` function that registers an
+instance of your plug-in class::
- Coverage.py will store its own information on your plugin object, using
- attributes whose names start with ``_coverage_``. Don't be startled.
+ import coverage
- To register your plugin, define a function called `coverage_init` in your
- module::
+ class MyPlugin(coverage.CoveragePlugin):
+ ...
- def coverage_init(reg, options):
- reg.add_file_tracer(MyPlugin())
+ def coverage_init(reg, options):
+ reg.add_file_tracer(MyPlugin())
- You use the `reg` parameter passed to your `coverage_init` function to
- register your plugin object. It has one method, `add_file_tracer`, which
- takes a newly created instance of your plugin.
+You use the `reg` parameter passed to your ``coverage_init`` function to
+register your plug-in object. The registration method you call depends on
+what kind of plug-in it is.
- If your plugin takes options, the `options` parameter is a dictionary of
- your plugin's options from the coverage.py configuration file. Use them
- however you want to configure your object before registering it.
+If your plug-in takes options, the `options` parameter is a dictionary of your
+plug-in's options from the coverage.py configuration file. Use them however
+you want to configure your object before registering it.
+
+Coverage.py will store its own information on your plug-in object, using
+attributes whose names start with ``_coverage_``. Don't be startled.
+
+
+File Tracers
+============
+
+File tracers implement measurement support for non-Python files. File tracers
+implement the :meth:`~coverage.CoveragePlugin.file_tracer` method to claim
+files and the :meth:`~coverage.CoveragePlugin.file_reporter` method to report
+on those files.
+
+In your ``coverage_init`` function, use the ``add_file_tracer`` method to
+register your file tracer.
+
+
+Configurers
+===========
+
+Configurers modify the configuration of coverage.py during start-up.
+Configurers implement the :meth:`~coverage.CoveragePlugin.configure` method to
+change the configuration.
+
+In your ``coverage_init`` function, use the ``add_configurer`` method to
+register your configurer.
+
+"""
+
+from coverage import files
+from coverage.misc import contract, _needs_to_implement
- """
+
+class CoveragePlugin(object):
+ """Base class for coverage.py plug-ins."""
def file_tracer(self, filename): # pylint: disable=unused-argument
"""Get a :class:`FileTracer` object for a file.
- Every Python source file is offered to the plugin to give it a chance
- to take responsibility for tracing the file. If your plugin can handle
- the file, then return a :class:`FileTracer` object. Otherwise return
- None.
+ Plug-in type: file tracer.
- There is no way to register your plugin for particular files. Instead,
- this method is invoked for all files, and the plugin decides whether it
- can trace the file or not. Be prepared for `filename` to refer to all
- kinds of files that have nothing to do with your plugin.
+ Every Python source file is offered to your plug-in to give it a chance
+ to take responsibility for tracing the file. If your plug-in can
+ handle the file, then return a :class:`FileTracer` object. Otherwise
+ return None.
+
+ There is no way to register your plug-in for particular files.
+ Instead, this method is invoked for all files, and the plug-in decides
+ whether it can trace the file or not. Be prepared for `filename` to
+ refer to all kinds of files that have nothing to do with your plug-in.
The file name will be a Python file being executed. There are two
- broad categories of behavior for a plugin, depending on the kind of
- files your plugin supports:
+ broad categories of behavior for a plug-in, depending on the kind of
+ files your plug-in supports:
* Static file names: each of your original source files has been
- converted into a distinct Python file. Your plugin is invoked with
+ converted into a distinct Python file. Your plug-in is invoked with
the Python file name, and it maps it back to its original source
file.
* Dynamic file names: all of your source files are executed by the same
- Python file. In this case, your plugin implements
+ Python file. In this case, your plug-in implements
:meth:`FileTracer.dynamic_source_filename` to provide the actual
source file for each execution frame.
@@ -73,7 +108,7 @@ class CoveragePlugin(object):
paths, be sure to take this into account.
Returns a :class:`FileTracer` object to use to trace `filename`, or
- None if this plugin cannot trace this file.
+ None if this plug-in cannot trace this file.
"""
return None
@@ -81,6 +116,8 @@ class CoveragePlugin(object):
def file_reporter(self, filename): # pylint: disable=unused-argument
"""Get the :class:`FileReporter` class to use for a file.
+ Plug-in type: file tracer.
+
This will only be invoked if `filename` returns non-None from
:meth:`file_tracer`. It's an error to return None from this method.
@@ -92,7 +129,9 @@ class CoveragePlugin(object):
def find_executable_files(self, src_dir): # pylint: disable=unused-argument
"""Yield all of the executable files in `src_dir`, recursively.
- Executability is a plugin-specific property, but generally means files
+ Plug-in type: file tracer.
+
+ Executability is a plug-in-specific property, but generally means files
which would have been considered for coverage analysis, had they been
included automatically.
@@ -102,11 +141,27 @@ class CoveragePlugin(object):
"""
return []
+ def configure(self, config):
+ """Modify the configuration of coverage.py.
+
+ Plug-in type: configurer.
+
+ This method is called during coverage.py start-up, to give your plug-in
+ a change to change the configuration. The `config` parameter is an
+ object with :meth:`~coverage.Coverage.get_option` and
+ :meth:`~coverage.Coverage.set_option` methods. Do not call any other
+ methods on the `config` object.
+
+ """
+ pass
+
def sys_info(self):
"""Get a list of information useful for debugging.
+ Plug-in type: any.
+
This method will be invoked for ``--debug=sys``. Your
- plugin can return any information it wants to be displayed.
+ plug-in can return any information it wants to be displayed.
Returns a list of pairs: `[(name, value), ...]`.
@@ -117,6 +172,9 @@ class CoveragePlugin(object):
class FileTracer(object):
"""Support needed for files during the execution phase.
+ File tracer plug-ins implement subclasses of FileTracer to return from
+ their :meth:`~CoveragePlugin.file_tracer` method.
+
You may construct this object from :meth:`CoveragePlugin.file_tracer` any
way you like. A natural choice would be to pass the file name given to
`file_tracer`.
@@ -131,7 +189,7 @@ class FileTracer(object):
def source_filename(self):
"""The source file name for this file.
- This may be any file name you like. A key responsibility of a plugin
+ This may be any file name you like. A key responsibility of a plug-in
is to own the mapping from Python execution back to whatever source
file name was originally the source of the code.
@@ -164,7 +222,7 @@ class FileTracer(object):
def dynamic_source_filename(self, filename, frame): # pylint: disable=unused-argument
"""Get a dynamically computed source file name.
- Some plugins need to compute the source file name dynamically for each
+ Some plug-ins need to compute the source file name dynamically for each
frame.
This function will not be invoked if
@@ -197,14 +255,14 @@ class FileTracer(object):
class FileReporter(object):
"""Support needed for files during the analysis and reporting phases.
- See :ref:`howitworks` for details of the different coverage.py phases.
-
- `FileReporter` objects should only be created in the
- :meth:`CoveragePlugin.file_reporter` method.
+ File tracer plug-ins implement a subclass of `FileReporter`, and return
+ instances from their :meth:`CoveragePlugin.file_reporter` method.
There are many methods here, but only :meth:`lines` is required, to provide
the set of executable lines in the file.
+ See :ref:`howitworks` for details of the different coverage.py phases.
+
"""
def __init__(self, filename):
@@ -248,7 +306,7 @@ class FileReporter(object):
def lines(self):
"""Get the executable lines in this file.
- Your plugin must determine which lines in the file were possibly
+ Your plug-in must determine which lines in the file were possibly
executable. This method returns a set of those line numbers.
Returns a set of line numbers.
@@ -259,7 +317,7 @@ class FileReporter(object):
def excluded_lines(self):
"""Get the excluded executable lines in this file.
- Your plugin can use any method it likes to allow the user to exclude
+ Your plug-in can use any method it likes to allow the user to exclude
executable lines from consideration.
Returns a set of line numbers.
@@ -277,8 +335,8 @@ class FileReporter(object):
multi-line statement, but reports are nicer if they mention the first
line.
- Your plugin can optionally define this method to perform these kinds of
- adjustment.
+ Your plug-in can optionally define this method to perform these kinds
+ of adjustment.
`lines` is a sequence of integers, the recorded line numbers.
@@ -292,7 +350,7 @@ class FileReporter(object):
def arcs(self):
"""Get the executable arcs in this file.
- To support branch coverage, your plugin needs to be able to indicate
+ To support branch coverage, your plug-in needs to be able to indicate
possible execution paths, as a set of line number pairs. Each pair is
a `(prev, next)` pair indicating that execution can transition from the
`prev` line number to the `next` line number.
@@ -306,7 +364,7 @@ class FileReporter(object):
def no_branch_lines(self):
"""Get the lines excused from branch coverage in this file.
- Your plugin can use any method it likes to allow the user to exclude
+ Your plug-in can use any method it likes to allow the user to exclude
lines from consideration of branch coverage.
Returns a set of line numbers.
@@ -337,7 +395,7 @@ class FileReporter(object):
executable line number to a count of how many exits it has.
To be honest, this feels wrong, and should be refactored. Let me know
- if you attempt to implement this method in your plugin...
+ if you attempt to implement this method in your plug-in...
"""
return {}
diff --git a/coverage/plugin_support.py b/coverage/plugin_support.py
index 66cc710..c737a42 100644
--- a/coverage/plugin_support.py
+++ b/coverage/plugin_support.py
@@ -20,6 +20,7 @@ class Plugins(object):
self.order = []
self.names = {}
self.file_tracers = []
+ self.configurers = []
self.current_module = None
self.debug = None
@@ -60,6 +61,15 @@ class Plugins(object):
"""
self._add_plugin(plugin, self.file_tracers)
+ def add_configurer(self, plugin):
+ """Add a configuring plugin.
+
+ `plugin` is an instance of a third-party plugin class. It must
+ implement the :meth:`CoveragePlugin.configure` method.
+
+ """
+ self._add_plugin(plugin, self.configurers)
+
def add_noop(self, plugin):
"""Add a plugin that does nothing.
diff --git a/doc/api_plugin.rst b/doc/api_plugin.rst
index 3ab9cb2..80d0830 100644
--- a/doc/api_plugin.rst
+++ b/doc/api_plugin.rst
@@ -3,14 +3,17 @@
.. _api_plugin:
-==============
-Plugin classes
-==============
+===============
+Plug-in classes
+===============
.. :history: 20150815T132400, new doc for 4.0b2
.. versionadded:: 4.0
+.. automodule:: coverage.plugin
+
+
.. module:: coverage
The CoveragePlugin class
diff --git a/doc/plugins.rst b/doc/plugins.rst
index 2d7fc27..fdc1adf 100644
--- a/doc/plugins.rst
+++ b/doc/plugins.rst
@@ -3,74 +3,72 @@
.. _plugins:
-=======
-Plugins
-=======
+========
+Plug-ins
+========
.. :history: 20150124T143000, new page.
.. :history: 20150802T174600, updated for 4.0b1
+.. :history: 20171228T213800, updated for configurer plugins
-Coverage.py's behavior can be extended with third-party plugins. A plugin is a
+Coverage.py's behavior can be extended with third-party plug-ins. A plug-in is a
separately installed Python class that you register in your .coveragerc.
-Plugins can be used to implement coverage measurement for non-Python files.
+Plugins can alter a number of aspects of coverage.py's behavior, including
+implementing coverage measurement for non-Python files.
-Plugins are only supported with the :ref:`C extension <install_extension>`,
-which must be installed for plugins to work.
-
-Information about using plugins is on this page. To write a plugin, see
+Information about using plug-ins is on this page. To write a plug-in, see
:ref:`api_plugin`.
.. versionadded:: 4.0
-Using plugins
--------------
+Using plug-ins
+--------------
-To use a coverage.py plugin, you install it, and configure it. For this
+To use a coverage.py plug-in, you install it and configure it. For this
example, let's say there's a Python package called ``something`` that provides
-a coverage.py plugin called ``something.plugin``.
+a coverage.py plug-in called ``something.plugin``.
-#. Install the plugin's package as you would any other Python package::
+#. Install the plug-in's package as you would any other Python package::
pip install something
-#. Configure coverage.py to use the plugin. You do this by editing (or
+#. Configure coverage.py to use the plug-in. You do this by editing (or
creating) your .coveragerc file, as described in :ref:`config`. The
- ``plugins`` setting indicates your plugin. It's a list of importable module
- names of plugins::
+ ``plugins`` setting indicates your plug-in. It's a list of importable module
+ names of plug-ins::
[run]
plugins =
something.plugin
-#. If the plugin needs its own configuration, you can add those settings in
- the .coveragerc file in a section named for the plugin::
+#. If the plug-in needs its own configuration, you can add those settings in
+ the .coveragerc file in a section named for the plug-in::
[something.plugin]
option1 = True
option2 = abc.foo
- Check the documentation for the plugin to see if it takes any options, and
- what they are.
+ Check the documentation for the plug-in for details on the options it takes.
#. Run your tests with coverage.py as you usually would. If you get a message
like "Plugin file tracers (something.plugin) aren't supported with
PyTracer," then you don't have the :ref:`C extension <install_extension>`
- installed.
+ installed. The C extension is needed for certain plug-ins.
-Available plugins
------------------
+Available plug-ins
+------------------
-Some coverage.py plugins you might find useful:
+Some coverage.py plug-ins you might find useful:
-* `Django template coverage.py plugin`__: for measuring coverage in Django
+* `Django template coverage.py plug-in`__: for measuring coverage in Django
templates.
.. __: https://pypi.python.org/pypi/django_coverage_plugin
-* `Mako template coverage plugin`__: for measuring coverage in Mako templates.
+* `Mako template coverage plug-in`__: for measuring coverage in Mako templates.
Doesn't work yet, probably needs some changes in Mako itself.
.. __: https://bitbucket.org/ned/coverage-mako-plugin
diff --git a/tests/plugin_config.py b/tests/plugin_config.py
new file mode 100644
index 0000000..67a790a
--- /dev/null
+++ b/tests/plugin_config.py
@@ -0,0 +1,22 @@
+# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
+
+"""A configuring plugin for test_plugins.py to import."""
+
+import coverage
+
+
+class Plugin(coverage.CoveragePlugin):
+ """A configuring plugin for testing."""
+ def configure(self, config):
+ """Configure all the things!"""
+ opt_name = "report:exclude_lines"
+ exclude_lines = config.get_option(opt_name)
+ exclude_lines.append(r"pragma: custom")
+ exclude_lines.append(r"pragma: or whatever")
+ config.set_option(opt_name, exclude_lines)
+
+
+def coverage_init(reg, options): # pylint: disable=unused-argument
+ """Called by coverage to initialize the plugins here."""
+ reg.add_configurer(Plugin())
diff --git a/tests/test_plugins.py b/tests/test_plugins.py
index 4dfe0bd..c4fc3ac 100644
--- a/tests/test_plugins.py
+++ b/tests/test_plugins.py
@@ -621,10 +621,10 @@ class BadFileTracerTest(FileTracerTest):
# There should be a warning explaining what's happening, but only one.
# The message can be in two forms:
- # Disabling plugin '...' due to previous exception
+ # Disabling plug-in '...' due to previous exception
# or:
- # Disabling plugin '...' due to an exception:
- msg = "Disabling plugin '%s.%s' due to " % (module_name, plugin_name)
+ # Disabling plug-in '...' due to an exception:
+ msg = "Disabling plug-in '%s.%s' due to " % (module_name, plugin_name)
warnings = stderr.count(msg)
self.assertEqual(warnings, 1)
@@ -848,3 +848,16 @@ class BadFileTracerTest(FileTracerTest):
self.run_bad_plugin(
"bad_plugin", "Plugin", our_error=False, excmsg="an integer is required",
)
+
+
+class ConfigurerPluginTest(CoverageTest):
+ """Test configuring plugins."""
+
+ def test_configurer_plugin(self):
+ cov = coverage.Coverage()
+ cov.set_option("run:plugins", ["tests.plugin_config"])
+ cov.start()
+ cov.stop() # pragma: nested
+ excluded = cov.get_option("report:exclude_lines")
+ self.assertIn("pragma: custom", excluded)
+ self.assertIn("pragma: or whatever", excluded)