0
# Command Line Interface
1
2
Complete CLI implementation providing file discovery, configuration loading, batch processing, and various output modes for formatting Python code from the command line.
3
4
## Capabilities
5
6
### Main CLI Entry Point
7
8
The primary command-line interface function with comprehensive option parsing and file processing.
9
10
```python { .api }
11
def main(
12
ctx: click.Context,
13
code: Optional[str] = None,
14
line_length: int = DEFAULT_LINE_LENGTH,
15
target_version: list[TargetVersion] = [],
16
pyi: bool = False,
17
ipynb: bool = False,
18
python_cell_magics: Sequence[str] = [],
19
skip_source_first_line: bool = False,
20
skip_string_normalization: bool = False,
21
skip_magic_trailing_comma: bool = False,
22
preview: bool = False,
23
unstable: bool = False,
24
enable_unstable_feature: list[Preview] = [],
25
check: bool = False,
26
diff: bool = False,
27
color: bool = False,
28
line_ranges: Sequence[str] = [],
29
fast: bool = False,
30
required_version: Optional[str] = None,
31
exclude: Optional[Pattern[str]] = None,
32
extend_exclude: Optional[Pattern[str]] = None,
33
force_exclude: Optional[Pattern[str]] = None,
34
stdin_filename: Optional[str] = None,
35
include: Pattern[str] = DEFAULT_INCLUDES,
36
workers: Optional[int] = None,
37
quiet: bool = False,
38
verbose: bool = False,
39
src: tuple[str, ...] = (),
40
config: Optional[str] = None
41
) -> None:
42
"""
43
Main CLI entry point with comprehensive option parsing.
44
45
Parameters:
46
- ctx: Click context for CLI handling
47
- code: Optional code string to format (instead of files)
48
- line_length: Maximum line length (default 88)
49
- target_version: List of Python versions to target
50
- pyi: Format as .pyi stub file
51
- ipynb: Format as Jupyter notebook
52
- python_cell_magics: IPython cell magics to recognize as Python
53
- skip_source_first_line: Skip shebang lines
54
- skip_string_normalization: Don't normalize string quotes
55
- skip_magic_trailing_comma: Don't use trailing comma splitting
56
- preview: Enable preview features
57
- unstable: Enable unstable features
58
- enable_unstable_feature: List of specific preview features to enable
59
- check: Exit with error if reformatting needed
60
- diff: Output unified diff instead of rewriting
61
- color: Colorize diff output
62
- line_ranges: Line ranges to format (e.g., "1-10,20-30")
63
- fast: Skip AST safety checks
64
- required_version: Require specific Black version
65
- exclude: File pattern to exclude (regex)
66
- extend_exclude: Additional exclude pattern
67
- force_exclude: Force exclude pattern (overrides include)
68
- stdin_filename: Filename for stdin input
69
- include: File pattern to include (default: Python files)
70
- workers: Number of parallel workers
71
- quiet: Minimal output
72
- verbose: Detailed output
73
- src: Source files/directories to format
74
- config: Configuration file path
75
76
Note:
77
Decorated with comprehensive Click options for all CLI functionality
78
"""
79
```
80
81
### File Discovery
82
83
Functions for discovering and filtering source files to format.
84
85
```python { .api }
86
def get_sources(
87
*,
88
root: Path,
89
src: tuple[str, ...],
90
quiet: bool,
91
verbose: bool,
92
include: Pattern[str],
93
exclude: Optional[Pattern[str]],
94
extend_exclude: Optional[Pattern[str]],
95
force_exclude: Optional[Pattern[str]],
96
report: Report,
97
stdin_filename: Optional[str]
98
) -> set[Path]:
99
"""
100
Compute the set of files to format.
101
102
Parameters:
103
- root: Project root directory
104
- src: Source file/directory paths
105
- quiet: Suppress output messages
106
- verbose: Enable detailed output
107
- include: File pattern to include
108
- exclude: Base exclude pattern
109
- extend_exclude: Additional exclude pattern
110
- force_exclude: Force exclude (overrides include)
111
- report: Report object for tracking results
112
- stdin_filename: Filename when reading from stdin
113
114
Returns:
115
Set of Path objects to format
116
117
Note:
118
- Automatically discovers .py, .pyi, and .ipynb files
119
- Respects .gitignore patterns
120
- Handles recursive directory traversal
121
"""
122
```
123
124
### Configuration Loading
125
126
Functions for loading and parsing Black configuration from pyproject.toml files.
127
128
```python { .api }
129
def read_pyproject_toml(ctx: click.Context, param: click.Parameter, value: Optional[str]) -> Optional[str]:
130
"""
131
Parse Black configuration from pyproject.toml files.
132
133
Parameters:
134
- ctx: Click context
135
- param: Click parameter being processed
136
- value: Path to configuration file (optional)
137
138
Returns:
139
Path to found configuration file, or None
140
141
Note:
142
- Searches for pyproject.toml in project hierarchy
143
- Loads [tool.black] section configuration
144
- Merges with command-line arguments
145
"""
146
147
def validate_regex(ctx: click.Context, param: click.Parameter, value: Optional[str]) -> Optional[Pattern[str]]:
148
"""
149
Validate and compile regex patterns for file filtering.
150
151
Parameters:
152
- ctx: Click context
153
- param: Click parameter being validated
154
- value: Regex pattern string
155
156
Returns:
157
Compiled regex Pattern object, or None
158
159
Raises:
160
- click.BadParameter: If regex is invalid
161
"""
162
```
163
164
## File Processing Functions
165
166
### Project Discovery
167
168
Utilities from the `black.files` module for project structure analysis.
169
170
```python { .api }
171
def find_project_root(srcs: tuple[str, ...], stdin_filename: Optional[str] = None) -> tuple[Path, str]:
172
"""
173
Find the project root directory and reason.
174
175
Parameters:
176
- srcs: Source file/directory paths
177
- stdin_filename: Filename for stdin processing
178
179
Returns:
180
Tuple of (project_root_path, reason_string)
181
"""
182
183
def find_pyproject_toml(path_search_start: tuple[str, ...], stdin_filename: Optional[str] = None) -> Optional[str]:
184
"""
185
Find pyproject.toml configuration file.
186
187
Parameters:
188
- path_search_start: Paths to start search from
189
- stdin_filename: Filename for stdin processing
190
191
Returns:
192
Path to pyproject.toml file, or None if not found
193
"""
194
195
def find_user_pyproject_toml() -> Path:
196
"""
197
Find user-level pyproject.toml configuration.
198
199
Returns:
200
Path to user configuration file
201
"""
202
203
def parse_pyproject_toml(path_config: str) -> dict[str, Any]:
204
"""
205
Parse Black configuration from pyproject.toml file.
206
207
Parameters:
208
- path_config: Path to configuration file
209
210
Returns:
211
Dictionary of configuration values
212
213
Raises:
214
- OSError: If file cannot be read
215
- tomllib.TOMLDecodeError: If TOML is invalid
216
"""
217
```
218
219
### File Generation and Filtering
220
221
```python { .api }
222
def gen_python_files(
223
paths: Iterable[Path],
224
root: Path,
225
include: Pattern[str],
226
exclude: Pattern[str],
227
extend_exclude: Optional[Pattern[str]],
228
force_exclude: Optional[Pattern[str]],
229
report: Report,
230
gitignore: Optional[PathSpec] = None
231
) -> Iterator[Path]:
232
"""
233
Generate Python files from given paths with filtering.
234
235
Parameters:
236
- paths: Iterable of source paths
237
- root: Project root directory
238
- include: Include pattern
239
- exclude: Exclude pattern
240
- extend_exclude: Additional exclude pattern
241
- force_exclude: Force exclude pattern
242
- report: Report object for tracking
243
- gitignore: Optional gitignore patterns
244
245
Yields:
246
Path objects for files to process
247
"""
248
249
def get_gitignore(root: Path) -> PathSpec:
250
"""
251
Get gitignore patterns for the project.
252
253
Parameters:
254
- root: Project root directory
255
256
Returns:
257
PathSpec object with gitignore patterns
258
"""
259
260
def path_is_excluded(
261
normalized_path: str,
262
pattern: Optional[Pattern[str]]
263
) -> bool:
264
"""
265
Check if path matches exclusion pattern.
266
267
Parameters:
268
- normalized_path: Normalized file path
269
- pattern: Regex pattern to match against
270
271
Returns:
272
True if path should be excluded
273
"""
274
```
275
276
## Line Range Processing
277
278
Functions for parsing and handling line range specifications.
279
280
```python { .api }
281
def parse_line_ranges(line_ranges: Sequence[str]) -> list[tuple[int, int]]:
282
"""
283
Parse line range specifications from CLI arguments.
284
285
Parameters:
286
- line_ranges: List of line range strings (e.g., ["1-10", "20-30"])
287
288
Returns:
289
List of (start_line, end_line) tuples
290
291
Note:
292
- Supports formats: "N", "N-M", "N-", "-M"
293
- Line numbers are 1-indexed
294
- Overlapping ranges are merged
295
296
Raises:
297
- ValueError: If range format is invalid
298
"""
299
300
def sanitized_lines(
301
src_contents: str,
302
lines: Collection[tuple[int, int]]
303
) -> Collection[tuple[int, int]]:
304
"""
305
Sanitize line ranges against actual file content.
306
307
Parameters:
308
- src_contents: Source file content
309
- lines: Line ranges to sanitize
310
311
Returns:
312
Sanitized line ranges within file bounds
313
"""
314
315
def adjusted_lines(
316
original_lines: Collection[tuple[int, int]],
317
src_contents: str,
318
dst_contents: str
319
) -> Collection[tuple[int, int]]:
320
"""
321
Adjust line ranges after formatting changes.
322
323
Parameters:
324
- original_lines: Original line ranges
325
- src_contents: Original file content
326
- dst_contents: Formatted file content
327
328
Returns:
329
Adjusted line ranges accounting for formatting changes
330
"""
331
```
332
333
## Usage Examples
334
335
### Basic CLI Usage
336
337
```python
338
import subprocess
339
import sys
340
341
# Format single file
342
result = subprocess.run([
343
sys.executable, "-m", "black", "script.py"
344
], capture_output=True, text=True)
345
346
# Format with options
347
result = subprocess.run([
348
sys.executable, "-m", "black",
349
"--line-length", "79",
350
"--target-version", "py39",
351
"--diff",
352
"src/"
353
], capture_output=True, text=True)
354
355
print(result.stdout)
356
```
357
358
### Programmatic CLI Access
359
360
```python
361
import click
362
import black
363
364
# Create Click context and call main
365
ctx = click.Context(black.main)
366
try:
367
black.main.invoke(ctx,
368
src=('src/',),
369
line_length=79,
370
target_version=[black.TargetVersion.PY39],
371
diff=True,
372
check=False
373
)
374
except SystemExit as e:
375
print(f"Exit code: {e.code}")
376
```
377
378
### File Discovery
379
380
```python
381
from pathlib import Path
382
import re
383
384
# Setup for file discovery
385
root = Path(".")
386
src_paths = ("src/", "tests/")
387
include = re.compile(r"\.pyi?$")
388
exclude = re.compile(r"/(\.git|__pycache__|build)/")
389
390
report = black.Report()
391
392
# Find files to format
393
sources = black.get_sources(
394
root=root,
395
src=src_paths,
396
quiet=False,
397
verbose=True,
398
include=include,
399
exclude=exclude,
400
extend_exclude=None,
401
force_exclude=None,
402
report=report,
403
stdin_filename=None
404
)
405
406
print(f"Found {len(sources)} files to format:")
407
for source in sorted(sources):
408
print(f" {source}")
409
```
410
411
### Configuration Loading
412
413
```python
414
# Find and load configuration
415
config_path = black.find_pyproject_toml((".",), stdin_filename=None)
416
if config_path:
417
config = black.parse_pyproject_toml(config_path)
418
print(f"Loaded config from {config_path}:")
419
print(config)
420
421
# Apply config to mode
422
mode_kwargs = {}
423
if "line_length" in config:
424
mode_kwargs["line_length"] = config["line_length"]
425
if "target_version" in config:
426
target_versions = {
427
black.TargetVersion[v.upper().replace(".", "")]
428
for v in config["target_version"]
429
}
430
mode_kwargs["target_versions"] = target_versions
431
432
mode = black.Mode(**mode_kwargs)
433
```
434
435
### Line Range Formatting
436
437
```python
438
# Parse line ranges from CLI-style strings
439
range_specs = ["1-10", "20-25", "50-"]
440
line_ranges = black.parse_line_ranges(range_specs)
441
print(f"Parsed ranges: {line_ranges}")
442
443
# Format specific lines only
444
code = open("example.py").read()
445
formatted = black.format_str(
446
code,
447
mode=black.Mode(),
448
lines=line_ranges
449
)
450
```
451
452
### Batch Processing
453
454
```python
455
from concurrent.futures import ThreadPoolExecutor
456
from pathlib import Path
457
458
def format_file(file_path: Path) -> bool:
459
"""Format a single file and return if changed."""
460
try:
461
return black.format_file_in_place(
462
file_path,
463
fast=False,
464
mode=black.Mode(),
465
write_back=black.WriteBack.YES
466
)
467
except Exception as e:
468
print(f"Error formatting {file_path}: {e}")
469
return False
470
471
# Process files in parallel
472
source_files = list(Path("src").glob("**/*.py"))
473
with ThreadPoolExecutor(max_workers=4) as executor:
474
results = list(executor.map(format_file, source_files))
475
476
changed_count = sum(results)
477
print(f"Formatted {changed_count} files")
478
```
479
480
### Custom Output Handling
481
482
```python
483
import io
484
import contextlib
485
486
# Capture CLI output
487
output_buffer = io.StringIO()
488
with contextlib.redirect_stdout(output_buffer):
489
try:
490
ctx = click.Context(black.main)
491
black.main.invoke(ctx,
492
src=('example.py',),
493
diff=True,
494
color=False
495
)
496
except SystemExit:
497
pass
498
499
diff_output = output_buffer.getvalue()
500
print("Captured diff:")
501
print(diff_output)
502
```
503
504
## Types
505
506
```python { .api }
507
# Path processing types
508
PathLike = Union[str, Path]
509
PathSpec = pathspec.PathSpec
510
Pattern = re.Pattern[str]
511
512
# File processing
513
FileList = set[Path]
514
SourcePaths = tuple[str, ...]
515
516
# Configuration
517
ConfigDict = dict[str, Any]
518
LineRange = tuple[int, int]
519
LineRanges = Collection[LineRange]
520
```