0
# Plugin System
1
2
Extensible plugin architecture for custom file tracers, configurers, and dynamic context switchers. Enables coverage measurement for non-Python files and custom execution environments.
3
4
## Capabilities
5
6
### CoveragePlugin Base Class
7
8
Base class for all coverage.py plugins providing hooks for file tracing, configuration, and dynamic context switching.
9
10
```python { .api }
11
class CoveragePlugin:
12
"""
13
Base class for coverage.py plugins.
14
15
Attributes set by coverage.py:
16
- _coverage_plugin_name (str): Plugin name
17
- _coverage_enabled (bool): Whether plugin is enabled
18
"""
19
20
def file_tracer(self, filename: str):
21
"""
22
Claim a file for tracing by this plugin.
23
24
Parameters:
25
- filename (str): The file being imported or executed
26
27
Returns:
28
FileTracer | None: FileTracer instance if this plugin handles the file
29
"""
30
31
def file_reporter(self, filename: str):
32
"""
33
Provide a FileReporter for a file handled by this plugin.
34
35
Parameters:
36
- filename (str): The file needing a reporter
37
38
Returns:
39
FileReporter | str: FileReporter instance or source filename
40
"""
41
42
def dynamic_context(self, frame):
43
"""
44
Determine the dynamic context for a frame.
45
46
Parameters:
47
- frame: Python frame object
48
49
Returns:
50
str | None: Context label or None to use default
51
"""
52
53
def find_executable_files(self, src_dir: str):
54
"""
55
Find executable files in a source directory.
56
57
Parameters:
58
- src_dir (str): Directory to search
59
60
Returns:
61
Iterable[str]: Executable file paths
62
"""
63
64
def configure(self, config):
65
"""
66
Configure coverage.py during startup.
67
68
Parameters:
69
- config: Coverage configuration object
70
"""
71
72
def sys_info(self):
73
"""
74
Return debugging information about this plugin.
75
76
Returns:
77
Iterable[tuple[str, Any]]: Key-value pairs of debug info
78
"""
79
```
80
81
Usage example:
82
83
```python
84
import coverage
85
86
class MyPlugin(coverage.CoveragePlugin):
87
def file_tracer(self, filename):
88
if filename.endswith('.myext'):
89
return MyFileTracer(filename)
90
return None
91
92
def configure(self, config):
93
# Modify configuration as needed
94
config.set_option('run:source', ['src/'])
95
96
def sys_info(self):
97
return [
98
('my_plugin_version', '1.0.0'),
99
('my_plugin_config', self.config_info)
100
]
101
102
def coverage_init(reg, options):
103
reg.add_file_tracer(MyPlugin())
104
```
105
106
### FileTracer Class
107
108
Base class for file tracers that handle non-Python files or custom execution environments.
109
110
```python { .api }
111
class FileTracer:
112
"""
113
Base class for file tracers that track execution in non-Python files.
114
"""
115
116
def source_filename(self) -> str:
117
"""
118
Get the source filename for this traced file.
119
120
Returns:
121
str: The source filename to report coverage for
122
"""
123
124
def has_dynamic_source_filename(self) -> bool:
125
"""
126
Check if source filename can change dynamically.
127
128
Returns:
129
bool: True if source_filename can vary per frame
130
"""
131
132
def dynamic_source_filename(self, filename: str, frame):
133
"""
134
Get the source filename for a specific frame.
135
136
Parameters:
137
- filename (str): The file being traced
138
- frame: Python frame object
139
140
Returns:
141
str | None: Source filename for this frame
142
"""
143
144
def line_number_range(self, frame):
145
"""
146
Get the range of line numbers for a frame.
147
148
Parameters:
149
- frame: Python frame object
150
151
Returns:
152
tuple[int, int]: (start_line, end_line) inclusive range
153
"""
154
```
155
156
Usage example:
157
158
```python
159
import coverage
160
161
class TemplateFileTracer(coverage.FileTracer):
162
def __init__(self, template_file):
163
self.template_file = template_file
164
self.source_file = template_file.replace('.tmpl', '.py')
165
166
def source_filename(self):
167
return self.source_file
168
169
def line_number_range(self, frame):
170
# Map template lines to source lines
171
template_line = frame.f_lineno
172
source_line = self.map_template_to_source(template_line)
173
return source_line, source_line
174
175
def map_template_to_source(self, template_line):
176
# Custom mapping logic
177
return template_line * 2 # Example mapping
178
179
class TemplatePlugin(coverage.CoveragePlugin):
180
def file_tracer(self, filename):
181
if filename.endswith('.tmpl'):
182
return TemplateFileTracer(filename)
183
return None
184
```
185
186
### FileReporter Class
187
188
Base class for file reporters that provide analysis information for files.
189
190
```python { .api }
191
class FileReporter:
192
"""
193
Base class for file reporters that analyze files for coverage reporting.
194
"""
195
196
def __init__(self, filename: str):
197
"""
198
Initialize the file reporter.
199
200
Parameters:
201
- filename (str): The file to report on
202
"""
203
204
def relative_filename(self) -> str:
205
"""
206
Get the relative filename for reporting.
207
208
Returns:
209
str: Relative path for display in reports
210
"""
211
212
def source(self) -> str:
213
"""
214
Get the source code of the file.
215
216
Returns:
217
str: Complete source code of the file
218
"""
219
220
def lines(self) -> set[int]:
221
"""
222
Get the set of executable line numbers.
223
224
Returns:
225
set[int]: Line numbers that can be executed
226
"""
227
228
def excluded_lines(self) -> set[int]:
229
"""
230
Get the set of excluded line numbers.
231
232
Returns:
233
set[int]: Line numbers excluded from coverage
234
"""
235
236
def translate_lines(self, lines) -> set[int]:
237
"""
238
Translate line numbers to the original file.
239
240
Parameters:
241
- lines (Iterable[int]): Line numbers to translate
242
243
Returns:
244
set[int]: Translated line numbers
245
"""
246
247
def arcs(self) -> set[tuple[int, int]]:
248
"""
249
Get the set of possible execution arcs.
250
251
Returns:
252
set[tuple[int, int]]: Possible (from_line, to_line) arcs
253
"""
254
255
def no_branch_lines(self) -> set[int]:
256
"""
257
Get lines that should not be considered for branch coverage.
258
259
Returns:
260
set[int]: Line numbers without branches
261
"""
262
263
def translate_arcs(self, arcs) -> set[tuple[int, int]]:
264
"""
265
Translate execution arcs to the original file.
266
267
Parameters:
268
- arcs (Iterable[tuple[int, int]]): Arcs to translate
269
270
Returns:
271
set[tuple[int, int]]: Translated arcs
272
"""
273
274
def exit_counts(self) -> dict[int, int]:
275
"""
276
Get exit counts for each line.
277
278
Returns:
279
dict[int, int]: Mapping of line numbers to exit counts
280
"""
281
282
def missing_arc_description(self, start: int, end: int, executed_arcs=None) -> str:
283
"""
284
Describe a missing arc for reporting.
285
286
Parameters:
287
- start (int): Starting line number
288
- end (int): Ending line number
289
- executed_arcs: Set of executed arcs for context
290
291
Returns:
292
str: Human-readable description of the missing arc
293
"""
294
295
def arc_description(self, start: int, end: int) -> str:
296
"""
297
Describe an arc for reporting.
298
299
Parameters:
300
- start (int): Starting line number
301
- end (int): Ending line number
302
303
Returns:
304
str: Human-readable description of the arc
305
"""
306
307
def source_token_lines(self):
308
"""
309
Get tokenized source lines for syntax highlighting.
310
311
Returns:
312
Iterable[list[tuple[str, str]]]: Lists of (token_type, token_text) tuples
313
"""
314
315
def code_regions(self):
316
"""
317
Get code regions (functions, classes) in the file.
318
319
Returns:
320
Iterable[CodeRegion]: Code regions with metadata
321
"""
322
323
def code_region_kinds(self):
324
"""
325
Get the kinds of code regions this reporter recognizes.
326
327
Returns:
328
Iterable[tuple[str, str]]: (kind, display_name) pairs
329
"""
330
```
331
332
Usage example:
333
334
```python
335
import coverage
336
from coverage.plugin import CodeRegion
337
338
class JSONFileReporter(coverage.FileReporter):
339
def __init__(self, filename):
340
super().__init__(filename)
341
self.filename = filename
342
with open(filename) as f:
343
self.json_data = json.load(f)
344
345
def source(self):
346
with open(self.filename) as f:
347
return f.read()
348
349
def lines(self):
350
# Determine executable lines based on JSON structure
351
return self.analyze_json_structure()
352
353
def code_regions(self):
354
regions = []
355
for key, value in self.json_data.items():
356
if isinstance(value, dict):
357
regions.append(CodeRegion(
358
kind='object',
359
name=key,
360
start=self.find_key_line(key),
361
lines=self.get_object_lines(value)
362
))
363
return regions
364
365
def analyze_json_structure(self):
366
# Custom logic to determine what constitutes "executable" JSON
367
return set(range(1, self.count_lines() + 1))
368
```
369
370
### CodeRegion Data Class
371
372
Represents a region of code with metadata for enhanced reporting.
373
374
```python { .api }
375
@dataclass
376
class CodeRegion:
377
"""
378
Represents a code region like a function or class.
379
380
Attributes:
381
- kind (str): Type of region ('function', 'class', 'method', etc.)
382
- name (str): Name of the region
383
- start (int): Starting line number
384
- lines (set[int]): All line numbers in the region
385
"""
386
kind: str
387
name: str
388
start: int
389
lines: set[int]
390
```
391
392
### Plugin Registration
393
394
Plugins are registered through a `coverage_init` function in the plugin module.
395
396
```python { .api }
397
def coverage_init(reg, options):
398
"""
399
Plugin initialization function.
400
401
Parameters:
402
- reg: Plugin registry object
403
- options (dict): Plugin configuration options
404
"""
405
# Register file tracers
406
reg.add_file_tracer(MyFileTracerPlugin())
407
408
# Register configurers
409
reg.add_configurer(MyConfigurerPlugin())
410
411
# Register dynamic context providers
412
reg.add_dynamic_context(MyContextPlugin())
413
```
414
415
Usage example:
416
417
```python
418
import coverage
419
420
class DatabaseQueryPlugin(coverage.CoveragePlugin):
421
def __init__(self, options):
422
self.connection_string = options.get('connection', 'sqlite:///:memory:')
423
self.track_queries = options.get('track_queries', True)
424
425
def dynamic_context(self, frame):
426
# Provide context based on database operations
427
if 'sqlalchemy' in frame.f_globals.get('__name__', ''):
428
return f"db_query:{frame.f_code.co_name}"
429
return None
430
431
def configure(self, config):
432
if self.track_queries:
433
config.set_option('run:contexts', ['db_operations'])
434
435
def coverage_init(reg, options):
436
plugin = DatabaseQueryPlugin(options)
437
reg.add_dynamic_context(plugin)
438
reg.add_configurer(plugin)
439
```
440
441
## Plugin Types
442
443
### File Tracer Plugins
444
445
Handle measurement of non-Python files by implementing `file_tracer()` and providing `FileTracer` instances.
446
447
```python
448
class MarkdownPlugin(coverage.CoveragePlugin):
449
"""Plugin to trace Markdown files with embedded Python code."""
450
451
def file_tracer(self, filename):
452
if filename.endswith('.md'):
453
return MarkdownTracer(filename)
454
return None
455
456
class MarkdownTracer(coverage.FileTracer):
457
def __init__(self, filename):
458
self.filename = filename
459
self.python_file = self.extract_python_code()
460
461
def source_filename(self):
462
return self.python_file
463
464
def extract_python_code(self):
465
# Extract Python code blocks from Markdown
466
# Return path to generated Python file
467
pass
468
```
469
470
### Configurer Plugins
471
472
Modify coverage.py configuration during startup by implementing `configure()`.
473
474
```python
475
class TeamConfigPlugin(coverage.CoveragePlugin):
476
"""Plugin to apply team-wide configuration standards."""
477
478
def configure(self, config):
479
# Apply team standards
480
config.set_option('run:branch', True)
481
config.set_option('run:source', ['src/', 'lib/'])
482
config.set_option('report:exclude_lines', [
483
'pragma: no cover',
484
'def __repr__',
485
'raise NotImplementedError'
486
])
487
```
488
489
### Dynamic Context Plugins
490
491
Provide dynamic context labels by implementing `dynamic_context()`.
492
493
```python
494
class TestFrameworkPlugin(coverage.CoveragePlugin):
495
"""Plugin to provide test-specific contexts."""
496
497
def dynamic_context(self, frame):
498
# Detect test framework and provide context
499
code_name = frame.f_code.co_name
500
filename = frame.f_code.co_filename
501
502
if 'test_' in code_name or '/tests/' in filename:
503
return f"test:{code_name}"
504
elif 'pytest' in str(frame.f_globals.get('__file__', '')):
505
return f"pytest:{code_name}"
506
507
return None
508
```
509
510
## Complete Plugin Example
511
512
Here's a comprehensive example of a plugin that handles custom template files:
513
514
```python
515
import coverage
516
from coverage.plugin import CodeRegion
517
import re
518
import os
519
520
class TemplatePlugin(coverage.CoveragePlugin):
521
"""Plugin for measuring coverage of custom template files."""
522
523
def __init__(self, options):
524
self.template_extensions = options.get('extensions', ['.tmpl', '.tpl'])
525
self.output_dir = options.get('output_dir', 'generated/')
526
527
def file_tracer(self, filename):
528
for ext in self.template_extensions:
529
if filename.endswith(ext):
530
return TemplateTracer(filename, self.output_dir)
531
return None
532
533
def file_reporter(self, filename):
534
for ext in self.template_extensions:
535
if filename.endswith(ext):
536
return TemplateReporter(filename)
537
return None
538
539
def sys_info(self):
540
return [
541
('template_plugin_version', '1.0.0'),
542
('template_extensions', self.template_extensions),
543
]
544
545
class TemplateTracer(coverage.FileTracer):
546
def __init__(self, template_file, output_dir):
547
self.template_file = template_file
548
self.output_dir = output_dir
549
self.python_file = self.generate_python_file()
550
551
def source_filename(self):
552
return self.python_file
553
554
def generate_python_file(self):
555
# Convert template to Python file
556
basename = os.path.basename(self.template_file)
557
python_file = os.path.join(self.output_dir, basename + '.py')
558
559
with open(self.template_file) as f:
560
template_content = f.read()
561
562
# Simple template-to-Python conversion
563
python_content = self.convert_template(template_content)
564
565
os.makedirs(self.output_dir, exist_ok=True)
566
with open(python_file, 'w') as f:
567
f.write(python_content)
568
569
return python_file
570
571
def convert_template(self, content):
572
# Convert template syntax to Python
573
# This is a simplified example
574
lines = content.split('\n')
575
python_lines = []
576
577
for line in lines:
578
if line.strip().startswith('{{ '):
579
# Template variable
580
var = line.strip()[3:-3].strip()
581
python_lines.append(f'print({var})')
582
elif line.strip().startswith('{% '):
583
# Template logic
584
logic = line.strip()[3:-3].strip()
585
python_lines.append(logic)
586
else:
587
# Static content
588
python_lines.append(f'print({repr(line)})')
589
590
return '\n'.join(python_lines)
591
592
class TemplateReporter(coverage.FileReporter):
593
def __init__(self, filename):
594
super().__init__(filename)
595
self.filename = filename
596
597
def source(self):
598
with open(self.filename) as f:
599
return f.read()
600
601
def lines(self):
602
# All non-empty lines are considered executable
603
with open(self.filename) as f:
604
lines = f.readlines()
605
606
executable = set()
607
for i, line in enumerate(lines, 1):
608
if line.strip():
609
executable.add(i)
610
611
return executable
612
613
def code_regions(self):
614
regions = []
615
with open(self.filename) as f:
616
content = f.read()
617
618
# Find template blocks
619
for match in re.finditer(r'{%\s*(\w+)', content):
620
block_type = match.group(1)
621
line_num = content[:match.start()].count('\n') + 1
622
623
regions.append(CodeRegion(
624
kind='template_block',
625
name=block_type,
626
start=line_num,
627
lines={line_num}
628
))
629
630
return regions
631
632
def coverage_init(reg, options):
633
"""Initialize the template plugin."""
634
plugin = TemplatePlugin(options)
635
reg.add_file_tracer(plugin)
636
```
637
638
To use this plugin, create a configuration file:
639
640
```ini
641
# .coveragerc
642
[run]
643
plugins = template_plugin
644
645
[template_plugin]
646
extensions = .tmpl, .tpl, .template
647
output_dir = generated/
648
```