src/eric7/DebugClients/Python/coverage/plugin.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8929
fcca2fa618bf
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
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 import functools
116
117 from coverage import files
118 from coverage.misc import contract, _needs_to_implement
119
120
121 class CoveragePlugin:
122 """Base class for coverage.py plug-ins."""
123
124 def file_tracer(self, filename): # pylint: disable=unused-argument
125 """Get a :class:`FileTracer` object for a file.
126
127 Plug-in type: file tracer.
128
129 Every Python source file is offered to your plug-in to give it a chance
130 to take responsibility for tracing the file. If your plug-in can
131 handle the file, it should return a :class:`FileTracer` object.
132 Otherwise return None.
133
134 There is no way to register your plug-in for particular files.
135 Instead, this method is invoked for all files as they are executed,
136 and the plug-in decides whether it can trace the file or not.
137 Be prepared for `filename` to refer to all kinds of files that have
138 nothing to do with your plug-in.
139
140 The file name will be a Python file being executed. There are two
141 broad categories of behavior for a plug-in, depending on the kind of
142 files your plug-in supports:
143
144 * Static file names: each of your original source files has been
145 converted into a distinct Python file. Your plug-in is invoked with
146 the Python file name, and it maps it back to its original source
147 file.
148
149 * Dynamic file names: all of your source files are executed by the same
150 Python file. In this case, your plug-in implements
151 :meth:`FileTracer.dynamic_source_filename` to provide the actual
152 source file for each execution frame.
153
154 `filename` is a string, the path to the file being considered. This is
155 the absolute real path to the file. If you are comparing to other
156 paths, be sure to take this into account.
157
158 Returns a :class:`FileTracer` object to use to trace `filename`, or
159 None if this plug-in cannot trace this file.
160
161 """
162 return None
163
164 def file_reporter(self, filename): # pylint: disable=unused-argument
165 """Get the :class:`FileReporter` class to use for a file.
166
167 Plug-in type: file tracer.
168
169 This will only be invoked if `filename` returns non-None from
170 :meth:`file_tracer`. It's an error to return None from this method.
171
172 Returns a :class:`FileReporter` object to use to report on `filename`,
173 or the string `"python"` to have coverage.py treat the file as Python.
174
175 """
176 _needs_to_implement(self, "file_reporter")
177
178 def dynamic_context(self, frame): # pylint: disable=unused-argument
179 """Get the dynamically computed context label for `frame`.
180
181 Plug-in type: dynamic context.
182
183 This method is invoked for each frame when outside of a dynamic
184 context, to see if a new dynamic context should be started. If it
185 returns a string, a new context label is set for this and deeper
186 frames. The dynamic context ends when this frame returns.
187
188 Returns a string to start a new dynamic context, or None if no new
189 context should be started.
190
191 """
192 return None
193
194 def find_executable_files(self, src_dir): # pylint: disable=unused-argument
195 """Yield all of the executable files in `src_dir`, recursively.
196
197 Plug-in type: file tracer.
198
199 Executability is a plug-in-specific property, but generally means files
200 which would have been considered for coverage analysis, had they been
201 included automatically.
202
203 Returns or yields a sequence of strings, the paths to files that could
204 have been executed, including files that had been executed.
205
206 """
207 return []
208
209 def configure(self, config):
210 """Modify the configuration of coverage.py.
211
212 Plug-in type: configurer.
213
214 This method is called during coverage.py start-up, to give your plug-in
215 a chance to change the configuration. The `config` parameter is an
216 object with :meth:`~coverage.Coverage.get_option` and
217 :meth:`~coverage.Coverage.set_option` methods. Do not call any other
218 methods on the `config` object.
219
220 """
221 pass
222
223 def sys_info(self):
224 """Get a list of information useful for debugging.
225
226 Plug-in type: any.
227
228 This method will be invoked for ``--debug=sys``. Your
229 plug-in can return any information it wants to be displayed.
230
231 Returns a list of pairs: `[(name, value), ...]`.
232
233 """
234 return []
235
236
237 class FileTracer:
238 """Support needed for files during the execution phase.
239
240 File tracer plug-ins implement subclasses of FileTracer to return from
241 their :meth:`~CoveragePlugin.file_tracer` method.
242
243 You may construct this object from :meth:`CoveragePlugin.file_tracer` any
244 way you like. A natural choice would be to pass the file name given to
245 `file_tracer`.
246
247 `FileTracer` objects should only be created in the
248 :meth:`CoveragePlugin.file_tracer` method.
249
250 See :ref:`howitworks` for details of the different coverage.py phases.
251
252 """
253
254 def source_filename(self):
255 """The source file name for this file.
256
257 This may be any file name you like. A key responsibility of a plug-in
258 is to own the mapping from Python execution back to whatever source
259 file name was originally the source of the code.
260
261 See :meth:`CoveragePlugin.file_tracer` for details about static and
262 dynamic file names.
263
264 Returns the file name to credit with this execution.
265
266 """
267 _needs_to_implement(self, "source_filename")
268
269 def has_dynamic_source_filename(self):
270 """Does this FileTracer have dynamic source file names?
271
272 FileTracers can provide dynamically determined file names by
273 implementing :meth:`dynamic_source_filename`. Invoking that function
274 is expensive. To determine whether to invoke it, coverage.py uses the
275 result of this function to know if it needs to bother invoking
276 :meth:`dynamic_source_filename`.
277
278 See :meth:`CoveragePlugin.file_tracer` for details about static and
279 dynamic file names.
280
281 Returns True if :meth:`dynamic_source_filename` should be called to get
282 dynamic source file names.
283
284 """
285 return False
286
287 def dynamic_source_filename(self, filename, frame): # pylint: disable=unused-argument
288 """Get a dynamically computed source file name.
289
290 Some plug-ins need to compute the source file name dynamically for each
291 frame.
292
293 This function will not be invoked if
294 :meth:`has_dynamic_source_filename` returns False.
295
296 Returns the source file name for this frame, or None if this frame
297 shouldn't be measured.
298
299 """
300 return None
301
302 def line_number_range(self, frame):
303 """Get the range of source line numbers for a given a call frame.
304
305 The call frame is examined, and the source line number in the original
306 file is returned. The return value is a pair of numbers, the starting
307 line number and the ending line number, both inclusive. For example,
308 returning (5, 7) means that lines 5, 6, and 7 should be considered
309 executed.
310
311 This function might decide that the frame doesn't indicate any lines
312 from the source file were executed. Return (-1, -1) in this case to
313 tell coverage.py that no lines should be recorded for this frame.
314
315 """
316 lineno = frame.f_lineno
317 return lineno, lineno
318
319
320 @functools.total_ordering
321 class FileReporter:
322 """Support needed for files during the analysis and reporting phases.
323
324 File tracer plug-ins implement a subclass of `FileReporter`, and return
325 instances from their :meth:`CoveragePlugin.file_reporter` method.
326
327 There are many methods here, but only :meth:`lines` is required, to provide
328 the set of executable lines in the file.
329
330 See :ref:`howitworks` for details of the different coverage.py phases.
331
332 """
333
334 def __init__(self, filename):
335 """Simple initialization of a `FileReporter`.
336
337 The `filename` argument is the path to the file being reported. This
338 will be available as the `.filename` attribute on the object. Other
339 method implementations on this base class rely on this attribute.
340
341 """
342 self.filename = filename
343
344 def __repr__(self):
345 return "<{0.__class__.__name__} filename={0.filename!r}>".format(self)
346
347 def relative_filename(self):
348 """Get the relative file name for this file.
349
350 This file path will be displayed in reports. The default
351 implementation will supply the actual project-relative file path. You
352 only need to supply this method if you have an unusual syntax for file
353 paths.
354
355 """
356 return files.relative_filename(self.filename)
357
358 @contract(returns='unicode')
359 def source(self):
360 """Get the source for the file.
361
362 Returns a Unicode string.
363
364 The base implementation simply reads the `self.filename` file and
365 decodes it as UTF-8. Override this method if your file isn't readable
366 as a text file, or if you need other encoding support.
367
368 """
369 with open(self.filename, "rb") as f:
370 return f.read().decode("utf-8")
371
372 def lines(self):
373 """Get the executable lines in this file.
374
375 Your plug-in must determine which lines in the file were possibly
376 executable. This method returns a set of those line numbers.
377
378 Returns a set of line numbers.
379
380 """
381 _needs_to_implement(self, "lines")
382
383 def excluded_lines(self):
384 """Get the excluded executable lines in this file.
385
386 Your plug-in can use any method it likes to allow the user to exclude
387 executable lines from consideration.
388
389 Returns a set of line numbers.
390
391 The base implementation returns the empty set.
392
393 """
394 return set()
395
396 def translate_lines(self, lines):
397 """Translate recorded lines into reported lines.
398
399 Some file formats will want to report lines slightly differently than
400 they are recorded. For example, Python records the last line of a
401 multi-line statement, but reports are nicer if they mention the first
402 line.
403
404 Your plug-in can optionally define this method to perform these kinds
405 of adjustment.
406
407 `lines` is a sequence of integers, the recorded line numbers.
408
409 Returns a set of integers, the adjusted line numbers.
410
411 The base implementation returns the numbers unchanged.
412
413 """
414 return set(lines)
415
416 def arcs(self):
417 """Get the executable arcs in this file.
418
419 To support branch coverage, your plug-in needs to be able to indicate
420 possible execution paths, as a set of line number pairs. Each pair is
421 a `(prev, next)` pair indicating that execution can transition from the
422 `prev` line number to the `next` line number.
423
424 Returns a set of pairs of line numbers. The default implementation
425 returns an empty set.
426
427 """
428 return set()
429
430 def no_branch_lines(self):
431 """Get the lines excused from branch coverage in this file.
432
433 Your plug-in can use any method it likes to allow the user to exclude
434 lines from consideration of branch coverage.
435
436 Returns a set of line numbers.
437
438 The base implementation returns the empty set.
439
440 """
441 return set()
442
443 def translate_arcs(self, arcs):
444 """Translate recorded arcs into reported arcs.
445
446 Similar to :meth:`translate_lines`, but for arcs. `arcs` is a set of
447 line number pairs.
448
449 Returns a set of line number pairs.
450
451 The default implementation returns `arcs` unchanged.
452
453 """
454 return arcs
455
456 def exit_counts(self):
457 """Get a count of exits from that each line.
458
459 To determine which lines are branches, coverage.py looks for lines that
460 have more than one exit. This function creates a dict mapping each
461 executable line number to a count of how many exits it has.
462
463 To be honest, this feels wrong, and should be refactored. Let me know
464 if you attempt to implement this method in your plug-in...
465
466 """
467 return {}
468
469 def missing_arc_description(self, start, end, executed_arcs=None): # pylint: disable=unused-argument
470 """Provide an English sentence describing a missing arc.
471
472 The `start` and `end` arguments are the line numbers of the missing
473 arc. Negative numbers indicate entering or exiting code objects.
474
475 The `executed_arcs` argument is a set of line number pairs, the arcs
476 that were executed in this file.
477
478 By default, this simply returns the string "Line {start} didn't jump
479 to {end}".
480
481 """
482 return f"Line {start} didn't jump to line {end}"
483
484 def source_token_lines(self):
485 """Generate a series of tokenized lines, one for each line in `source`.
486
487 These tokens are used for syntax-colored reports.
488
489 Each line is a list of pairs, each pair is a token::
490
491 [('key', 'def'), ('ws', ' '), ('nam', 'hello'), ('op', '('), ... ]
492
493 Each pair has a token class, and the token text. The token classes
494 are:
495
496 * ``'com'``: a comment
497 * ``'key'``: a keyword
498 * ``'nam'``: a name, or identifier
499 * ``'num'``: a number
500 * ``'op'``: an operator
501 * ``'str'``: a string literal
502 * ``'ws'``: some white space
503 * ``'txt'``: some other kind of text
504
505 If you concatenate all the token texts, and then join them with
506 newlines, you should have your original source back.
507
508 The default implementation simply returns each line tagged as
509 ``'txt'``.
510
511 """
512 for line in self.source().splitlines():
513 yield [('txt', line)]
514
515 def __eq__(self, other):
516 return isinstance(other, FileReporter) and self.filename == other.filename
517
518 def __lt__(self, other):
519 return isinstance(other, FileReporter) and self.filename < other.filename
520
521 __hash__ = None # This object doesn't need to be hashed.

eric ide

mercurial