|
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 |
|
7 Plug-in interfaces for coverage.py. |
|
8 |
|
9 Coverage.py supports a few different kinds of plug-ins that change its |
|
10 behavior: |
|
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 |
|
17 To 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 |
|
19 participate in various aspects of coverage.py's processing. |
|
20 Different types of plug-ins have to override different methods. |
|
21 |
|
22 Any plug-in can optionally implement :meth:`~coverage.CoveragePlugin.sys_info` |
|
23 to provide debugging information about their operation. |
|
24 |
|
25 Your module must also contain a ``coverage_init`` function that registers an |
|
26 instance 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 |
|
36 You use the `reg` parameter passed to your ``coverage_init`` function to |
|
37 register your plug-in object. The registration method you call depends on |
|
38 what kind of plug-in it is. |
|
39 |
|
40 If your plug-in takes options, the `options` parameter is a dictionary of your |
|
41 plug-in's options from the coverage.py configuration file. Use them however |
|
42 you want to configure your object before registering it. |
|
43 |
|
44 Coverage.py will store its own information on your plug-in object, using |
|
45 attributes 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 |
|
57 File Tracers |
|
58 ============ |
|
59 |
|
60 File tracers implement measurement support for non-Python files. File tracers |
|
61 implement the :meth:`~coverage.CoveragePlugin.file_tracer` method to claim |
|
62 files and the :meth:`~coverage.CoveragePlugin.file_reporter` method to report |
|
63 on those files. |
|
64 |
|
65 In your ``coverage_init`` function, use the ``add_file_tracer`` method to |
|
66 register your file tracer. |
|
67 |
|
68 |
|
69 Configurers |
|
70 =========== |
|
71 |
|
72 .. versionadded:: 4.5 |
|
73 |
|
74 Configurers modify the configuration of coverage.py during start-up. |
|
75 Configurers implement the :meth:`~coverage.CoveragePlugin.configure` method to |
|
76 change the configuration. |
|
77 |
|
78 In your ``coverage_init`` function, use the ``add_configurer`` method to |
|
79 register your configurer. |
|
80 |
|
81 """ |
|
82 |
|
83 from coverage import files |
|
84 from coverage.misc import contract, _needs_to_implement |
|
85 |
|
86 |
|
87 class 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 |
|
185 class 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 |
|
268 class 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. |