0
# Formatter System
1
2
Pluggable formatter system supporting multiple code formatters like Black, Ruff, and Pyupgrade through a common interface and entry point system.
3
4
## Capabilities
5
6
### Formatter Plugin Management
7
8
Functions for discovering and creating formatter instances.
9
10
```python { .api }
11
def get_formatter_entry_points(name: str | None = None) -> tuple[EntryPoint, ...]:
12
"""
13
Get the entry points of all built-in code re-formatter plugins.
14
15
Parameters:
16
- name: Optional formatter name to filter by
17
18
Returns:
19
Tuple of entry points for formatter plugins
20
"""
21
22
def get_formatter_names() -> list[str]:
23
"""
24
Get the names of all built-in code re-formatter plugins.
25
26
Returns:
27
List of available formatter names (e.g., ['black', 'ruff', 'pyupgrade', 'none'])
28
"""
29
30
def create_formatter(name: str) -> BaseFormatter:
31
"""
32
Create a code re-formatter plugin instance by name.
33
34
Parameters:
35
- name: Name of the formatter to create
36
37
Returns:
38
Formatter instance implementing BaseFormatter interface
39
"""
40
41
ENTRY_POINT_GROUP: str = "darker.formatter"
42
"""Entry point group name for formatter plugins."""
43
```
44
45
### Base Formatter Interface
46
47
Abstract base class and common interfaces for all formatters.
48
49
```python { .api }
50
class BaseFormatter(ABC):
51
"""
52
Abstract base class for code re-formatters.
53
54
All formatter plugins must inherit from this class and implement
55
the required methods and attributes.
56
"""
57
name: str # Human-readable formatter name
58
preserves_ast: bool # Whether formatter preserves AST
59
config_section: str # Configuration section name
60
61
def read_config(self, src: tuple[str, ...], args: Namespace) -> None:
62
"""
63
Read configuration for the formatter.
64
65
Parameters:
66
- src: Source file/directory paths for configuration context
67
- args: Command line arguments namespace
68
69
Returns:
70
None (configuration is stored in self.config)
71
"""
72
73
@abstractmethod
74
def run(self, content: TextDocument, path_from_cwd: Path) -> TextDocument:
75
"""
76
Run the formatter on the given content.
77
78
Parameters:
79
- content: Source code content to format
80
- path_from_cwd: Path to the file being formatted (relative to cwd)
81
82
Returns:
83
Formatted source code content
84
"""
85
86
def get_config_path(self, config: Any) -> Path | None:
87
"""Get configuration file path if available."""
88
89
def get_line_length(self, config: Any) -> int:
90
"""Get line length setting from configuration."""
91
92
def get_exclude(self, config: Any) -> Pattern[str]:
93
"""Get file exclusion patterns."""
94
95
def get_extend_exclude(self, config: Any) -> Pattern[str]:
96
"""Get extended file exclusion patterns."""
97
98
def get_force_exclude(self, config: Any) -> Pattern[str]:
99
"""Get forced file exclusion patterns."""
100
101
class HasConfig(Generic[ConfigT]):
102
"""
103
Generic base class for formatters with typed configuration.
104
105
Type parameter ConfigT represents the formatter's configuration type.
106
"""
107
config: ConfigT
108
```
109
110
### Built-in Formatters
111
112
Concrete formatter implementations included with darker.
113
114
```python { .api }
115
class BlackFormatter(BaseFormatter):
116
"""
117
Black code formatter plugin interface.
118
119
Integrates with Black for Python code formatting with full AST preservation.
120
"""
121
name = "black"
122
preserves_ast = True
123
config_section = "tool.black"
124
125
class RuffFormatter(BaseFormatter):
126
"""
127
Ruff code formatter plugin interface.
128
129
Integrates with Ruff's formatting capabilities for fast Python code formatting.
130
"""
131
name = "ruff format"
132
preserves_ast = True
133
config_section = "tool.ruff"
134
135
class PyupgradeFormatter(BaseFormatter):
136
"""
137
Pyupgrade code formatter plugin interface.
138
139
Upgrades Python syntax for newer language versions. Does not preserve AST
140
due to syntax transformations.
141
"""
142
name = "pyupgrade"
143
preserves_ast = False
144
config_section = "tool.pyupgrade" # Custom section
145
146
class NoneFormatter(BaseFormatter):
147
"""
148
Dummy formatter that returns code unmodified.
149
150
Useful for testing or when only pre-processors (isort, flynt) are desired.
151
"""
152
name = "dummy reformat"
153
preserves_ast = True
154
config_section = "tool.none"
155
```
156
157
### Configuration Types
158
159
Type definitions for formatter configuration.
160
161
```python { .api }
162
class FormatterConfig(TypedDict, total=False):
163
"""Base class for formatter configuration."""
164
line_length: int
165
target_version: str
166
167
class BlackCompatibleConfig(FormatterConfig, total=False):
168
"""Configuration for Black-compatible formatters."""
169
skip_string_normalization: bool
170
skip_magic_trailing_comma: bool
171
preview: bool
172
173
class BlackModeAttributes(TypedDict, total=False):
174
"""Type definition for Black Mode configuration items."""
175
target_versions: Set[TargetVersion]
176
line_length: int
177
string_normalization: bool
178
magic_trailing_comma: bool
179
preview: bool
180
```
181
182
### Configuration Utilities
183
184
Helper functions for reading and validating formatter configuration.
185
186
```python { .api }
187
def validate_target_versions(target_versions: List[str]) -> Set[TargetVersion]:
188
"""
189
Validate target-version configuration option.
190
191
Parameters:
192
- target_versions: List of target version strings
193
194
Returns:
195
Set of validated TargetVersion enum values
196
"""
197
198
def read_black_compatible_cli_args(
199
config: BlackCompatibleConfig,
200
line_length: int,
201
) -> List[str]:
202
"""
203
Convert Black-compatible configuration to CLI arguments.
204
205
Parameters:
206
- config: Black-compatible configuration dictionary
207
- line_length: Line length setting
208
209
Returns:
210
List of CLI arguments for the formatter
211
"""
212
```
213
214
## Usage Examples
215
216
### Basic Formatter Usage
217
218
```python
219
from darker.formatters import create_formatter, get_formatter_names
220
from darkgraylib.utils import TextDocument
221
222
# List available formatters
223
formatters = get_formatter_names()
224
print(f"Available formatters: {formatters}")
225
226
# Create a Black formatter
227
black_formatter = create_formatter("black")
228
print(f"Formatter: {black_formatter.name}")
229
print(f"Preserves AST: {black_formatter.preserves_ast}")
230
231
# Read configuration
232
config = black_formatter.read_config(
233
src=("src/",),
234
config="pyproject.toml"
235
)
236
237
# Format code
238
source_code = TextDocument.from_lines([
239
"def hello( name ):",
240
" print( f'Hello {name}!' )",
241
""
242
])
243
244
formatted_code = black_formatter.run(source_code, config)
245
print(formatted_code.string)
246
```
247
248
### Custom Formatter Plugin
249
250
```python
251
from darker.formatters.base_formatter import BaseFormatter
252
from darkgraylib.utils import TextDocument
253
from typing import Any
254
255
class MyFormatter(BaseFormatter):
256
"""Custom formatter example."""
257
258
name = "my-formatter"
259
preserves_ast = True
260
config_section = "tool.my-formatter"
261
262
def read_config(self, src: tuple[str, ...], config: str) -> dict:
263
"""Read custom configuration."""
264
# Load configuration from file or use defaults
265
return {
266
'line_length': 88,
267
'indent_size': 4,
268
}
269
270
def run(self, content: TextDocument, config: dict) -> TextDocument:
271
"""Apply custom formatting."""
272
# Implement custom formatting logic
273
lines = content.lines
274
formatted_lines = []
275
276
for line in lines:
277
# Example: ensure proper indentation
278
stripped = line.lstrip()
279
if stripped:
280
indent_level = (len(line) - len(stripped)) // config['indent_size']
281
formatted_line = ' ' * (indent_level * config['indent_size']) + stripped
282
else:
283
formatted_line = ''
284
formatted_lines.append(formatted_line)
285
286
return TextDocument.from_lines(formatted_lines)
287
288
# Register the formatter (would typically be done via entry points)
289
# This is just for illustration
290
formatter = MyFormatter()
291
```
292
293
### Formatter Configuration
294
295
```python
296
from darker.formatters import create_formatter
297
from darker.formatters.formatter_config import BlackCompatibleConfig
298
299
# Create formatter with specific configuration
300
formatter = create_formatter("black")
301
302
# Custom configuration
303
custom_config: BlackCompatibleConfig = {
304
'line_length': 100,
305
'skip_string_normalization': True,
306
'skip_magic_trailing_comma': False,
307
'target_version': 'py39',
308
'preview': False,
309
}
310
311
# Read configuration from file
312
config = formatter.read_config(
313
src=("src/", "tests/"),
314
config="pyproject.toml"
315
)
316
317
# Check configuration properties
318
line_length = formatter.get_line_length(config)
319
exclude_pattern = formatter.get_exclude(config)
320
print(f"Line length: {line_length}")
321
print(f"Exclude pattern: {exclude_pattern.pattern}")
322
```
323
324
### Entry Point Registration
325
326
To register a custom formatter plugin, add to your package's setup.py or pyproject.toml:
327
328
```toml
329
[project.entry-points."darker.formatter"]
330
my-formatter = "mypackage.formatters:MyFormatter"
331
```
332
333
```python
334
# setup.py equivalent
335
entry_points={
336
"darker.formatter": [
337
"my-formatter = mypackage.formatters:MyFormatter",
338
],
339
}
340
```
341
342
### Working with Formatter Results
343
344
```python
345
from darker.formatters import create_formatter
346
from darkgraylib.utils import TextDocument
347
348
formatter = create_formatter("black")
349
config = formatter.read_config(("src/",), "pyproject.toml")
350
351
original = TextDocument.from_lines([
352
"def poorly_formatted(x,y):",
353
" return x+y"
354
])
355
356
formatted = formatter.run(original, config)
357
358
if original != formatted:
359
print("Code was reformatted:")
360
print(f"Original:\n{original.string}")
361
print(f"Formatted:\n{formatted.string}")
362
else:
363
print("No formatting changes needed")
364
```