diff options
Diffstat (limited to 'coverage/plugin.py')
-rw-r--r-- | coverage/plugin.py | 367 |
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 |