summaryrefslogtreecommitdiff
path: root/coverage/plugin.py
diff options
context:
space:
mode:
Diffstat (limited to 'coverage/plugin.py')
-rw-r--r--coverage/plugin.py367
1 files changed, 248 insertions, 119 deletions
diff --git a/coverage/plugin.py b/coverage/plugin.py
index 6648d7a6..f870c254 100644
--- a/coverage/plugin.py
+++ b/coverage/plugin.py
@@ -1,19 +1,18 @@
-"""Plugin interfaces for coverage.py"""
+# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
-import os
-import re
+"""Plugin interfaces for coverage.py"""
-from coverage.misc import _needs_to_implement
+from coverage import files
+from coverage.misc import contract, _needs_to_implement
-# TODO: document that the plugin objects may be decorated with attributes with
-# named "_coverage_*".
class CoveragePlugin(object):
"""Base class for coverage.py plugins.
- To write a coverage.py plugin, create a subclass of `CoveragePlugin`.
- You can override methods here to participate in various aspects of
- coverage.py's processing.
+ 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.
Currently the only plugin type is a file tracer, for implementing
measurement support for non-Python files. File tracer plugins implement
@@ -23,122 +22,149 @@ class CoveragePlugin(object):
Any plugin can optionally implement :meth:`sys_info` to provide debugging
information about their operation.
- """
+ Coverage.py will store its own information on your plugin object, using
+ attributes whose names start with ``_coverage_``. Don't be startled.
- def __init__(self, options):
- """
- When the plugin is constructed, it will be passed a dictionary of
- plugin-specific options read from the .coveragerc configuration file.
- The base class stores these on the `self.options` attribute.
+ To register your plugin, define a function called `coverage_init` in your
+ module::
- Arguments:
- options (dict): The plugin-specific options read from the
- .coveragerc configuration file.
+ def coverage_init(reg, options):
+ reg.add_file_tracer(MyPlugin())
- """
- self.options = options
+ 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.
+
+ 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.
+
+ """
def file_tracer(self, filename): # pylint: disable=unused-argument
- """Return a FileTracer object for a file.
+ """Get a :class:`FileTracer` object for a file.
- Every 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.
+ 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.
There is no way to register your plugin for particular files. Instead,
- this method is invoked for all files, and can decide whether it can
- trace the file or not. Be prepared for `filename` to refer to all
+ 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.
- Arguments:
- filename (str): The path to the file being considered. This is the
- absolute real path to the file. If you are comparing to other
- paths, be sure to take this into account.
+ 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:
+
+ * Static file names: each of your original source files has been
+ converted into a distinct Python file. Your plugin 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
+ :meth:`FileTracer.dynamic_source_filename` to provide the actual
+ source file for each execution frame.
+
+ `filename` is a string, the path to the file being considered. This is
+ the absolute real path to the file. If you are comparing to other
+ paths, be sure to take this into account.
- Returns:
- FileTracer: the :class:`FileTracer` object to use to trace
- `filename`, or None if this plugin cannot trace this file.
+ Returns a :class:`FileTracer` object to use to trace `filename`, or
+ None if this plugin cannot trace this file.
"""
return None
def file_reporter(self, filename): # pylint: disable=unused-argument
- """Return the FileReporter class to use for filename.
+ """Get the :class:`FileReporter` class to use for a file.
This will only be invoked if `filename` returns non-None from
- :meth:`file_tracer`. It's an error to return None.
+ :meth:`file_tracer`. It's an error to return None from this method.
+
+ Returns a :class:`FileReporter` object to use to report on `filename`.
"""
_needs_to_implement(self, "file_reporter")
def sys_info(self):
- """Return a list of information useful for debugging.
+ """Get a list of information useful for debugging.
This method will be invoked for ``--debug=sys``. Your
plugin can return any information it wants to be displayed.
- The return value is a list of pairs: (name, value).
+ Returns a list of pairs: `[(name, value), ...]`.
"""
return []
class FileTracer(object):
- """Support needed for files during the tracing phase.
+ """Support needed for files during the execution phase.
You may construct this object from :meth:`CoveragePlugin.file_tracer` any
- way you like. A natural choice would be to pass the filename given to
+ way you like. A natural choice would be to pass the file name given to
`file_tracer`.
+ `FileTracer` objects should only be created in the
+ :meth:`CoveragePlugin.file_tracer` method.
+
+ See :ref:`howitworks` for details of the different coverage.py phases.
+
"""
def source_filename(self):
- """The source filename for this file.
+ """The source file name for this file.
- This may be any filename you like. A key responsibility of a plugin is
- to own the mapping from Python execution back to whatever source
- filename was originally the source of the code.
+ This may be any file name you like. A key responsibility of a plugin
+ is to own the mapping from Python execution back to whatever source
+ file name was originally the source of the code.
- Returns:
- The filename to credit with this execution.
+ See :meth:`CoveragePlugin.file_tracer` for details about static and
+ dynamic file names.
+
+ Returns the file name to credit with this execution.
"""
_needs_to_implement(self, "source_filename")
def has_dynamic_source_filename(self):
- """Does this FileTracer have dynamic source filenames?
+ """Does this FileTracer have dynamic source file names?
- FileTracers can provide dynamically determined filenames by
- implementing dynamic_source_filename. Invoking that function is
- expensive. To determine whether to invoke it, coverage.py uses
- the result of this function to know if it needs to bother invoking
+ FileTracers can provide dynamically determined file names by
+ implementing :meth:`dynamic_source_filename`. Invoking that function
+ is expensive. To determine whether to invoke it, coverage.py uses the
+ result of this function to know if it needs to bother invoking
:meth:`dynamic_source_filename`.
- Returns:
- boolean: True if :meth:`dynamic_source_filename` should be called
- to get dynamic source filenames.
+ See :meth:`CoveragePlugin.file_tracer` for details about static and
+ dynamic file names.
+
+ Returns True if :meth:`dynamic_source_filename` should be called to get
+ dynamic source file names.
"""
return False
def dynamic_source_filename(self, filename, frame): # pylint: disable=unused-argument
- """Returns a dynamically computed source filename.
+ """Get a dynamically computed source file name.
- Some plugins need to compute the source filename dynamically for each
+ Some plugins need to compute the source file name dynamically for each
frame.
This function will not be invoked if
:meth:`has_dynamic_source_filename` returns False.
- Returns:
- The source filename for this frame, or None if this frame shouldn't
- be measured.
+ Returns the source file name for this frame, or None if this frame
+ shouldn't be measured.
"""
return None
def line_number_range(self, frame):
- """Return the range of source line numbers for a given a call frame.
+ """Get the range of source line numbers for a given a call frame.
The call frame is examined, and the source line number in the original
file is returned. The return value is a pair of numbers, the starting
@@ -150,103 +176,206 @@ class FileTracer(object):
from the source file were executed. Return (-1, -1) in this case to
tell coverage.py that no lines should be recorded for this frame.
- Arguments:
- frame: the call frame to examine.
-
- Returns:
- int, int: a pair of line numbers, the start and end lines
- executed in the source, inclusive.
-
"""
lineno = frame.f_lineno
return lineno, lineno
class FileReporter(object):
- """Support needed for files during the reporting phase."""
+ """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.
+
+ There are many methods here, but only :meth:`lines` is required, to provide
+ the set of executable lines in the file.
+
+ """
+
def __init__(self, filename):
- # TODO: document that this init happens.
+ """Simple initialization of a `FileReporter`.
+
+ The `filename` argument is the path to the file being reported. This
+ will be available as the `.filename` attribute on the object. Other
+ method implementations on this base class rely on this attribute.
+
+ """
self.filename = filename
def __repr__(self):
- return (
- # pylint: disable=redundant-keyword-arg
- "<{self.__class__.__name__}"
- " filename={self.filename!r}>".format(self=self)
- )
+ return "<{0.__class__.__name__} filename={0.filename!r}>".format(self)
- # Annoying comparison operators. Py3k wants __lt__ etc, and Py2k needs all
- # of them defined.
+ def relative_filename(self):
+ """Get the relative file name for this file.
- def __lt__(self, other):
- return self.filename < other.filename
+ This file path will be displayed in reports. The default
+ implementation will supply the actual project-relative file path. You
+ only need to supply this method if you have an unusual syntax for file
+ paths.
- def __le__(self, other):
- return self.filename <= other.filename
+ """
+ return files.relative_filename(self.filename)
- def __eq__(self, other):
- return self.filename == other.filename
+ @contract(returns='unicode')
+ def source(self):
+ """Get the source for the file.
- def __ne__(self, other):
- return self.filename != other.filename
+ Returns a Unicode string.
- def __gt__(self, other):
- return self.filename > other.filename
+ The base implementation simply reads the `self.filename` file and
+ decodes it as UTF8. Override this method if your file isn't readable
+ as a text file, or if you need other encoding support.
- def __ge__(self, other):
- return self.filename >= other.filename
+ """
+ with open(self.filename, "rb") as f:
+ return f.read().decode("utf8")
- def statements(self):
- _needs_to_implement(self, "statements")
+ def lines(self):
+ """Get the executable lines in this file.
- def excluded_statements(self):
- return set([])
+ Your plugin 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.
+
+ """
+ _needs_to_implement(self, "lines")
+
+ 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
+ executable lines from consideration.
+
+ Returns a set of line numbers.
+
+ The base implementation returns the empty set.
+
+ """
+ return set()
def translate_lines(self, lines):
+ """Translate recorded lines into reported lines.
+
+ Some file formats will want to report lines slightly differently than
+ they are recorded. For example, Python records the last line of a
+ 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.
+
+ `lines` is a sequence of integers, the recorded line numbers.
+
+ Returns a set of integers, the adjusted line numbers.
+
+ The base implementation returns the numbers unchanged.
+
+ """
return set(lines)
- def translate_arcs(self, arcs):
- return arcs
+ def arcs(self):
+ """Get the executable arcs in this file.
+
+ To support branch coverage, your plugin 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.
+
+ Returns a set of pairs of line numbers. The default implementation
+ returns an empty set.
+
+ """
+ return set()
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
+ lines from consideration of branch coverage.
+
+ Returns a set of line numbers.
+
+ The base implementation returns the empty set.
+
+ """
return set()
+ def translate_arcs(self, arcs):
+ """Translate recorded arcs into reported arcs.
+
+ Similar to :meth:`translate_lines`, but for arcs. `arcs` is a set of
+ line number pairs.
+
+ Returns a set of line number pairs.
+
+ The default implementation returns `arcs` unchanged.
+
+ """
+ return arcs
+
def exit_counts(self):
- return {}
+ """Get a count of exits from that each line.
- def arcs(self):
- return []
+ To determine which lines are branches, coverage.py looks for lines that
+ have more than one exit. This function creates a dict mapping each
+ executable line number to a count of how many exits it has.
- def source(self):
- """Return the source for the code, a Unicode string."""
- # A generic implementation: read the text of self.filename
- with open(self.filename) as f:
- return f.read()
+ To be honest, this feels wrong, and should be refactored. Let me know
+ if you attempt to implement this...
+
+ """
+ return {}
def source_token_lines(self):
- """Return the 'tokenized' text for the code."""
- # A generic implementation, each line is one "txt" token.
+ """Generate a series of tokenized lines, one for each line in `source`.
+
+ These tokens are used for syntax-colored reports.
+
+ Each line is a list of pairs, each pair is a token::
+
+ [('key', 'def'), ('ws', ' '), ('nam', 'hello'), ('op', '('), ... ]
+
+ Each pair has a token class, and the token text. The token classes
+ are:
+
+ * ``'com'``: a comment
+ * ``'key'``: a keyword
+ * ``'nam'``: a name, or identifier
+ * ``'num'``: a number
+ * ``'op'``: an operator
+ * ``'str'``: a string literal
+ * ``'txt'``: some other kind of text
+
+ If you concatenate all the token texts, and then join them with
+ newlines, you should have your original source back.
+
+ The default implementation simply returns each line tagged as
+ ``'txt'``.
+
+ """
for line in self.source().splitlines():
yield [('txt', line)]
- def should_be_python(self):
- """Does it seem like this file should contain Python?
+ # Annoying comparison operators. Py3k wants __lt__ etc, and Py2k needs all
+ # of them defined.
+
+ def __eq__(self, other):
+ return isinstance(other, FileReporter) and self.filename == other.filename
- This is used to decide if a file reported as part of the execution of
- a program was really likely to have contained Python in the first
- place.
- """
- return False
+ def __ne__(self, other):
+ return not (self == other)
- def flat_rootname(self):
- """A base for a flat filename to correspond to this file.
+ def __lt__(self, other):
+ return self.filename < other.filename
- Useful for writing files about the code where you want all the files in
- the same directory, but need to differentiate same-named files from
- different directories.
+ def __le__(self, other):
+ return self.filename <= other.filename
- For example, the file a/b/c.py will return 'a_b_c_py'
+ def __gt__(self, other):
+ return self.filename > other.filename
- """
- name = os.path.splitdrive(self.name)[1]
- return re.sub(r"[\\/.:]", "_", name)
+ def __ge__(self, other):
+ return self.filename >= other.filename