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