1# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
3
4"""
5.. versionadded:: 4.0
6
7Plug-in interfaces for coverage.py.
8
9Coverage.py supports a few different kinds of plug-ins that change its
10behavior:
11
12* File tracers implement tracing of non-Python file types.
13
14* Configurers add custom configuration, using Python code to change the
15  configuration.
16
17To write a coverage.py plug-in, create a module with a subclass of
18:class:`~coverage.CoveragePlugin`.  You will override methods in your class to
19participate in various aspects of coverage.py's processing.
20Different types of plug-ins have to override different methods.
21
22Any plug-in can optionally implement :meth:`~coverage.CoveragePlugin.sys_info`
23to provide debugging information about their operation.
24
25Your module must also contain a ``coverage_init`` function that registers an
26instance of your plug-in class::
27
28    import coverage
29
30    class MyPlugin(coverage.CoveragePlugin):
31        ...
32
33    def coverage_init(reg, options):
34        reg.add_file_tracer(MyPlugin())
35
36You use the `reg` parameter passed to your ``coverage_init`` function to
37register your plug-in object.  The registration method you call depends on
38what kind of plug-in it is.
39
40If your plug-in takes options, the `options` parameter is a dictionary of your
41plug-in's options from the coverage.py configuration file.  Use them however
42you want to configure your object before registering it.
43
44Coverage.py will store its own information on your plug-in object, using
45attributes whose names start with ``_coverage_``.  Don't be startled.
46
47.. warning::
48    Plug-ins are imported by coverage.py before it begins measuring code.
49    If you write a plugin in your own project, it might import your product
50    code before coverage.py can start measuring.  This can result in your
51    own code being reported as missing.
52
53    One solution is to put your plugins in your project tree, but not in
54    your importable Python package.
55
56
57File Tracers
58============
59
60File tracers implement measurement support for non-Python files.  File tracers
61implement the :meth:`~coverage.CoveragePlugin.file_tracer` method to claim
62files and the :meth:`~coverage.CoveragePlugin.file_reporter` method to report
63on those files.
64
65In your ``coverage_init`` function, use the ``add_file_tracer`` method to
66register your file tracer.
67
68
69Configurers
70===========
71
72.. versionadded:: 4.5
73
74Configurers modify the configuration of coverage.py during start-up.
75Configurers implement the :meth:`~coverage.CoveragePlugin.configure` method to
76change the configuration.
77
78In your ``coverage_init`` function, use the ``add_configurer`` method to
79register your configurer.
80
81"""
82
83from coverage import files
84from coverage.misc import contract, _needs_to_implement
85
86
87class CoveragePlugin(object):
88    """Base class for coverage.py plug-ins."""
89
90    def file_tracer(self, filename):        # pylint: disable=unused-argument
91        """Get a :class:`FileTracer` object for a file.
92
93        Plug-in type: file tracer.
94
95        Every Python source file is offered to your plug-in to give it a chance
96        to take responsibility for tracing the file.  If your plug-in can
97        handle the file, then return a :class:`FileTracer` object.  Otherwise
98        return None.
99
100        There is no way to register your plug-in for particular files.
101        Instead, this method is invoked for all files, and the plug-in decides
102        whether it can trace the file or not.  Be prepared for `filename` to
103        refer to all kinds of files that have nothing to do with your plug-in.
104
105        The file name will be a Python file being executed.  There are two
106        broad categories of behavior for a plug-in, depending on the kind of
107        files your plug-in supports:
108
109        * Static file names: each of your original source files has been
110          converted into a distinct Python file.  Your plug-in is invoked with
111          the Python file name, and it maps it back to its original source
112          file.
113
114        * Dynamic file names: all of your source files are executed by the same
115          Python file.  In this case, your plug-in implements
116          :meth:`FileTracer.dynamic_source_filename` to provide the actual
117          source file for each execution frame.
118
119        `filename` is a string, the path to the file being considered.  This is
120        the absolute real path to the file.  If you are comparing to other
121        paths, be sure to take this into account.
122
123        Returns a :class:`FileTracer` object to use to trace `filename`, or
124        None if this plug-in cannot trace this file.
125
126        """
127        return None
128
129    def file_reporter(self, filename):      # pylint: disable=unused-argument
130        """Get the :class:`FileReporter` class to use for a file.
131
132        Plug-in type: file tracer.
133
134        This will only be invoked if `filename` returns non-None from
135        :meth:`file_tracer`.  It's an error to return None from this method.
136
137        Returns a :class:`FileReporter` object to use to report on `filename`.
138
139        """
140        _needs_to_implement(self, "file_reporter")
141
142    def find_executable_files(self, src_dir):       # pylint: disable=unused-argument
143        """Yield all of the executable files in `src_dir`, recursively.
144
145        Plug-in type: file tracer.
146
147        Executability is a plug-in-specific property, but generally means files
148        which would have been considered for coverage analysis, had they been
149        included automatically.
150
151        Returns or yields a sequence of strings, the paths to files that could
152        have been executed, including files that had been executed.
153
154        """
155        return []
156
157    def configure(self, config):
158        """Modify the configuration of coverage.py.
159
160        Plug-in type: configurer.
161
162        This method is called during coverage.py start-up, to give your plug-in
163        a chance to change the configuration.  The `config` parameter is an
164        object with :meth:`~coverage.Coverage.get_option` and
165        :meth:`~coverage.Coverage.set_option` methods.  Do not call any other
166        methods on the `config` object.
167
168        """
169        pass
170
171    def sys_info(self):
172        """Get a list of information useful for debugging.
173
174        Plug-in type: any.
175
176        This method will be invoked for ``--debug=sys``.  Your
177        plug-in can return any information it wants to be displayed.
178
179        Returns a list of pairs: `[(name, value), ...]`.
180
181        """
182        return []
183
184
185class FileTracer(object):
186    """Support needed for files during the execution phase.
187
188    File tracer plug-ins implement subclasses of FileTracer to return from
189    their :meth:`~CoveragePlugin.file_tracer` method.
190
191    You may construct this object from :meth:`CoveragePlugin.file_tracer` any
192    way you like.  A natural choice would be to pass the file name given to
193    `file_tracer`.
194
195    `FileTracer` objects should only be created in the
196    :meth:`CoveragePlugin.file_tracer` method.
197
198    See :ref:`howitworks` for details of the different coverage.py phases.
199
200    """
201
202    def source_filename(self):
203        """The source file name for this file.
204
205        This may be any file name you like.  A key responsibility of a plug-in
206        is to own the mapping from Python execution back to whatever source
207        file name was originally the source of the code.
208
209        See :meth:`CoveragePlugin.file_tracer` for details about static and
210        dynamic file names.
211
212        Returns the file name to credit with this execution.
213
214        """
215        _needs_to_implement(self, "source_filename")
216
217    def has_dynamic_source_filename(self):
218        """Does this FileTracer have dynamic source file names?
219
220        FileTracers can provide dynamically determined file names by
221        implementing :meth:`dynamic_source_filename`.  Invoking that function
222        is expensive. To determine whether to invoke it, coverage.py uses the
223        result of this function to know if it needs to bother invoking
224        :meth:`dynamic_source_filename`.
225
226        See :meth:`CoveragePlugin.file_tracer` for details about static and
227        dynamic file names.
228
229        Returns True if :meth:`dynamic_source_filename` should be called to get
230        dynamic source file names.
231
232        """
233        return False
234
235    def dynamic_source_filename(self, filename, frame):     # pylint: disable=unused-argument
236        """Get a dynamically computed source file name.
237
238        Some plug-ins need to compute the source file name dynamically for each
239        frame.
240
241        This function will not be invoked if
242        :meth:`has_dynamic_source_filename` returns False.
243
244        Returns the source file name for this frame, or None if this frame
245        shouldn't be measured.
246
247        """
248        return None
249
250    def line_number_range(self, frame):
251        """Get the range of source line numbers for a given a call frame.
252
253        The call frame is examined, and the source line number in the original
254        file is returned.  The return value is a pair of numbers, the starting
255        line number and the ending line number, both inclusive.  For example,
256        returning (5, 7) means that lines 5, 6, and 7 should be considered
257        executed.
258
259        This function might decide that the frame doesn't indicate any lines
260        from the source file were executed.  Return (-1, -1) in this case to
261        tell coverage.py that no lines should be recorded for this frame.
262
263        """
264        lineno = frame.f_lineno
265        return lineno, lineno
266
267
268class FileReporter(object):
269    """Support needed for files during the analysis and reporting phases.
270
271    File tracer plug-ins implement a subclass of `FileReporter`, and return
272    instances from their :meth:`CoveragePlugin.file_reporter` method.
273
274    There are many methods here, but only :meth:`lines` is required, to provide
275    the set of executable lines in the file.
276
277    See :ref:`howitworks` for details of the different coverage.py phases.
278
279    """
280
281    def __init__(self, filename):
282        """Simple initialization of a `FileReporter`.
283
284        The `filename` argument is the path to the file being reported.  This
285        will be available as the `.filename` attribute on the object.  Other
286        method implementations on this base class rely on this attribute.
287
288        """
289        self.filename = filename
290
291    def __repr__(self):
292        return "<{0.__class__.__name__} filename={0.filename!r}>".format(self)
293
294    def relative_filename(self):
295        """Get the relative file name for this file.
296
297        This file path will be displayed in reports.  The default
298        implementation will supply the actual project-relative file path.  You
299        only need to supply this method if you have an unusual syntax for file
300        paths.
301
302        """
303        return files.relative_filename(self.filename)
304
305    @contract(returns='unicode')
306    def source(self):
307        """Get the source for the file.
308
309        Returns a Unicode string.
310
311        The base implementation simply reads the `self.filename` file and
312        decodes it as UTF8.  Override this method if your file isn't readable
313        as a text file, or if you need other encoding support.
314
315        """
316        with open(self.filename, "rb") as f:
317            return f.read().decode("utf8")
318
319    def lines(self):
320        """Get the executable lines in this file.
321
322        Your plug-in must determine which lines in the file were possibly
323        executable.  This method returns a set of those line numbers.
324
325        Returns a set of line numbers.
326
327        """
328        _needs_to_implement(self, "lines")
329
330    def excluded_lines(self):
331        """Get the excluded executable lines in this file.
332
333        Your plug-in can use any method it likes to allow the user to exclude
334        executable lines from consideration.
335
336        Returns a set of line numbers.
337
338        The base implementation returns the empty set.
339
340        """
341        return set()
342
343    def translate_lines(self, lines):
344        """Translate recorded lines into reported lines.
345
346        Some file formats will want to report lines slightly differently than
347        they are recorded.  For example, Python records the last line of a
348        multi-line statement, but reports are nicer if they mention the first
349        line.
350
351        Your plug-in can optionally define this method to perform these kinds
352        of adjustment.
353
354        `lines` is a sequence of integers, the recorded line numbers.
355
356        Returns a set of integers, the adjusted line numbers.
357
358        The base implementation returns the numbers unchanged.
359
360        """
361        return set(lines)
362
363    def arcs(self):
364        """Get the executable arcs in this file.
365
366        To support branch coverage, your plug-in needs to be able to indicate
367        possible execution paths, as a set of line number pairs.  Each pair is
368        a `(prev, next)` pair indicating that execution can transition from the
369        `prev` line number to the `next` line number.
370
371        Returns a set of pairs of line numbers.  The default implementation
372        returns an empty set.
373
374        """
375        return set()
376
377    def no_branch_lines(self):
378        """Get the lines excused from branch coverage in this file.
379
380        Your plug-in can use any method it likes to allow the user to exclude
381        lines from consideration of branch coverage.
382
383        Returns a set of line numbers.
384
385        The base implementation returns the empty set.
386
387        """
388        return set()
389
390    def translate_arcs(self, arcs):
391        """Translate recorded arcs into reported arcs.
392
393        Similar to :meth:`translate_lines`, but for arcs.  `arcs` is a set of
394        line number pairs.
395
396        Returns a set of line number pairs.
397
398        The default implementation returns `arcs` unchanged.
399
400        """
401        return arcs
402
403    def exit_counts(self):
404        """Get a count of exits from that each line.
405
406        To determine which lines are branches, coverage.py looks for lines that
407        have more than one exit.  This function creates a dict mapping each
408        executable line number to a count of how many exits it has.
409
410        To be honest, this feels wrong, and should be refactored.  Let me know
411        if you attempt to implement this method in your plug-in...
412
413        """
414        return {}
415
416    def missing_arc_description(self, start, end, executed_arcs=None):     # pylint: disable=unused-argument
417        """Provide an English sentence describing a missing arc.
418
419        The `start` and `end` arguments are the line numbers of the missing
420        arc. Negative numbers indicate entering or exiting code objects.
421
422        The `executed_arcs` argument is a set of line number pairs, the arcs
423        that were executed in this file.
424
425        By default, this simply returns the string "Line {start} didn't jump
426        to {end}".
427
428        """
429        return "Line {start} didn't jump to line {end}".format(start=start, end=end)
430
431    def source_token_lines(self):
432        """Generate a series of tokenized lines, one for each line in `source`.
433
434        These tokens are used for syntax-colored reports.
435
436        Each line is a list of pairs, each pair is a token::
437
438            [('key', 'def'), ('ws', ' '), ('nam', 'hello'), ('op', '('), ... ]
439
440        Each pair has a token class, and the token text.  The token classes
441        are:
442
443        * ``'com'``: a comment
444        * ``'key'``: a keyword
445        * ``'nam'``: a name, or identifier
446        * ``'num'``: a number
447        * ``'op'``: an operator
448        * ``'str'``: a string literal
449        * ``'txt'``: some other kind of text
450
451        If you concatenate all the token texts, and then join them with
452        newlines, you should have your original source back.
453
454        The default implementation simply returns each line tagged as
455        ``'txt'``.
456
457        """
458        for line in self.source().splitlines():
459            yield [('txt', line)]
460
461    # Annoying comparison operators. Py3k wants __lt__ etc, and Py2k needs all
462    # of them defined.
463
464    def __eq__(self, other):
465        return isinstance(other, FileReporter) and self.filename == other.filename
466
467    def __ne__(self, other):
468        return not (self == other)
469
470    def __lt__(self, other):
471        return self.filename < other.filename
472
473    def __le__(self, other):
474        return self.filename <= other.filename
475
476    def __gt__(self, other):
477        return self.filename > other.filename
478
479    def __ge__(self, other):
480        return self.filename >= other.filename
481
482    __hash__ = None     # This object doesn't need to be hashed.
483