eric7/DebugClients/Python/coverage/plugin.py

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

eric ide

mercurial