|
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 """Support for plugins.""" |
|
5 |
|
6 import os |
|
7 import os.path |
|
8 import sys |
|
9 |
|
10 from coverage.exceptions import PluginError |
|
11 from coverage.misc import isolate_module |
|
12 from coverage.plugin import CoveragePlugin, FileTracer, FileReporter |
|
13 |
|
14 os = isolate_module(os) |
|
15 |
|
16 |
|
17 class Plugins: |
|
18 """The currently loaded collection of coverage.py plugins.""" |
|
19 |
|
20 def __init__(self): |
|
21 self.order = [] |
|
22 self.names = {} |
|
23 self.file_tracers = [] |
|
24 self.configurers = [] |
|
25 self.context_switchers = [] |
|
26 |
|
27 self.current_module = None |
|
28 self.debug = None |
|
29 |
|
30 @classmethod |
|
31 def load_plugins(cls, modules, config, debug=None): |
|
32 """Load plugins from `modules`. |
|
33 |
|
34 Returns a Plugins object with the loaded and configured plugins. |
|
35 |
|
36 """ |
|
37 plugins = cls() |
|
38 plugins.debug = debug |
|
39 |
|
40 for module in modules: |
|
41 plugins.current_module = module |
|
42 __import__(module) |
|
43 mod = sys.modules[module] |
|
44 |
|
45 coverage_init = getattr(mod, "coverage_init", None) |
|
46 if not coverage_init: |
|
47 raise PluginError( |
|
48 f"Plugin module {module!r} didn't define a coverage_init function" |
|
49 ) |
|
50 |
|
51 options = config.get_plugin_options(module) |
|
52 coverage_init(plugins, options) |
|
53 |
|
54 plugins.current_module = None |
|
55 return plugins |
|
56 |
|
57 def add_file_tracer(self, plugin): |
|
58 """Add a file tracer plugin. |
|
59 |
|
60 `plugin` is an instance of a third-party plugin class. It must |
|
61 implement the :meth:`CoveragePlugin.file_tracer` method. |
|
62 |
|
63 """ |
|
64 self._add_plugin(plugin, self.file_tracers) |
|
65 |
|
66 def add_configurer(self, plugin): |
|
67 """Add a configuring plugin. |
|
68 |
|
69 `plugin` is an instance of a third-party plugin class. It must |
|
70 implement the :meth:`CoveragePlugin.configure` method. |
|
71 |
|
72 """ |
|
73 self._add_plugin(plugin, self.configurers) |
|
74 |
|
75 def add_dynamic_context(self, plugin): |
|
76 """Add a dynamic context plugin. |
|
77 |
|
78 `plugin` is an instance of a third-party plugin class. It must |
|
79 implement the :meth:`CoveragePlugin.dynamic_context` method. |
|
80 |
|
81 """ |
|
82 self._add_plugin(plugin, self.context_switchers) |
|
83 |
|
84 def add_noop(self, plugin): |
|
85 """Add a plugin that does nothing. |
|
86 |
|
87 This is only useful for testing the plugin support. |
|
88 |
|
89 """ |
|
90 self._add_plugin(plugin, None) |
|
91 |
|
92 def _add_plugin(self, plugin, specialized): |
|
93 """Add a plugin object. |
|
94 |
|
95 `plugin` is a :class:`CoveragePlugin` instance to add. `specialized` |
|
96 is a list to append the plugin to. |
|
97 |
|
98 """ |
|
99 plugin_name = f"{self.current_module}.{plugin.__class__.__name__}" |
|
100 if self.debug and self.debug.should('plugin'): |
|
101 self.debug.write(f"Loaded plugin {self.current_module!r}: {plugin!r}") |
|
102 labelled = LabelledDebug(f"plugin {self.current_module!r}", self.debug) |
|
103 plugin = DebugPluginWrapper(plugin, labelled) |
|
104 |
|
105 # pylint: disable=attribute-defined-outside-init |
|
106 plugin._coverage_plugin_name = plugin_name |
|
107 plugin._coverage_enabled = True |
|
108 self.order.append(plugin) |
|
109 self.names[plugin_name] = plugin |
|
110 if specialized is not None: |
|
111 specialized.append(plugin) |
|
112 |
|
113 def __bool__(self): |
|
114 return bool(self.order) |
|
115 |
|
116 def __iter__(self): |
|
117 return iter(self.order) |
|
118 |
|
119 def get(self, plugin_name): |
|
120 """Return a plugin by name.""" |
|
121 return self.names[plugin_name] |
|
122 |
|
123 |
|
124 class LabelledDebug: |
|
125 """A Debug writer, but with labels for prepending to the messages.""" |
|
126 |
|
127 def __init__(self, label, debug, prev_labels=()): |
|
128 self.labels = list(prev_labels) + [label] |
|
129 self.debug = debug |
|
130 |
|
131 def add_label(self, label): |
|
132 """Add a label to the writer, and return a new `LabelledDebug`.""" |
|
133 return LabelledDebug(label, self.debug, self.labels) |
|
134 |
|
135 def message_prefix(self): |
|
136 """The prefix to use on messages, combining the labels.""" |
|
137 prefixes = self.labels + [''] |
|
138 return ":\n".join(" "*i+label for i, label in enumerate(prefixes)) |
|
139 |
|
140 def write(self, message): |
|
141 """Write `message`, but with the labels prepended.""" |
|
142 self.debug.write(f"{self.message_prefix()}{message}") |
|
143 |
|
144 |
|
145 class DebugPluginWrapper(CoveragePlugin): |
|
146 """Wrap a plugin, and use debug to report on what it's doing.""" |
|
147 |
|
148 def __init__(self, plugin, debug): |
|
149 super().__init__() |
|
150 self.plugin = plugin |
|
151 self.debug = debug |
|
152 |
|
153 def file_tracer(self, filename): |
|
154 tracer = self.plugin.file_tracer(filename) |
|
155 self.debug.write(f"file_tracer({filename!r}) --> {tracer!r}") |
|
156 if tracer: |
|
157 debug = self.debug.add_label(f"file {filename!r}") |
|
158 tracer = DebugFileTracerWrapper(tracer, debug) |
|
159 return tracer |
|
160 |
|
161 def file_reporter(self, filename): |
|
162 reporter = self.plugin.file_reporter(filename) |
|
163 self.debug.write(f"file_reporter({filename!r}) --> {reporter!r}") |
|
164 if reporter: |
|
165 debug = self.debug.add_label(f"file {filename!r}") |
|
166 reporter = DebugFileReporterWrapper(filename, reporter, debug) |
|
167 return reporter |
|
168 |
|
169 def dynamic_context(self, frame): |
|
170 context = self.plugin.dynamic_context(frame) |
|
171 self.debug.write(f"dynamic_context({frame!r}) --> {context!r}") |
|
172 return context |
|
173 |
|
174 def find_executable_files(self, src_dir): |
|
175 executable_files = self.plugin.find_executable_files(src_dir) |
|
176 self.debug.write(f"find_executable_files({src_dir!r}) --> {executable_files!r}") |
|
177 return executable_files |
|
178 |
|
179 def configure(self, config): |
|
180 self.debug.write(f"configure({config!r})") |
|
181 self.plugin.configure(config) |
|
182 |
|
183 def sys_info(self): |
|
184 return self.plugin.sys_info() |
|
185 |
|
186 |
|
187 class DebugFileTracerWrapper(FileTracer): |
|
188 """A debugging `FileTracer`.""" |
|
189 |
|
190 def __init__(self, tracer, debug): |
|
191 self.tracer = tracer |
|
192 self.debug = debug |
|
193 |
|
194 def _show_frame(self, frame): |
|
195 """A short string identifying a frame, for debug messages.""" |
|
196 return "%s@%d" % ( |
|
197 os.path.basename(frame.f_code.co_filename), |
|
198 frame.f_lineno, |
|
199 ) |
|
200 |
|
201 def source_filename(self): |
|
202 sfilename = self.tracer.source_filename() |
|
203 self.debug.write(f"source_filename() --> {sfilename!r}") |
|
204 return sfilename |
|
205 |
|
206 def has_dynamic_source_filename(self): |
|
207 has = self.tracer.has_dynamic_source_filename() |
|
208 self.debug.write(f"has_dynamic_source_filename() --> {has!r}") |
|
209 return has |
|
210 |
|
211 def dynamic_source_filename(self, filename, frame): |
|
212 dyn = self.tracer.dynamic_source_filename(filename, frame) |
|
213 self.debug.write("dynamic_source_filename({!r}, {}) --> {!r}".format( |
|
214 filename, self._show_frame(frame), dyn, |
|
215 )) |
|
216 return dyn |
|
217 |
|
218 def line_number_range(self, frame): |
|
219 pair = self.tracer.line_number_range(frame) |
|
220 self.debug.write(f"line_number_range({self._show_frame(frame)}) --> {pair!r}") |
|
221 return pair |
|
222 |
|
223 |
|
224 class DebugFileReporterWrapper(FileReporter): |
|
225 """A debugging `FileReporter`.""" |
|
226 |
|
227 def __init__(self, filename, reporter, debug): |
|
228 super().__init__(filename) |
|
229 self.reporter = reporter |
|
230 self.debug = debug |
|
231 |
|
232 def relative_filename(self): |
|
233 ret = self.reporter.relative_filename() |
|
234 self.debug.write(f"relative_filename() --> {ret!r}") |
|
235 return ret |
|
236 |
|
237 def lines(self): |
|
238 ret = self.reporter.lines() |
|
239 self.debug.write(f"lines() --> {ret!r}") |
|
240 return ret |
|
241 |
|
242 def excluded_lines(self): |
|
243 ret = self.reporter.excluded_lines() |
|
244 self.debug.write(f"excluded_lines() --> {ret!r}") |
|
245 return ret |
|
246 |
|
247 def translate_lines(self, lines): |
|
248 ret = self.reporter.translate_lines(lines) |
|
249 self.debug.write(f"translate_lines({lines!r}) --> {ret!r}") |
|
250 return ret |
|
251 |
|
252 def translate_arcs(self, arcs): |
|
253 ret = self.reporter.translate_arcs(arcs) |
|
254 self.debug.write(f"translate_arcs({arcs!r}) --> {ret!r}") |
|
255 return ret |
|
256 |
|
257 def no_branch_lines(self): |
|
258 ret = self.reporter.no_branch_lines() |
|
259 self.debug.write(f"no_branch_lines() --> {ret!r}") |
|
260 return ret |
|
261 |
|
262 def exit_counts(self): |
|
263 ret = self.reporter.exit_counts() |
|
264 self.debug.write(f"exit_counts() --> {ret!r}") |
|
265 return ret |
|
266 |
|
267 def arcs(self): |
|
268 ret = self.reporter.arcs() |
|
269 self.debug.write(f"arcs() --> {ret!r}") |
|
270 return ret |
|
271 |
|
272 def source(self): |
|
273 ret = self.reporter.source() |
|
274 self.debug.write("source() --> %d chars" % (len(ret),)) |
|
275 return ret |
|
276 |
|
277 def source_token_lines(self): |
|
278 ret = list(self.reporter.source_token_lines()) |
|
279 self.debug.write("source_token_lines() --> %d tokens" % (len(ret),)) |
|
280 return ret |