0
# Custom Formatting
1
2
Base classes and interfaces for creating custom output formatters to control how flake8 violations are displayed and reported. This enables integration with different tools and customized output formats.
3
4
## Capabilities
5
6
### Base Formatter Class
7
8
Abstract base class for all flake8 output formatters.
9
10
```python { .api }
11
class BaseFormatter:
12
"""
13
Base class defining the formatter interface for custom output formatting.
14
15
All custom formatters must inherit from this class and implement the
16
required methods to handle flake8 violation output formatting.
17
"""
18
19
def __init__(self, options) -> None:
20
"""
21
Initialize formatter with parsed options.
22
23
This method is called automatically when the formatter is created.
24
Subclasses should not need to override this unless they need custom
25
initialization logic.
26
27
Parameters:
28
options: argparse.Namespace containing parsed command-line options
29
and configuration settings
30
"""
31
32
def start(self) -> None:
33
"""
34
Called when flake8 starts processing files.
35
36
Override this method to perform any setup operations needed
37
before violation reporting begins (e.g., opening files, writing
38
headers, initializing data structures).
39
40
Example:
41
def start(self):
42
if self.filename:
43
self.output_fd = open(self.filename, 'w')
44
print("Starting flake8 analysis...")
45
"""
46
47
def stop(self) -> None:
48
"""
49
Called when flake8 finishes processing all files.
50
51
Override this method to perform cleanup operations after all
52
violations have been reported (e.g., closing files, writing
53
footers, finalizing reports).
54
55
Example:
56
def stop(self):
57
if self.output_fd:
58
self.output_fd.close()
59
print("Analysis complete.")
60
"""
61
62
def format(self, error) -> str | None:
63
"""
64
Format a single violation for output.
65
66
This is the core method that must be implemented by all custom
67
formatters. It receives a Violation object and should return
68
a formatted string representation.
69
70
Parameters:
71
error: Violation object containing error details
72
- error.code: Error code (e.g., "E501")
73
- error.filename: File path
74
- error.line_number: Line number
75
- error.column_number: Column number
76
- error.text: Error message
77
78
Returns:
79
str: Formatted string representation of the violation
80
None: If the violation should be suppressed from output
81
82
Example:
83
def format(self, error):
84
return f"{error.filename}:{error.line_number}: {error.code} {error.text}"
85
"""
86
87
def show_statistics(self, statistics) -> None:
88
"""
89
Display statistics summary after all files are processed.
90
91
Override this method to customize how violation statistics
92
are displayed at the end of flake8 execution.
93
94
Parameters:
95
statistics: Statistics object containing violation counts and summaries
96
97
Example:
98
def show_statistics(self, statistics):
99
for stat in statistics.statistics:
100
print(f"{stat.error_code}: {stat.count} occurrences")
101
"""
102
103
def show_benchmarks(self, benchmarks) -> None:
104
"""
105
Display performance benchmarks after execution.
106
107
Override this method to customize how execution timing and
108
performance metrics are displayed.
109
110
Parameters:
111
benchmarks: Benchmark data containing timing information
112
"""
113
114
@property
115
def options(self):
116
"""Access to parsed configuration options."""
117
118
@property
119
def filename(self) -> str | None:
120
"""Output filename if specified, None for stdout/stderr."""
121
122
@property
123
def output_fd(self):
124
"""File descriptor for output (set during start())."""
125
```
126
127
### Built-in Formatter Examples
128
129
Reference implementations showing common formatting patterns.
130
131
```python { .api }
132
class SimpleFormatter(BaseFormatter):
133
"""
134
Abstract base class for simple line-based formatters.
135
136
Provides common functionality for formatters that output one line
137
per violation using a template string format.
138
"""
139
140
error_format: str
141
"""
142
Format string template for violation output.
143
144
Must be defined by subclasses. Uses old-style Python string formatting
145
with named parameters:
146
- %(code)s: Error code
147
- %(text)s: Error message
148
- %(path)s: File path
149
- %(row)d: Line number
150
- %(col)d: Column number
151
152
Example:
153
error_format = "%(path)s:%(row)d:%(col)d: %(code)s %(text)s"
154
"""
155
156
def format(self, error) -> str | None:
157
"""Format violation using the error_format template."""
158
```
159
160
## Custom Formatter Examples
161
162
### JSON Formatter
163
164
```python
165
from flake8.formatting.base import BaseFormatter
166
import json
167
168
class JSONFormatter(BaseFormatter):
169
"""Output flake8 violations in JSON format."""
170
171
def __init__(self, options):
172
super().__init__(options)
173
self.violations = []
174
175
def start(self):
176
"""Initialize violation collection."""
177
self.violations = []
178
if self.filename:
179
self.output_fd = open(self.filename, 'w')
180
181
def format(self, error):
182
"""Collect violation data instead of immediate output."""
183
violation_data = {
184
'file': error.filename,
185
'line': error.line_number,
186
'column': error.column_number,
187
'code': error.code,
188
'message': error.text,
189
'physical_line': error.physical_line
190
}
191
self.violations.append(violation_data)
192
return None # Suppress immediate output
193
194
def stop(self):
195
"""Output collected violations as JSON."""
196
output_data = {
197
'flake8_version': '7.3.0',
198
'violations': self.violations,
199
'total_violations': len(self.violations)
200
}
201
202
json_output = json.dumps(output_data, indent=2)
203
204
if self.output_fd:
205
self.output_fd.write(json_output)
206
self.output_fd.close()
207
else:
208
print(json_output)
209
210
def show_statistics(self, statistics):
211
"""Statistics are included in final JSON output."""
212
pass # Already handled in stop()
213
214
# Usage with programmatic API
215
from flake8.api import legacy
216
217
style_guide = legacy.get_style_guide()
218
style_guide.init_report(JSONFormatter)
219
report = style_guide.check_files(['myproject/'])
220
```
221
222
### HTML Report Formatter
223
224
```python
225
from flake8.formatting.base import BaseFormatter
226
from html import escape
227
import os
228
229
class HTMLFormatter(BaseFormatter):
230
"""Generate HTML report with violation details."""
231
232
def __init__(self, options):
233
super().__init__(options)
234
self.violations_by_file = {}
235
236
def start(self):
237
"""Start HTML document."""
238
if self.filename:
239
self.output_fd = open(self.filename, 'w')
240
241
html_header = """
242
<!DOCTYPE html>
243
<html>
244
<head>
245
<title>Flake8 Code Quality Report</title>
246
<style>
247
body { font-family: Arial, sans-serif; margin: 20px; }
248
.file-section { margin-bottom: 30px; border: 1px solid #ddd; padding: 15px; }
249
.violation { margin: 5px 0; padding: 5px; background: #f5f5f5; }
250
.error { border-left: 4px solid #ff4444; }
251
.warning { border-left: 4px solid #ffaa00; }
252
.code { font-family: monospace; font-weight: bold; }
253
.summary { background: #e8f4f8; padding: 15px; margin-bottom: 20px; }
254
</style>
255
</head>
256
<body>
257
<h1>Flake8 Code Quality Report</h1>
258
"""
259
260
if self.output_fd:
261
self.output_fd.write(html_header)
262
else:
263
print(html_header)
264
265
def format(self, error):
266
"""Collect violations by file."""
267
filename = error.filename
268
if filename not in self.violations_by_file:
269
self.violations_by_file[filename] = []
270
271
self.violations_by_file[filename].append(error)
272
return None # Suppress immediate output
273
274
def stop(self):
275
"""Generate complete HTML report."""
276
total_violations = sum(len(violations) for violations in self.violations_by_file.values())
277
278
# Summary section
279
html_content = f"""
280
<div class="summary">
281
<h2>Summary</h2>
282
<p><strong>Total Files:</strong> {len(self.violations_by_file)}</p>
283
<p><strong>Total Violations:</strong> {total_violations}</p>
284
</div>
285
"""
286
287
# Violations by file
288
for filename, violations in self.violations_by_file.items():
289
html_content += f"""
290
<div class="file-section">
291
<h3>{escape(filename)} ({len(violations)} violations)</h3>
292
"""
293
294
for violation in violations:
295
violation_class = "error" if violation.code.startswith('E') else "warning"
296
html_content += f"""
297
<div class="violation {violation_class}">
298
<span class="code">{escape(violation.code)}</span>
299
Line {violation.line_number}, Column {violation.column_number}:
300
{escape(violation.text)}
301
</div>
302
"""
303
304
html_content += "</div>"
305
306
html_footer = """
307
</body>
308
</html>
309
"""
310
311
full_html = html_content + html_footer
312
313
if self.output_fd:
314
self.output_fd.write(full_html)
315
self.output_fd.close()
316
else:
317
print(full_html)
318
319
# Usage
320
style_guide = legacy.get_style_guide(output_file='report.html')
321
style_guide.init_report(HTMLFormatter)
322
report = style_guide.check_files(['myproject/'])
323
print("HTML report generated: report.html")
324
```
325
326
### Team Dashboard Formatter
327
328
```python
329
from flake8.formatting.base import BaseFormatter
330
import requests
331
import json
332
from datetime import datetime
333
334
class TeamDashboardFormatter(BaseFormatter):
335
"""Send flake8 results to team dashboard API."""
336
337
def __init__(self, options):
338
super().__init__(options)
339
self.violations = []
340
self.start_time = datetime.now()
341
342
# Get dashboard config from environment or options
343
self.dashboard_url = getattr(options, 'dashboard_url',
344
os.environ.get('FLAKE8_DASHBOARD_URL'))
345
self.project_name = getattr(options, 'project_name',
346
os.environ.get('PROJECT_NAME', 'unknown'))
347
348
def format(self, error):
349
"""Collect violation data for dashboard."""
350
self.violations.append({
351
'file': error.filename,
352
'line': error.line_number,
353
'column': error.column_number,
354
'code': error.code,
355
'message': error.text,
356
'severity': 'error' if error.code.startswith('E') else 'warning'
357
})
358
359
# Also output to console
360
return f"{error.filename}:{error.line_number}:{error.column_number}: {error.code} {error.text}"
361
362
def stop(self):
363
"""Send results to team dashboard."""
364
if not self.dashboard_url:
365
print("Warning: No dashboard URL configured")
366
return
367
368
end_time = datetime.now()
369
duration = (end_time - self.start_time).total_seconds()
370
371
# Prepare data for dashboard
372
dashboard_data = {
373
'project': self.project_name,
374
'timestamp': self.start_time.isoformat(),
375
'duration_seconds': duration,
376
'total_violations': len(self.violations),
377
'error_count': len([v for v in self.violations if v['severity'] == 'error']),
378
'warning_count': len([v for v in self.violations if v['severity'] == 'warning']),
379
'violations': self.violations,
380
'files_checked': len(set(v['file'] for v in self.violations)),
381
'flake8_version': '7.3.0'
382
}
383
384
try:
385
response = requests.post(
386
f"{self.dashboard_url}/api/flake8-results",
387
json=dashboard_data,
388
headers={'Content-Type': 'application/json'},
389
timeout=30
390
)
391
392
if response.status_code == 200:
393
print(f"β Results sent to team dashboard: {self.dashboard_url}")
394
dashboard_response = response.json()
395
if 'report_url' in dashboard_response:
396
print(f"π View report: {dashboard_response['report_url']}")
397
else:
398
print(f"β οΈ Dashboard upload failed: {response.status_code}")
399
400
except requests.RequestException as e:
401
print(f"β Failed to send results to dashboard: {e}")
402
403
def show_statistics(self, statistics):
404
"""Display local statistics summary."""
405
print(f"\nπ Quality Summary for {self.project_name}:")
406
print(f" Total violations: {len(self.violations)}")
407
print(f" Files affected: {len(set(v['file'] for v in self.violations))}")
408
409
# Group by error code
410
code_counts = {}
411
for violation in self.violations:
412
code = violation['code']
413
code_counts[code] = code_counts.get(code, 0) + 1
414
415
print(" Top violation types:")
416
for code, count in sorted(code_counts.items(), key=lambda x: x[1], reverse=True)[:5]:
417
print(f" {code}: {count}")
418
419
# Usage in CI/CD pipeline
420
import os
421
os.environ['FLAKE8_DASHBOARD_URL'] = 'https://dashboard.example.com'
422
os.environ['PROJECT_NAME'] = 'my-python-project'
423
424
style_guide = legacy.get_style_guide()
425
style_guide.init_report(TeamDashboardFormatter)
426
report = style_guide.check_files(['src/', 'tests/'])
427
```
428
429
### Custom Color Formatter
430
431
```python
432
from flake8.formatting.base import BaseFormatter
433
434
class ColorFormatter(BaseFormatter):
435
"""Formatter with custom color coding for different violation types."""
436
437
# ANSI color codes
438
COLORS = {
439
'red': '\033[91m',
440
'yellow': '\033[93m',
441
'blue': '\033[94m',
442
'green': '\033[92m',
443
'cyan': '\033[96m',
444
'bold': '\033[1m',
445
'reset': '\033[0m'
446
}
447
448
def __init__(self, options):
449
super().__init__(options)
450
# Disable colors if outputting to file or if requested
451
self.use_colors = (not self.filename and
452
getattr(options, 'color', 'auto') != 'never' and
453
hasattr(sys.stdout, 'isatty') and sys.stdout.isatty())
454
455
def colorize(self, text, color):
456
"""Apply color to text if colors are enabled."""
457
if not self.use_colors:
458
return text
459
return f"{self.COLORS.get(color, '')}{text}{self.COLORS['reset']}"
460
461
def format(self, error):
462
"""Format violation with appropriate colors."""
463
# Choose color based on error type
464
if error.code.startswith('E'):
465
# Style errors in red
466
code_color = 'red'
467
severity = 'ERROR'
468
elif error.code.startswith('W'):
469
# Style warnings in yellow
470
code_color = 'yellow'
471
severity = 'WARN'
472
elif error.code.startswith('F'):
473
# Style flakes errors in cyan
474
code_color = 'cyan'
475
severity = 'FLAKE'
476
else:
477
# Other codes in blue
478
code_color = 'blue'
479
severity = 'OTHER'
480
481
# Format components with colors
482
filename = self.colorize(error.filename, 'bold')
483
line_col = self.colorize(f"{error.line_number}:{error.column_number}", 'green')
484
code = self.colorize(error.code, code_color)
485
severity_label = self.colorize(f"[{severity}]", code_color)
486
487
return f"{filename}:{line_col} {severity_label} {code} {error.text}"
488
489
def show_statistics(self, statistics):
490
"""Show colorized statistics summary."""
491
if not hasattr(statistics, 'statistics'):
492
return
493
494
print(self.colorize("\nπ Violation Statistics:", 'bold'))
495
496
for stat in statistics.statistics:
497
count_color = 'red' if stat.count > 10 else 'yellow' if stat.count > 5 else 'green'
498
count_str = self.colorize(str(stat.count), count_color)
499
code_str = self.colorize(stat.error_code, 'cyan')
500
print(f" {count_str} Γ {code_str} {stat.message}")
501
502
# Usage
503
import sys
504
style_guide = legacy.get_style_guide(color='always', statistics=True)
505
style_guide.init_report(ColorFormatter)
506
report = style_guide.check_files(['myproject/'])
507
```