or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

configuration.mdcore-coverage.mddata-storage.mdexceptions.mdindex.mdplugins.mdreporting.md

plugins.mddocs/

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

```