Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/black/__init__.py: 17%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

753 statements  

1import io 

2import json 

3import os 

4import platform 

5import re 

6import sys 

7import tokenize 

8import traceback 

9from collections.abc import ( 

10 Collection, 

11 Generator, 

12 MutableMapping, 

13 Sequence, 

14) 

15from contextlib import nullcontext 

16from dataclasses import replace 

17from datetime import datetime, timezone 

18from enum import Enum 

19from json.decoder import JSONDecodeError 

20from pathlib import Path 

21from re import Pattern 

22from typing import Any 

23 

24import click 

25from click.core import ParameterSource 

26from mypy_extensions import mypyc_attr 

27from pathspec import GitIgnoreSpec 

28from pathspec.patterns.gitignore import GitIgnorePatternError 

29 

30import black.cache as cache_module 

31from _black_version import version as __version__ 

32from black.cache import Cache, get_cache_dir 

33from black.comments import normalize_fmt_off 

34from black.const import ( 

35 DEFAULT_EXCLUDES, 

36 DEFAULT_INCLUDES, 

37 DEFAULT_LINE_LENGTH, 

38 STDIN_PLACEHOLDER, 

39) 

40from black.files import ( 

41 best_effort_relative_path, 

42 find_project_root, 

43 find_pyproject_toml, 

44 find_user_pyproject_toml, 

45 gen_python_files, 

46 get_gitignore, 

47 parse_pyproject_toml, 

48 path_is_excluded, 

49 resolves_outside_root_or_cannot_stat, 

50 wrap_stream_for_windows, 

51) 

52from black.handle_ipynb_magics import ( 

53 PYTHON_CELL_MAGICS, 

54 jupyter_dependencies_are_installed, 

55 mask_cell, 

56 put_trailing_semicolon_back, 

57 remove_trailing_semicolon, 

58 unmask_cell, 

59 validate_cell, 

60) 

61from black.linegen import LN, LineGenerator, transform_line 

62from black.lines import EmptyLineTracker, LinesBlock 

63from black.mode import FUTURE_FLAG_TO_FEATURE, VERSION_TO_FEATURES, Feature 

64from black.mode import Mode as Mode # re-exported 

65from black.mode import Preview, TargetVersion, supports_feature 

66from black.nodes import STARS, is_number_token, is_simple_decorator_expression, syms 

67from black.output import color_diff, diff, dump_to_file, err, ipynb_diff, out 

68from black.parsing import ( # noqa F401 

69 ASTSafetyError, 

70 InvalidInput, 

71 SourceASTParseError, 

72 lib2to3_parse, 

73 parse_ast, 

74 stringify_ast, 

75) 

76from black.ranges import ( 

77 adjusted_lines, 

78 convert_unchanged_lines, 

79 parse_line_ranges, 

80 sanitized_lines, 

81) 

82from black.report import Changed, NothingChanged, Report 

83from blib2to3 import pygram 

84from blib2to3.pgen2 import token 

85from blib2to3.pytree import Leaf, Node 

86 

87COMPILED = Path(__file__).suffix in (".pyd", ".so") 

88 

89# types 

90FileContent = str 

91Encoding = str 

92NewLine = str 

93 

94 

95class WriteBack(Enum): 

96 NO = 0 

97 YES = 1 

98 DIFF = 2 

99 CHECK = 3 

100 COLOR_DIFF = 4 

101 

102 @classmethod 

103 def from_configuration( 

104 cls, *, check: bool, diff: bool, color: bool = False 

105 ) -> "WriteBack": 

106 if check and not diff: 

107 return cls.CHECK 

108 

109 if diff and color: 

110 return cls.COLOR_DIFF 

111 

112 return cls.DIFF if diff else cls.YES 

113 

114 

115# Legacy name, left for integrations. 

116FileMode = Mode 

117 

118 

119def read_pyproject_toml( 

120 ctx: click.Context, param: click.Parameter | None, value: str | None 

121) -> str | None: 

122 """Inject Black configuration from "pyproject.toml" into defaults in `ctx`. 

123 

124 Returns the path to a successfully found and read configuration file, None 

125 otherwise. 

126 """ 

127 if not value: 

128 value = find_pyproject_toml( 

129 ctx.params.get("src", ()), ctx.params.get("stdin_filename", None) 

130 ) 

131 if value is None: 

132 return None 

133 

134 try: 

135 config = parse_pyproject_toml(value) 

136 except (OSError, ValueError) as e: 

137 raise click.FileError( 

138 filename=value, hint=f"Error reading configuration file: {e}" 

139 ) from None 

140 

141 if not config: 

142 return None 

143 else: 

144 spellcheck_pyproject_toml_keys(ctx, list(config), value) 

145 # Sanitize the values to be Click friendly. For more information please see: 

146 # https://github.com/psf/black/issues/1458 

147 # https://github.com/pallets/click/issues/1567 

148 config = { 

149 k: str(v) if not isinstance(v, (list, dict)) else v 

150 for k, v in config.items() 

151 } 

152 

153 target_version = config.get("target_version") 

154 if target_version is not None and not isinstance(target_version, list): 

155 raise click.BadOptionUsage( 

156 "target-version", "Config key target-version must be a list" 

157 ) 

158 

159 exclude = config.get("exclude") 

160 if exclude is not None and not isinstance(exclude, str): 

161 raise click.BadOptionUsage("exclude", "Config key exclude must be a string") 

162 

163 include = config.get("include") 

164 if include is not None and not isinstance(include, str): 

165 raise click.BadOptionUsage("include", "Config key include must be a string") 

166 

167 extend_exclude = config.get("extend_exclude") 

168 if extend_exclude is not None and not isinstance(extend_exclude, str): 

169 raise click.BadOptionUsage( 

170 "extend-exclude", "Config key extend-exclude must be a string" 

171 ) 

172 

173 force_exclude = config.get("force_exclude") 

174 if force_exclude is not None and not isinstance(force_exclude, str): 

175 raise click.BadOptionUsage( 

176 "force-exclude", "Config key force-exclude must be a string" 

177 ) 

178 

179 line_ranges = config.get("line_ranges") 

180 if line_ranges is not None: 

181 raise click.BadOptionUsage( 

182 "line-ranges", "Cannot use line-ranges in the pyproject.toml file." 

183 ) 

184 

185 default_map: dict[str, Any] = {} 

186 if ctx.default_map: 

187 default_map.update(ctx.default_map) 

188 default_map.update(config) 

189 

190 ctx.default_map = default_map 

191 return value 

192 

193 

194def spellcheck_pyproject_toml_keys( 

195 ctx: click.Context, config_keys: list[str], config_file_path: str 

196) -> None: 

197 invalid_keys: list[str] = [] 

198 available_config_options = {param.name for param in ctx.command.params} 

199 invalid_keys = [key for key in config_keys if key not in available_config_options] 

200 if invalid_keys: 

201 keys_str = ", ".join(map(repr, invalid_keys)) 

202 out( 

203 f"Invalid config keys detected: {keys_str} (in {config_file_path})", 

204 fg="red", 

205 ) 

206 

207 

208def target_version_option_callback( 

209 c: click.Context, p: click.Option | click.Parameter, v: tuple[str, ...] 

210) -> list[TargetVersion]: 

211 """Compute the target versions from a --target-version flag. 

212 

213 This is its own function because mypy couldn't infer the type correctly 

214 when it was a lambda, causing mypyc trouble. 

215 """ 

216 return [TargetVersion[val.upper()] for val in v] 

217 

218 

219def _target_versions_exceed_runtime( 

220 target_versions: set[TargetVersion], 

221) -> bool: 

222 """Check if ALL target versions exceed the runtime Python version. 

223 

224 If any target version is at or below the runtime version, the AST 

225 safety check can succeed for that version's features, so the warning 

226 would be spurious. 

227 """ 

228 if not target_versions: 

229 return False 

230 min_target_minor = min(tv.value for tv in target_versions) 

231 return min_target_minor > sys.version_info[1] 

232 

233 

234def _version_mismatch_message(target_versions: set[TargetVersion]) -> str: 

235 max_target = max(target_versions, key=lambda tv: tv.value) 

236 runtime = f"{sys.version_info[0]}.{sys.version_info[1]}" 

237 return ( 

238 f"Python {runtime} cannot parse code formatted for" 

239 f" {max_target.pretty()}. To fix this: run Black with" 

240 f" {max_target.pretty()}, set --target-version to" 

241 f" py3{sys.version_info[1]}, or use --fast to skip the safety" 

242 " check." 

243 ) 

244 

245 

246def enable_unstable_feature_callback( 

247 c: click.Context, p: click.Option | click.Parameter, v: tuple[str, ...] 

248) -> list[Preview]: 

249 """Compute the features from an --enable-unstable-feature flag.""" 

250 return [Preview[val] for val in v] 

251 

252 

253def re_compile_maybe_verbose(regex: str) -> Pattern[str]: 

254 """Compile a regular expression string in `regex`. 

255 

256 If it contains newlines, use verbose mode. 

257 """ 

258 if "\n" in regex: 

259 regex = "(?x)" + regex 

260 compiled: Pattern[str] = re.compile(regex) 

261 return compiled 

262 

263 

264def validate_regex( 

265 ctx: click.Context, 

266 param: click.Parameter, 

267 value: str | None, 

268) -> Pattern[str] | None: 

269 try: 

270 return re_compile_maybe_verbose(value) if value is not None else None 

271 except re.error as e: 

272 raise click.BadParameter(f"Not a valid regular expression: {e}") from None 

273 

274 

275@click.command( 

276 context_settings={"help_option_names": ["-h", "--help"]}, 

277 # While Click does set this field automatically using the docstring, mypyc 

278 # (annoyingly) strips 'em so we need to set it here too. 

279 help="The uncompromising code formatter.", 

280) 

281@click.option("-c", "--code", type=str, help="Format the code passed in as a string.") 

282@click.option( 

283 "-l", 

284 "--line-length", 

285 type=int, 

286 default=DEFAULT_LINE_LENGTH, 

287 help="How many characters per line to allow.", 

288 show_default=True, 

289) 

290@click.option( 

291 "-t", 

292 "--target-version", 

293 type=click.Choice([v.name.lower() for v in TargetVersion]), 

294 callback=target_version_option_callback, 

295 multiple=True, 

296 help=( 

297 "Python versions that should be supported by Black's output. You should" 

298 " include all versions that your code supports. By default, Black will infer" 

299 " target versions from the project metadata in pyproject.toml. If this does" 

300 " not yield conclusive results, Black will use per-file auto-detection." 

301 ), 

302) 

303@click.option( 

304 "--pyi", 

305 is_flag=True, 

306 help=( 

307 "Format all input files like typing stubs regardless of file extension. This" 

308 " is useful when piping source on standard input." 

309 ), 

310) 

311@click.option( 

312 "--ipynb", 

313 is_flag=True, 

314 help=( 

315 "Format all input files like Jupyter Notebooks regardless of file extension." 

316 " This is useful when piping source on standard input." 

317 ), 

318) 

319@click.option( 

320 "--python-cell-magics", 

321 multiple=True, 

322 help=( 

323 "When processing Jupyter Notebooks, add the given magic to the list" 

324 f" of known python-magics ({', '.join(sorted(PYTHON_CELL_MAGICS))})." 

325 " Useful for formatting cells with custom python magics." 

326 ), 

327 default=[], 

328) 

329@click.option( 

330 "-x", 

331 "--skip-source-first-line", 

332 is_flag=True, 

333 help="Skip the first line of the source code.", 

334) 

335@click.option( 

336 "-S", 

337 "--skip-string-normalization", 

338 is_flag=True, 

339 help="Don't normalize string quotes or prefixes.", 

340) 

341@click.option( 

342 "-C", 

343 "--skip-magic-trailing-comma", 

344 is_flag=True, 

345 help="Don't use trailing commas as a reason to split lines.", 

346) 

347@click.option( 

348 "--preview", 

349 is_flag=True, 

350 help=( 

351 "Enable potentially disruptive style changes that may be added to Black's main" 

352 " functionality in the next major release." 

353 ), 

354) 

355@click.option( 

356 "--unstable", 

357 is_flag=True, 

358 help=( 

359 "Enable potentially disruptive style changes that have known bugs or are not" 

360 " currently expected to make it into the stable style Black's next major" 

361 " release. Implies --preview." 

362 ), 

363) 

364@click.option( 

365 "--enable-unstable-feature", 

366 type=click.Choice([v.name for v in Preview]), 

367 callback=enable_unstable_feature_callback, 

368 multiple=True, 

369 help=( 

370 "Enable specific features included in the `--unstable` style. Requires" 

371 " `--preview`. No compatibility guarantees are provided on the behavior" 

372 " or existence of any unstable features." 

373 ), 

374) 

375@click.option( 

376 "--check", 

377 is_flag=True, 

378 help=( 

379 "Don't write the files back, just return the status. Return code 0 means" 

380 " nothing would change. Return code 1 means some files would be reformatted." 

381 " Return code 123 means there was an internal error." 

382 ), 

383) 

384@click.option( 

385 "--diff", 

386 is_flag=True, 

387 help=( 

388 "Don't write the files back, just output a diff to indicate what changes" 

389 " Black would've made. They are printed to stdout so capturing them is simple." 

390 ), 

391) 

392@click.option( 

393 "--color/--no-color", 

394 is_flag=True, 

395 help="Show (or do not show) colored diff. Only applies when --diff is given.", 

396) 

397@click.option( 

398 "--line-ranges", 

399 multiple=True, 

400 metavar="START-END", 

401 help=( 

402 "When specified, Black will try its best to only format these lines. This" 

403 " option can be specified multiple times, and a union of the lines will be" 

404 " formatted. Each range must be specified as two integers connected by a `-`:" 

405 " `<START>-<END>`. The `<START>` and `<END>` integer indices are 1-based and" 

406 " inclusive on both ends." 

407 ), 

408 default=(), 

409) 

410@click.option( 

411 "--fast/--safe", 

412 is_flag=True, 

413 help=( 

414 "By default, Black performs an AST safety check after formatting your code." 

415 " The --fast flag turns off this check and the --safe flag explicitly enables" 

416 " it. [default: --safe]" 

417 ), 

418) 

419@click.option( 

420 "--required-version", 

421 type=str, 

422 help=( 

423 "Require a specific version of Black to be running. This is useful for" 

424 " ensuring that all contributors to your project are using the same" 

425 " version, because different versions of Black may format code a little" 

426 " differently. This option can be set in a configuration file for consistent" 

427 " results across environments." 

428 ), 

429) 

430@click.option( 

431 "--exclude", 

432 type=str, 

433 callback=validate_regex, 

434 help=( 

435 "A regular expression that matches files and directories that should be" 

436 " excluded on recursive searches. An empty value means no paths are excluded." 

437 " Use forward slashes for directories on all platforms (Windows, too)." 

438 " By default, Black also ignores all paths listed in .gitignore. Changing this" 

439 f" value will override all default exclusions. [default: {DEFAULT_EXCLUDES}]" 

440 ), 

441 show_default=False, 

442) 

443@click.option( 

444 "--extend-exclude", 

445 type=str, 

446 callback=validate_regex, 

447 help=( 

448 "Like --exclude, but adds additional files and directories on top of the" 

449 " default values instead of overriding them." 

450 ), 

451) 

452@click.option( 

453 "--force-exclude", 

454 type=str, 

455 callback=validate_regex, 

456 help=( 

457 "Like --exclude, but files and directories matching this regex will be excluded" 

458 " even when they are passed explicitly as arguments. This is useful when" 

459 " invoking Black programmatically on changed files, such as in a pre-commit" 

460 " hook or editor plugin." 

461 ), 

462) 

463@click.option( 

464 "--stdin-filename", 

465 type=str, 

466 is_eager=True, 

467 help=( 

468 "The name of the file when passing it through stdin. Useful to make sure Black" 

469 " will respect the --force-exclude option on some editors that rely on using" 

470 " stdin." 

471 ), 

472) 

473@click.option( 

474 "--include", 

475 type=str, 

476 default=DEFAULT_INCLUDES, 

477 callback=validate_regex, 

478 help=( 

479 "A regular expression that matches files and directories that should be" 

480 " included on recursive searches. An empty value means all files are included" 

481 " regardless of the name. Use forward slashes for directories on all platforms" 

482 " (Windows, too). Overrides all exclusions, including from .gitignore and" 

483 " command line options." 

484 ), 

485 show_default=True, 

486) 

487@click.option( 

488 "-W", 

489 "--workers", 

490 type=click.IntRange(min=1), 

491 default=None, 

492 help=( 

493 "When Black formats multiple files, it may use a process pool to speed up" 

494 " formatting. This option controls the number of parallel workers. This can" 

495 " also be specified via the BLACK_NUM_WORKERS environment variable. Defaults" 

496 " to the number of CPUs in the system." 

497 ), 

498) 

499@click.option( 

500 "-q", 

501 "--quiet", 

502 is_flag=True, 

503 help=( 

504 "Stop emitting all non-critical output. Error messages will still be emitted" 

505 " (which can be silenced by 2>/dev/null)." 

506 ), 

507) 

508@click.option( 

509 "-v", 

510 "--verbose", 

511 is_flag=True, 

512 help=( 

513 "Emit messages about files that were not changed or were ignored due to" 

514 " exclusion patterns. If Black is using a configuration file, a message" 

515 " detailing which one it is using will be emitted." 

516 ), 

517) 

518@click.version_option( 

519 version=__version__, 

520 message=( 

521 f"%(prog)s, %(version)s (compiled: {'yes' if COMPILED else 'no'})\n" 

522 f"Python ({platform.python_implementation()}) {platform.python_version()}" 

523 ), 

524) 

525@click.argument( 

526 "src", 

527 nargs=-1, 

528 type=click.Path( 

529 exists=True, file_okay=True, dir_okay=True, readable=True, allow_dash=True 

530 ), 

531 is_eager=True, 

532 metavar="SRC ...", 

533) 

534@click.option( 

535 "--config", 

536 type=click.Path( 

537 exists=True, 

538 file_okay=True, 

539 dir_okay=False, 

540 readable=True, 

541 allow_dash=False, 

542 path_type=str, 

543 ), 

544 is_eager=True, 

545 callback=read_pyproject_toml, 

546 help="Read configuration options from a configuration file.", 

547) 

548@click.option( 

549 "--cache-dir", 

550 type=click.Path(file_okay=False, path_type=Path), 

551 help="Store the cache in this directory. Takes precedence over BLACK_CACHE_DIR.", 

552) 

553@click.option( 

554 "--no-cache", 

555 is_flag=True, 

556 help=( 

557 "Skip reading and writing the cache, forcing Black to reformat all" 

558 " included files. Overrides --cache-dir." 

559 ), 

560) 

561@click.pass_context 

562def main( 

563 ctx: click.Context, 

564 code: str | None, 

565 line_length: int, 

566 target_version: list[TargetVersion], 

567 check: bool, 

568 diff: bool, 

569 line_ranges: Sequence[str], 

570 color: bool, 

571 fast: bool, 

572 pyi: bool, 

573 ipynb: bool, 

574 python_cell_magics: Sequence[str], 

575 skip_source_first_line: bool, 

576 skip_string_normalization: bool, 

577 skip_magic_trailing_comma: bool, 

578 preview: bool, 

579 unstable: bool, 

580 enable_unstable_feature: list[Preview], 

581 quiet: bool, 

582 verbose: bool, 

583 required_version: str | None, 

584 include: Pattern[str], 

585 exclude: Pattern[str] | None, 

586 extend_exclude: Pattern[str] | None, 

587 force_exclude: Pattern[str] | None, 

588 stdin_filename: str | None, 

589 workers: int | None, 

590 src: tuple[str, ...], 

591 config: str | None, 

592 cache_dir: Path | None, 

593 no_cache: bool, 

594) -> None: 

595 """The uncompromising code formatter.""" 

596 ctx.ensure_object(dict) 

597 

598 assert sys.version_info >= (3, 10), "Black requires Python 3.10+" 

599 if sys.version_info[:3] == (3, 12, 5): 

600 out( 

601 "Python 3.12.5 has a memory safety issue that can cause Black's " 

602 "AST safety checks to fail. " 

603 "Please upgrade to Python 3.12.6 or downgrade to Python 3.12.4" 

604 ) 

605 ctx.exit(1) 

606 

607 if src and code is not None: 

608 out( 

609 main.get_usage(ctx) 

610 + "\n\n'SRC' and 'code' cannot be passed simultaneously." 

611 ) 

612 ctx.exit(1) 

613 if not src and code is None: 

614 out(main.get_usage(ctx) + "\n\nOne of 'SRC' or 'code' is required.") 

615 ctx.exit(1) 

616 

617 # It doesn't do anything if --unstable is also passed, so just allow it. 

618 if enable_unstable_feature and not (preview or unstable): 

619 out( 

620 main.get_usage(ctx) 

621 + "\n\n'--enable-unstable-feature' requires '--preview'." 

622 ) 

623 ctx.exit(1) 

624 

625 root, method = ( 

626 find_project_root(src, stdin_filename) if code is None else (None, None) 

627 ) 

628 ctx.obj["root"] = root 

629 

630 if ( 

631 code is None 

632 and root is None 

633 and not quiet 

634 and ctx.get_parameter_source("config") != ParameterSource.COMMANDLINE 

635 ): 

636 err( 

637 "No project root could be identified: the given sources share no" 

638 " common parent directory (for example, they are on different" 

639 " drives). Black will use its default configuration. Pass" 

640 " --config to select a configuration file or --quiet to silence" 

641 " this warning." 

642 ) 

643 

644 if verbose: 

645 if root: 

646 out( 

647 f"Identified `{root}` as project root containing a {method}.", 

648 fg="blue", 

649 ) 

650 

651 if config: 

652 config_source = ctx.get_parameter_source("config") 

653 user_level_config = str(find_user_pyproject_toml()) 

654 if config == user_level_config: 

655 out( 

656 "Using configuration from user-level config at " 

657 f"'{user_level_config}'.", 

658 fg="blue", 

659 ) 

660 elif config_source in ( 

661 ParameterSource.DEFAULT, 

662 ParameterSource.DEFAULT_MAP, 

663 ): 

664 out("Using configuration from project root.", fg="blue") 

665 else: 

666 out(f"Using configuration in '{config}'.", fg="blue") 

667 if ctx.default_map: 

668 for param, value in ctx.default_map.items(): 

669 out(f"{param}: {value}") 

670 

671 error_msg = "Oh no! 💥 💔 💥" 

672 if ( 

673 required_version 

674 and required_version != __version__ 

675 and required_version != __version__.split(".")[0] 

676 ): 

677 err( 

678 f"{error_msg} The required version `{required_version}` does not match" 

679 f" the running version `{__version__}`!" 

680 ) 

681 ctx.exit(1) 

682 if ipynb and pyi: 

683 err("Cannot pass both `pyi` and `ipynb` flags!") 

684 ctx.exit(1) 

685 

686 write_back = WriteBack.from_configuration(check=check, diff=diff, color=color) 

687 if cache_dir is None: 

688 cache_dir = cache_module.CACHE_DIR 

689 else: 

690 if ( 

691 not cache_dir.is_absolute() 

692 and ctx.get_parameter_source("cache_dir") == ParameterSource.DEFAULT_MAP 

693 and config is not None 

694 ): 

695 cache_dir = Path(config).resolve().parent / cache_dir 

696 cache_dir = get_cache_dir(cache_dir) 

697 if not no_cache: 

698 try: 

699 cache_dir.mkdir(parents=True, exist_ok=True) 

700 except OSError as e: 

701 no_cache = True 

702 if verbose: 

703 out( 

704 f"Unable to use cache directory `{cache_dir}`: {e}. " 

705 "Disabling the cache.", 

706 fg="blue", 

707 ) 

708 if not no_cache: 

709 pygram.initialize(cache_dir) 

710 if target_version: 

711 versions = set(target_version) 

712 else: 

713 # We'll autodetect later. 

714 versions = set() 

715 mode = Mode( 

716 target_versions=versions, 

717 line_length=line_length, 

718 is_pyi=pyi, 

719 is_ipynb=ipynb, 

720 skip_source_first_line=skip_source_first_line, 

721 string_normalization=not skip_string_normalization, 

722 magic_trailing_comma=not skip_magic_trailing_comma, 

723 preview=preview, 

724 unstable=unstable, 

725 python_cell_magics=set(python_cell_magics), 

726 enabled_features=set(enable_unstable_feature), 

727 ) 

728 

729 if not fast and _target_versions_exceed_runtime(versions): 

730 err( 

731 f"Warning: {_version_mismatch_message(versions)} Black's safety" 

732 " check verifies equivalence by parsing the AST, which fails" 

733 " when the running Python is older than the target version.", 

734 fg="yellow", 

735 ) 

736 

737 lines: list[tuple[int, int]] = [] 

738 if line_ranges: 

739 if ipynb: 

740 err("Cannot use --line-ranges with ipynb files.") 

741 ctx.exit(1) 

742 

743 try: 

744 lines = parse_line_ranges(line_ranges) 

745 except ValueError as e: 

746 err(str(e)) 

747 ctx.exit(1) 

748 

749 if code is not None: 

750 # Run in quiet mode by default with -c; the extra output isn't useful. 

751 # You can still pass -v to get verbose output. 

752 quiet = True 

753 

754 report = Report(check=check, diff=diff, quiet=quiet, verbose=verbose) 

755 

756 if code is not None: 

757 reformat_code( 

758 content=code, 

759 fast=fast, 

760 write_back=write_back, 

761 mode=mode, 

762 report=report, 

763 lines=lines, 

764 ) 

765 else: 

766 try: 

767 sources = get_sources( 

768 root=root, 

769 src=src, 

770 quiet=quiet, 

771 verbose=verbose, 

772 include=include, 

773 exclude=exclude, 

774 extend_exclude=extend_exclude, 

775 force_exclude=force_exclude, 

776 report=report, 

777 stdin_filename=stdin_filename, 

778 ) 

779 except GitIgnorePatternError: 

780 ctx.exit(1) 

781 

782 if not sources: 

783 if verbose or not quiet: 

784 out("No Python files are present to be formatted. Nothing to do 😴") 

785 if "-" in src and write_back is WriteBack.YES: 

786 _echo_stdin_unchanged() 

787 if "GITHUB_OUTPUT" in os.environ: 

788 report.write_github_outputs(Path(os.environ["GITHUB_OUTPUT"])) 

789 ctx.exit(0) 

790 

791 if len(sources) == 1: 

792 reformat_one( 

793 src=sources.pop(), 

794 fast=fast, 

795 write_back=write_back, 

796 mode=mode, 

797 report=report, 

798 lines=lines, 

799 no_cache=no_cache, 

800 cache_dir=cache_dir, 

801 ) 

802 else: 

803 from black.concurrency import reformat_many 

804 

805 if lines: 

806 err("Cannot use --line-ranges to format multiple files.") 

807 ctx.exit(1) 

808 reformat_many( 

809 sources=sources, 

810 fast=fast, 

811 write_back=write_back, 

812 mode=mode, 

813 report=report, 

814 workers=workers, 

815 no_cache=no_cache, 

816 cache_dir=cache_dir, 

817 ) 

818 

819 if verbose or not quiet: 

820 if code is None and (verbose or report.change_count or report.failure_count): 

821 out() 

822 out(error_msg if report.return_code else "All done! ✨ 🍰 ✨") 

823 if code is None: 

824 click.echo(str(report), err=True) 

825 if "GITHUB_OUTPUT" in os.environ: 

826 report.write_github_outputs(Path(os.environ["GITHUB_OUTPUT"])) 

827 ctx.exit(report.return_code) 

828 

829 

830def get_sources( 

831 *, 

832 root: Path | None, 

833 src: tuple[str, ...], 

834 quiet: bool, 

835 verbose: bool, 

836 include: Pattern[str], 

837 exclude: Pattern[str] | None, 

838 extend_exclude: Pattern[str] | None, 

839 force_exclude: Pattern[str] | None, 

840 report: "Report", 

841 stdin_filename: str | None, 

842) -> set[Path]: 

843 """Compute the set of files to be formatted.""" 

844 sources: set[Path] = set() 

845 

846 assert ( 

847 root is None or root.is_absolute() 

848 ), f"INTERNAL ERROR: `root` must be absolute but is {root}" 

849 using_default_exclude = exclude is None 

850 exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES) if exclude is None else exclude 

851 gitignore: dict[Path, GitIgnoreSpec] | None = None 

852 root_gitignore = get_gitignore(root) 

853 

854 for s in src: 

855 if s == "-" and stdin_filename: 

856 path = Path(stdin_filename) 

857 if path_is_excluded(stdin_filename, force_exclude): 

858 report.path_ignored( 

859 path, 

860 "--stdin-filename matches the --force-exclude regular expression", 

861 ) 

862 continue 

863 is_stdin = True 

864 else: 

865 path = Path(s) 

866 is_stdin = False 

867 

868 # When the sources share no common parent, there is no project root. 

869 # Fall back to a per-source root so that exclusion, symlink, and 

870 # gitignore handling still work for each source independently. 

871 if root is None: 

872 src_root = path.resolve() if path.is_dir() else path.resolve().parent 

873 else: 

874 src_root = root 

875 

876 # Compare the logic here to the logic in `gen_python_files`. 

877 if is_stdin or path.is_file(): 

878 if resolves_outside_root_or_cannot_stat(path, src_root, report): 

879 if verbose: 

880 out(f'Skipping invalid source: "{path}"', fg="red") 

881 continue 

882 

883 root_relative_path = best_effort_relative_path(path, src_root).as_posix() 

884 root_relative_path = "/" + root_relative_path 

885 

886 # Hard-exclude any files that matches the `--force-exclude` regex. 

887 if path_is_excluded(root_relative_path, force_exclude): 

888 report.path_ignored( 

889 path, "matches the --force-exclude regular expression" 

890 ) 

891 continue 

892 

893 if is_stdin: 

894 path = Path(f"{STDIN_PLACEHOLDER}{path}") 

895 

896 if path.suffix == ".ipynb" and not jupyter_dependencies_are_installed( 

897 warn=verbose or not quiet 

898 ): 

899 continue 

900 

901 if verbose: 

902 out(f'Found input source: "{path}"', fg="blue") 

903 sources.add(path) 

904 elif path.is_dir(): 

905 path = src_root / (path.resolve().relative_to(src_root)) 

906 if verbose: 

907 out(f'Found input source directory: "{path}"', fg="blue") 

908 

909 if using_default_exclude: 

910 gitignore = { 

911 src_root: root_gitignore, 

912 path: get_gitignore(path), 

913 } 

914 sources.update( 

915 gen_python_files( 

916 path.iterdir(), 

917 src_root, 

918 include, 

919 exclude, 

920 extend_exclude, 

921 force_exclude, 

922 report, 

923 gitignore, 

924 verbose=verbose, 

925 quiet=quiet, 

926 ) 

927 ) 

928 elif s == "-": 

929 if verbose: 

930 out("Found input source stdin", fg="blue") 

931 sources.add(path) 

932 else: 

933 err(f"invalid path: {s}") 

934 

935 return sources 

936 

937 

938def reformat_code( 

939 content: str, 

940 fast: bool, 

941 write_back: WriteBack, 

942 mode: Mode, 

943 report: Report, 

944 *, 

945 lines: Collection[tuple[int, int]] = (), 

946) -> None: 

947 """ 

948 Reformat and print out `content` without spawning child processes. 

949 Similar to `reformat_one`, but for string content. 

950 

951 `fast`, `write_back`, and `mode` options are passed to 

952 :func:`format_file_in_place` or :func:`format_stdin_to_stdout`. 

953 """ 

954 path = Path("<string>") 

955 try: 

956 changed = Changed.NO 

957 if format_stdin_to_stdout( 

958 content=content, fast=fast, write_back=write_back, mode=mode, lines=lines 

959 ): 

960 changed = Changed.YES 

961 report.done(path, changed) 

962 except Exception as exc: 

963 if report.verbose: 

964 traceback.print_exc() 

965 report.failed(path, exc) 

966 

967 

968# diff-shades depends on being to monkeypatch this function to operate. I know it's 

969# not ideal, but this shouldn't cause any issues ... hopefully. ~ichard26 

970@mypyc_attr(patchable=True) 

971def reformat_one( 

972 src: Path, 

973 fast: bool, 

974 write_back: WriteBack, 

975 mode: Mode, 

976 report: "Report", 

977 *, 

978 lines: Collection[tuple[int, int]] = (), 

979 no_cache: bool = False, 

980 cache_dir: Path | None = None, 

981) -> None: 

982 """Reformat a single file under `src` without spawning child processes. 

983 

984 `fast`, `write_back`, and `mode` options are passed to 

985 :func:`format_file_in_place` or :func:`format_stdin_to_stdout`. 

986 """ 

987 try: 

988 changed = Changed.NO 

989 

990 if str(src) == "-": 

991 is_stdin = True 

992 elif str(src).startswith(STDIN_PLACEHOLDER): 

993 is_stdin = True 

994 # Use the original name again in case we want to print something 

995 # to the user 

996 src = Path(str(src)[len(STDIN_PLACEHOLDER) :]) 

997 else: 

998 is_stdin = False 

999 

1000 if is_stdin: 

1001 if src.suffix == ".pyi": 

1002 mode = replace(mode, is_pyi=True) 

1003 elif src.suffix == ".ipynb": 

1004 mode = replace(mode, is_ipynb=True) 

1005 if format_stdin_to_stdout( 

1006 fast=fast, write_back=write_back, mode=mode, lines=lines 

1007 ): 

1008 changed = Changed.YES 

1009 else: 

1010 if no_cache: 

1011 cache = None 

1012 elif cache_dir is None: 

1013 cache = Cache.read(mode) 

1014 else: 

1015 cache = Cache.read(mode, cache_dir) 

1016 if cache is not None: 

1017 if not cache.is_changed(src): 

1018 changed = Changed.CACHED 

1019 if changed is not Changed.CACHED and format_file_in_place( 

1020 src, fast=fast, write_back=write_back, mode=mode, lines=lines 

1021 ): 

1022 changed = Changed.YES 

1023 can_cache_unmodified = ( 

1024 write_back in (WriteBack.CHECK, WriteBack.DIFF, WriteBack.COLOR_DIFF) 

1025 and changed is Changed.NO 

1026 ) 

1027 # Formatting only some lines doesn't make the whole file formatted, and 

1028 # the cache key doesn't include the line ranges, so don't record it. 

1029 if ( 

1030 cache is not None 

1031 and not lines 

1032 and ( 

1033 (write_back is WriteBack.YES and changed is not Changed.CACHED) 

1034 or can_cache_unmodified 

1035 ) 

1036 ): 

1037 cache.write([src]) 

1038 report.done(src, changed) 

1039 except Exception as exc: 

1040 if report.verbose: 

1041 traceback.print_exc() 

1042 report.failed(src, exc) 

1043 

1044 

1045def format_file_in_place( 

1046 src: Path, 

1047 fast: bool, 

1048 mode: Mode, 

1049 write_back: WriteBack = WriteBack.NO, 

1050 lock: Any = None, # multiprocessing.Manager().Lock() is some crazy proxy 

1051 *, 

1052 lines: Collection[tuple[int, int]] = (), 

1053) -> bool: 

1054 """Format file under `src` path. Return True if changed. 

1055 

1056 If `write_back` is DIFF, write a diff to stdout. If it is YES, write reformatted 

1057 code to the file. 

1058 `mode` and `fast` options are passed to :func:`format_file_contents`. 

1059 """ 

1060 if src.suffix == ".pyi": 

1061 mode = replace(mode, is_pyi=True) 

1062 elif src.suffix == ".ipynb": 

1063 mode = replace(mode, is_ipynb=True) 

1064 

1065 then = datetime.fromtimestamp(src.stat().st_mtime, timezone.utc) 

1066 header = b"" 

1067 with open(src, "rb") as buf: 

1068 if mode.skip_source_first_line: 

1069 header = buf.readline() 

1070 src_contents, encoding, newline = decode_bytes(buf.read(), mode) 

1071 try: 

1072 dst_contents = format_file_contents( 

1073 src_contents, fast=fast, mode=mode, lines=lines 

1074 ) 

1075 except NothingChanged: 

1076 return False 

1077 except JSONDecodeError: 

1078 raise ValueError( 

1079 f"File '{src}' cannot be parsed as valid Jupyter notebook." 

1080 ) from None 

1081 header_str = header.decode(encoding) 

1082 if newline != "\n" and header_str.endswith(newline): 

1083 # The contents are written with `newline` translation, which would turn the 

1084 # header's original line ending into e.g. "\r\r\n". 

1085 header_str = header_str[: -len(newline)] + "\n" 

1086 src_contents = header_str + src_contents 

1087 dst_contents = header_str + dst_contents 

1088 

1089 if write_back == WriteBack.YES: 

1090 with open(src, "w", encoding=encoding, newline=newline) as f: 

1091 f.write(dst_contents) 

1092 elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF): 

1093 now = datetime.now(timezone.utc) 

1094 src_name = f"{src}\t{then}" 

1095 dst_name = f"{src}\t{now}" 

1096 if mode.is_ipynb: 

1097 diff_contents = ipynb_diff(src_contents, dst_contents, src_name, dst_name) 

1098 else: 

1099 diff_contents = diff(src_contents, dst_contents, src_name, dst_name) 

1100 

1101 if write_back == WriteBack.COLOR_DIFF: 

1102 diff_contents = color_diff(diff_contents) 

1103 

1104 with lock or nullcontext(): 

1105 _write_to_stdout( 

1106 diff_contents, 

1107 encoding=encoding, 

1108 newline=newline, 

1109 wrap_for_windows=True, 

1110 ) 

1111 

1112 return True 

1113 

1114 

1115def format_stdin_to_stdout( 

1116 fast: bool, 

1117 *, 

1118 content: str | None = None, 

1119 write_back: WriteBack = WriteBack.NO, 

1120 mode: Mode, 

1121 lines: Collection[tuple[int, int]] = (), 

1122) -> bool: 

1123 """Format file on stdin. Return True if changed. 

1124 

1125 If content is None, it's read from sys.stdin. 

1126 

1127 If `write_back` is YES, write reformatted code back to stdout. If it is DIFF, 

1128 write a diff to stdout. The `mode` argument is passed to 

1129 :func:`format_file_contents`. 

1130 """ 

1131 then = datetime.now(timezone.utc) 

1132 

1133 header_str = "" 

1134 if content is None: 

1135 buf = io.BytesIO(sys.stdin.buffer.read()) 

1136 header = buf.readline() if mode.skip_source_first_line else b"" 

1137 src, encoding, newline = decode_bytes(buf.read(), mode) 

1138 header_str = header.decode(encoding) 

1139 if newline != "\n" and header_str.endswith(newline): 

1140 # The output is written with `newline` translation, which would turn 

1141 # the header's original line ending into e.g. "\r\r\n". 

1142 header_str = header_str[: -len(newline)] + "\n" 

1143 else: 

1144 src, encoding, newline = content, "utf-8", "\n" 

1145 

1146 dst = src 

1147 try: 

1148 dst = format_file_contents(src, fast=fast, mode=mode, lines=lines) 

1149 return True 

1150 

1151 except NothingChanged: 

1152 return False 

1153 

1154 finally: 

1155 src = header_str + src 

1156 dst = header_str + dst 

1157 if write_back == WriteBack.YES: 

1158 # Make sure there's a newline after the content. `_write_to_stdout()` 

1159 # translates "\n" into `newline` itself, so append a bare "\n" here; 

1160 # appending `newline` would write e.g. "\r\r\n" for CRLF input. 

1161 if dst and dst[-1] != "\n" and dst[-1] != "\r": 

1162 dst += "\n" 

1163 _write_to_stdout(dst, encoding=encoding, newline=newline) 

1164 elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF): 

1165 now = datetime.now(timezone.utc) 

1166 src_name = f"STDIN\t{then}" 

1167 dst_name = f"STDOUT\t{now}" 

1168 d = diff(src, dst, src_name, dst_name) 

1169 if write_back == WriteBack.COLOR_DIFF: 

1170 d = color_diff(d) 

1171 _write_to_stdout( 

1172 d, 

1173 encoding=encoding, 

1174 newline=newline, 

1175 wrap_for_windows=write_back == WriteBack.COLOR_DIFF, 

1176 ) 

1177 

1178 

1179def _write_to_stdout( 

1180 content: str, *, encoding: str, newline: str, wrap_for_windows: bool = False 

1181) -> None: 

1182 """Write `content` to stdout with the given encoding and newline translation. 

1183 

1184 `sys.stdout` isn't required to expose the underlying binary `buffer` (for 

1185 example ipykernel's `OutStream` in Jupyter doesn't). In that case the stream 

1186 only accepts text, so the newline translation that `io.TextIOWrapper` would do 

1187 is applied here and the encoding is left to the stream itself. 

1188 """ 

1189 buffer = getattr(sys.stdout, "buffer", None) 

1190 if buffer is None: 

1191 if newline != "\n": 

1192 content = content.replace("\n", newline) 

1193 stream = wrap_stream_for_windows(sys.stdout) if wrap_for_windows else sys.stdout 

1194 stream.write(content) 

1195 return 

1196 

1197 f = io.TextIOWrapper(buffer, encoding=encoding, newline=newline, write_through=True) 

1198 wrapped = wrap_stream_for_windows(f) if wrap_for_windows else f 

1199 wrapped.write(content) 

1200 f.detach() 

1201 

1202 

1203def _echo_stdin_unchanged() -> None: 

1204 """Copy stdin to stdout byte for byte. 

1205 

1206 Going through the text streams instead would fail on input that isn't in their 

1207 encoding (e.g. a latin-1 file) and, on Windows, turn LF line endings into CRLF. 

1208 """ 

1209 in_buffer = getattr(sys.stdin, "buffer", None) 

1210 out_buffer = getattr(sys.stdout, "buffer", None) 

1211 if in_buffer is None or out_buffer is None: 

1212 sys.stdout.write(sys.stdin.read()) 

1213 return 

1214 

1215 out_buffer.write(in_buffer.read()) 

1216 out_buffer.flush() 

1217 

1218 

1219def check_stability_and_equivalence( 

1220 src_contents: str, 

1221 dst_contents: str, 

1222 *, 

1223 mode: Mode, 

1224 lines: Collection[tuple[int, int]] = (), 

1225) -> None: 

1226 """Perform stability and equivalence checks. 

1227 

1228 Raise AssertionError if source and destination contents are not 

1229 equivalent, or if a second pass of the formatter would format the 

1230 content differently. 

1231 """ 

1232 try: 

1233 assert_equivalent(src_contents, dst_contents) 

1234 except SourceASTParseError: 

1235 raise 

1236 except ASTSafetyError: 

1237 if _target_versions_exceed_runtime(mode.target_versions): 

1238 raise ASTSafetyError( 

1239 "failed to verify equivalence of the formatted output:" 

1240 f" {_version_mismatch_message(mode.target_versions)}" 

1241 ) from None 

1242 raise 

1243 assert_stable(src_contents, dst_contents, mode=mode, lines=lines) 

1244 

1245 

1246def format_file_contents( 

1247 src_contents: str, 

1248 *, 

1249 fast: bool, 

1250 mode: Mode, 

1251 lines: Collection[tuple[int, int]] = (), 

1252) -> FileContent: 

1253 """Reformat contents of a file and return new contents. 

1254 

1255 If `fast` is False, additionally confirm that the reformatted code is 

1256 valid by calling :func:`assert_equivalent` and :func:`assert_stable` on it. 

1257 `mode` is passed to :func:`format_str`. 

1258 """ 

1259 if mode.is_ipynb: 

1260 dst_contents = format_ipynb_string(src_contents, fast=fast, mode=mode) 

1261 else: 

1262 dst_contents = format_str(src_contents, mode=mode, lines=lines) 

1263 if src_contents == dst_contents: 

1264 raise NothingChanged 

1265 

1266 if not fast and not mode.is_ipynb: 

1267 # Jupyter notebooks will already have been checked above. 

1268 check_stability_and_equivalence( 

1269 src_contents, dst_contents, mode=mode, lines=lines 

1270 ) 

1271 return dst_contents 

1272 

1273 

1274def format_cell(src: str, *, fast: bool, mode: Mode) -> str: 

1275 """Format code in given cell of Jupyter notebook. 

1276 

1277 General idea is: 

1278 

1279 - if cell has trailing semicolon, remove it; 

1280 - if cell has IPython magics, mask them; 

1281 - format cell; 

1282 - reinstate IPython magics; 

1283 - reinstate trailing semicolon (if originally present); 

1284 - strip trailing newlines. 

1285 

1286 Cells with syntax errors will not be processed, as they 

1287 could potentially be automagics or multi-line magics, which 

1288 are currently not supported. 

1289 """ 

1290 validate_cell(src, mode) 

1291 src_without_trailing_semicolon, has_trailing_semicolon = remove_trailing_semicolon( 

1292 src 

1293 ) 

1294 try: 

1295 masked_src, replacements = mask_cell(src_without_trailing_semicolon) 

1296 except SyntaxError: 

1297 raise NothingChanged from None 

1298 masked_dst = format_str(masked_src, mode=mode) 

1299 if not fast: 

1300 check_stability_and_equivalence(masked_src, masked_dst, mode=mode) 

1301 dst_without_trailing_semicolon = unmask_cell(masked_dst, replacements) 

1302 dst = put_trailing_semicolon_back( 

1303 dst_without_trailing_semicolon, has_trailing_semicolon 

1304 ) 

1305 dst = dst.rstrip("\n") 

1306 if dst == src: 

1307 raise NothingChanged from None 

1308 return dst 

1309 

1310 

1311def validate_metadata(nb: MutableMapping[str, Any]) -> None: 

1312 """If notebook is marked as non-Python, don't format it. 

1313 

1314 All notebook metadata fields are optional, see 

1315 https://nbformat.readthedocs.io/en/stable/format_description.html. So 

1316 if a notebook has empty metadata, we will try to parse it anyway. 

1317 """ 

1318 language = nb.get("metadata", {}).get("language_info", {}).get("name", None) 

1319 if language is not None and language != "python": 

1320 raise NothingChanged from None 

1321 

1322 

1323def format_ipynb_string(src_contents: str, *, fast: bool, mode: Mode) -> FileContent: 

1324 """Format Jupyter notebook. 

1325 

1326 Operate cell-by-cell, only on code cells, only for Python notebooks. 

1327 If the ``.ipynb`` originally had a trailing newline, it'll be preserved. 

1328 """ 

1329 if not src_contents: 

1330 raise NothingChanged 

1331 

1332 trailing_newline = src_contents[-1] == "\n" 

1333 modified = False 

1334 nb = json.loads(src_contents) 

1335 validate_metadata(nb) 

1336 for cell in nb["cells"]: 

1337 if cell.get("cell_type", None) == "code": 

1338 try: 

1339 src = "".join(cell["source"]) 

1340 dst = format_cell(src, fast=fast, mode=mode) 

1341 except NothingChanged: 

1342 pass 

1343 else: 

1344 cell["source"] = dst.splitlines(keepends=True) 

1345 modified = True 

1346 if modified: 

1347 dst_contents = json.dumps(nb, indent=1, ensure_ascii=False) 

1348 if trailing_newline: 

1349 dst_contents = dst_contents + "\n" 

1350 return dst_contents 

1351 else: 

1352 raise NothingChanged 

1353 

1354 

1355def format_str( 

1356 src_contents: str, *, mode: Mode, lines: Collection[tuple[int, int]] = () 

1357) -> str: 

1358 """Reformat a string and return new contents. 

1359 

1360 `mode` determines formatting options, such as how many characters per line are 

1361 allowed. Example: 

1362 

1363 >>> import black 

1364 >>> print(black.format_str("def f(arg:str='')->None:...", mode=black.Mode())) 

1365 def f(arg: str = "") -> None: 

1366 ... 

1367 

1368 A more complex example: 

1369 

1370 >>> print( 

1371 ... black.format_str( 

1372 ... "def f(arg:str='')->None: hey", 

1373 ... mode=black.Mode( 

1374 ... target_versions={black.TargetVersion.PY36}, 

1375 ... line_length=10, 

1376 ... string_normalization=False, 

1377 ... is_pyi=False, 

1378 ... ), 

1379 ... ), 

1380 ... ) 

1381 def f( 

1382 arg: str = '', 

1383 ) -> None: 

1384 hey 

1385 

1386 """ 

1387 if lines: 

1388 lines = sanitized_lines(lines, src_contents) 

1389 if not lines: 

1390 return src_contents # Nothing to format 

1391 dst_contents = _format_str_once(src_contents, mode=mode, lines=lines) 

1392 # Forced second pass to work around optional trailing commas (becoming 

1393 # forced trailing commas on pass 2) interacting differently with optional 

1394 # parentheses. Admittedly ugly. 

1395 if src_contents != dst_contents: 

1396 if lines: 

1397 lines = adjusted_lines(lines, src_contents, dst_contents) 

1398 if not lines: 

1399 # None of the requested lines survived the first pass, so there is 

1400 # nothing left to format. Passing the empty `lines` to the second 

1401 # pass would format the whole file instead. 

1402 return dst_contents 

1403 try: 

1404 dst_contents = _format_str_once(dst_contents, mode=mode, lines=lines) 

1405 except InvalidInput as exc: 

1406 # This pass parses Black's own output, so failing to parse it means 

1407 # Black produced the invalid code, not the user. Reported as an 

1408 # internal error rather than as a syntax error in the user's file, 

1409 # keeping the parse location, which points into Black's output. 

1410 log = dump_to_file(dst_contents) 

1411 exc.context = ( 

1412 f"INTERNAL ERROR: {_black_info()} produced invalid code:\n" 

1413 f"{exc.context or 'cannot parse'}" 

1414 ) 

1415 exc.details = ( 

1416 f"{exc.details or ''}\n" 

1417 "Please report a bug on https://github.com/psf/black/issues. " 

1418 f"This invalid output might be helpful: {log}" 

1419 ) 

1420 exc.args = ( 

1421 f"{exc.context}: {exc.lineno}:{exc.column}{exc.details}", 

1422 exc.lineno, 

1423 exc.column, 

1424 exc.context, 

1425 exc.details, 

1426 ) 

1427 raise 

1428 if lines: 

1429 dst_contents = _restore_unselected_trailing_blank_lines( 

1430 src_contents, dst_contents, lines 

1431 ) 

1432 return dst_contents 

1433 

1434 

1435def _restore_unselected_trailing_blank_lines( 

1436 src_contents: str, 

1437 dst_contents: str, 

1438 lines: Collection[tuple[int, int]], 

1439) -> str: 

1440 """Restore trailing whitespace-only lines outside requested line ranges.""" 

1441 src_lines = src_contents.splitlines(keepends=True) 

1442 dst_lines = dst_contents.splitlines(keepends=True) 

1443 if len(dst_lines) >= len(src_lines): 

1444 return dst_contents 

1445 

1446 suffix = src_lines[len(dst_lines) :] 

1447 first_suffix_line = len(dst_lines) + 1 

1448 if all( 

1449 not line.strip() 

1450 and not any(start <= line_number <= end for start, end in lines) 

1451 for line_number, line in enumerate(suffix, start=first_suffix_line) 

1452 ): 

1453 return dst_contents + "".join(suffix) 

1454 return dst_contents 

1455 

1456 

1457def _format_str_once( 

1458 src_contents: str, *, mode: Mode, lines: Collection[tuple[int, int]] = () 

1459) -> str: 

1460 # Use the encoding overwrite since the src_contents may contain a different 

1461 # magic encoding comment than utf-8 

1462 normalized_contents, _, newline_type = decode_bytes( 

1463 src_contents.encode("utf-8"), mode, encoding_overwrite="utf-8" 

1464 ) 

1465 

1466 src_node = lib2to3_parse( 

1467 normalized_contents.lstrip(), target_versions=mode.target_versions 

1468 ) 

1469 

1470 dst_blocks: list[LinesBlock] = [] 

1471 if mode.target_versions: 

1472 versions = mode.target_versions 

1473 else: 

1474 future_imports = get_future_imports(src_node) 

1475 versions = detect_target_versions(src_node, future_imports=future_imports) 

1476 

1477 line_generation_features = { 

1478 feature 

1479 for feature in { 

1480 Feature.PARENTHESIZED_CONTEXT_MANAGERS, 

1481 Feature.UNPARENTHESIZED_EXCEPT_TYPES, 

1482 Feature.T_STRINGS, 

1483 Feature.UNPARENTHESIZED_WALRUS_IN_SETS, 

1484 } 

1485 if supports_feature(versions, feature) 

1486 } 

1487 normalize_fmt_off(src_node, mode, lines) 

1488 if lines: 

1489 # This should be called after normalize_fmt_off. 

1490 convert_unchanged_lines(src_node, lines) 

1491 

1492 line_generator = LineGenerator(mode=mode, features=line_generation_features) 

1493 elt = EmptyLineTracker(mode=mode) 

1494 split_line_features = { 

1495 feature 

1496 for feature in { 

1497 Feature.TRAILING_COMMA_IN_CALL, 

1498 Feature.TRAILING_COMMA_IN_DEF, 

1499 } 

1500 if supports_feature(versions, feature) 

1501 } 

1502 block: LinesBlock | None = None 

1503 for current_line in line_generator.visit(src_node): 

1504 block = elt.maybe_empty_lines(current_line) 

1505 dst_blocks.append(block) 

1506 for line in transform_line( 

1507 current_line, mode=mode, features=split_line_features 

1508 ): 

1509 block.content_lines.append(str(line)) 

1510 if dst_blocks: 

1511 dst_blocks[-1].after = 0 

1512 dst_contents = [] 

1513 for block in dst_blocks: 

1514 dst_contents.extend(block.all_lines()) 

1515 if not dst_contents: 

1516 if "\n" in normalized_contents: 

1517 return newline_type 

1518 dst = "".join(dst_contents) 

1519 if not mode.is_ipynb: 

1520 dst = _keep_encoding_declaration_ignored(normalized_contents, dst) 

1521 return dst.replace("\n", newline_type) 

1522 

1523 

1524def _declared_encoding(contents: str) -> str | None: 

1525 """Return the encoding Python would decode `contents` with. 

1526 

1527 Only an encoding declaration on the first two lines counts (PEP 263). Returns 

1528 None if the declared encoding is invalid. 

1529 """ 

1530 first_two_lines = "\n".join(contents.split("\n", 2)[:2]) 

1531 try: 

1532 encoding, _ = tokenize.detect_encoding( 

1533 io.BytesIO(first_two_lines.encode("utf-8")).readline 

1534 ) 

1535 except SyntaxError: 

1536 return None 

1537 return encoding 

1538 

1539 

1540def _keep_encoding_declaration_ignored(src_contents: str, dst_contents: str) -> str: 

1541 """Retain up to two blank lines so an ignored encoding declaration stays ignored. 

1542 

1543 Python ignores an encoding declaration below the second line. Removing the 

1544 blank lines at the start of the file can move one onto the first two lines, 

1545 where it takes effect and changes how the file is decoded. 

1546 """ 

1547 src_encoding = _declared_encoding(src_contents) 

1548 for blank_lines in range(3): 

1549 retained = "\n" * blank_lines + dst_contents 

1550 if _declared_encoding(retained) == src_encoding: 

1551 return retained 

1552 return dst_contents 

1553 

1554 

1555def decode_bytes( 

1556 src: bytes, mode: Mode, *, encoding_overwrite: str | None = None 

1557) -> tuple[FileContent, Encoding, NewLine]: 

1558 """Return a tuple of (decoded_contents, encoding, newline). 

1559 

1560 `newline` is either CRLF, LF, or CR, but `decoded_contents` is decoded with 

1561 universal newlines (i.e. only contains LF). 

1562 

1563 Use the keyword only encoding_overwrite argument if the bytes are encoded 

1564 differently to their possible encoding magic comment. 

1565 """ 

1566 srcbuf = io.BytesIO(src) 

1567 

1568 # Still use detect encoding even if overwrite set because otherwise lines 

1569 # might be different 

1570 encoding, lines = tokenize.detect_encoding(srcbuf.readline) 

1571 if encoding_overwrite is not None: 

1572 encoding = encoding_overwrite 

1573 

1574 if not lines: 

1575 return "", encoding, "\n" 

1576 

1577 if lines[0][-2:] == b"\r\n": 

1578 if b"\r" in lines[0][:-2]: 

1579 newline = "\r" 

1580 else: 

1581 newline = "\r\n" 

1582 elif lines[0][-1:] == b"\n": 

1583 if b"\r" in lines[0][:-1]: 

1584 newline = "\r" 

1585 else: 

1586 newline = "\n" 

1587 else: 

1588 if b"\r" in lines[0]: 

1589 newline = "\r" 

1590 else: 

1591 newline = "\n" 

1592 

1593 srcbuf.seek(0) 

1594 with io.TextIOWrapper(srcbuf, encoding) as tiow: 

1595 return tiow.read(), encoding, newline 

1596 

1597 

1598def get_features_used( 

1599 node: Node, *, future_imports: set[str] | None = None 

1600) -> set[Feature]: 

1601 """Return a set of (relatively) new Python features used in this file. 

1602 

1603 Currently looking for: 

1604 - f-strings; 

1605 - self-documenting expressions in f-strings (f"{x=}"); 

1606 - underscores in numeric literals; 

1607 - trailing commas after * or ** in function signatures and calls; 

1608 - positional only arguments in function signatures and lambdas; 

1609 - assignment expression; 

1610 - relaxed decorator syntax; 

1611 - usage of __future__ flags (annotations); 

1612 - print / exec statements; 

1613 - parenthesized context managers; 

1614 - match statements; 

1615 - except* clause; 

1616 - variadic generics; 

1617 - lazy imports; 

1618 - starred or double-starred comprehensions. 

1619 """ 

1620 features: set[Feature] = set() 

1621 if future_imports: 

1622 features |= { 

1623 FUTURE_FLAG_TO_FEATURE[future_import] 

1624 for future_import in future_imports 

1625 if future_import in FUTURE_FLAG_TO_FEATURE 

1626 } 

1627 

1628 for n in node.pre_order(): 

1629 if n.type == token.FSTRING_START: 

1630 features.add(Feature.F_STRINGS) 

1631 elif n.type == token.TSTRING_START: 

1632 features.add(Feature.T_STRINGS) 

1633 elif ( 

1634 n.type == token.RBRACE 

1635 and n.parent is not None 

1636 and any(child.type == token.EQUAL for child in n.parent.children) 

1637 ): 

1638 features.add(Feature.DEBUG_F_STRINGS) 

1639 

1640 elif is_number_token(n): 

1641 if "_" in n.value: 

1642 features.add(Feature.NUMERIC_UNDERSCORES) 

1643 

1644 elif n.type == token.SLASH: 

1645 if n.parent and n.parent.type in { 

1646 syms.typedargslist, 

1647 syms.arglist, 

1648 syms.varargslist, 

1649 }: 

1650 features.add(Feature.POS_ONLY_ARGUMENTS) 

1651 

1652 elif n.type == token.COLONEQUAL: 

1653 features.add(Feature.ASSIGNMENT_EXPRESSIONS) 

1654 

1655 elif n.type == token.LAZY: 

1656 features.add(Feature.LAZY_IMPORTS) 

1657 

1658 elif n.type == syms.decorator: 

1659 if len(n.children) > 1 and not is_simple_decorator_expression( 

1660 n.children[1] 

1661 ): 

1662 features.add(Feature.RELAXED_DECORATORS) 

1663 

1664 elif is_unpacking_comprehension(n): 

1665 features.add(Feature.UNPACKING_IN_COMPREHENSIONS) 

1666 

1667 elif ( 

1668 n.type in {syms.typedargslist, syms.arglist} 

1669 and n.children 

1670 and n.children[-1].type == token.COMMA 

1671 ): 

1672 if n.type == syms.typedargslist: 

1673 feature = Feature.TRAILING_COMMA_IN_DEF 

1674 else: 

1675 feature = Feature.TRAILING_COMMA_IN_CALL 

1676 

1677 for ch in n.children: 

1678 if ch.type in STARS: 

1679 features.add(feature) 

1680 

1681 if ch.type == syms.argument: 

1682 for argch in ch.children: 

1683 if argch.type in STARS: 

1684 features.add(feature) 

1685 

1686 elif ( 

1687 n.type in {syms.return_stmt, syms.yield_expr} 

1688 and len(n.children) >= 2 

1689 and n.children[1].type == syms.testlist_star_expr 

1690 and any(child.type == syms.star_expr for child in n.children[1].children) 

1691 ): 

1692 features.add(Feature.UNPACKING_ON_FLOW) 

1693 

1694 elif ( 

1695 n.type == syms.annassign 

1696 and len(n.children) >= 4 

1697 and n.children[3].type == syms.testlist_star_expr 

1698 ): 

1699 features.add(Feature.ANN_ASSIGN_EXTENDED_RHS) 

1700 

1701 elif ( 

1702 n.type == syms.with_stmt 

1703 and len(n.children) > 2 

1704 and n.children[1].type == syms.atom 

1705 ): 

1706 atom_children = n.children[1].children 

1707 if ( 

1708 len(atom_children) == 3 

1709 and atom_children[0].type == token.LPAR 

1710 and _contains_asexpr(atom_children[1]) 

1711 and atom_children[2].type == token.RPAR 

1712 ): 

1713 features.add(Feature.PARENTHESIZED_CONTEXT_MANAGERS) 

1714 

1715 elif n.type == syms.match_stmt: 

1716 features.add(Feature.PATTERN_MATCHING) 

1717 

1718 elif n.type in {syms.subscriptlist, syms.trailer} and any( 

1719 child.type == syms.star_expr for child in n.children 

1720 ): 

1721 features.add(Feature.VARIADIC_GENERICS) 

1722 

1723 elif ( 

1724 n.type == syms.tname_star 

1725 and len(n.children) == 3 

1726 and n.children[2].type == syms.star_expr 

1727 ): 

1728 features.add(Feature.VARIADIC_GENERICS) 

1729 

1730 elif n.type in (syms.type_stmt, syms.typeparams): 

1731 features.add(Feature.TYPE_PARAMS) 

1732 

1733 elif ( 

1734 n.type in (syms.typevartuple, syms.paramspec, syms.typevar) 

1735 and n.children[-2].type == token.EQUAL 

1736 ): 

1737 features.add(Feature.TYPE_PARAM_DEFAULTS) 

1738 

1739 elif ( 

1740 n.type == syms.except_clause 

1741 and len(n.children) >= 2 

1742 and ( 

1743 n.children[1].type == token.STAR or n.children[1].type == syms.testlist 

1744 ) 

1745 ): 

1746 is_star_except = n.children[1].type == token.STAR 

1747 

1748 if is_star_except: 

1749 features.add(Feature.EXCEPT_STAR) 

1750 

1751 # Presence of except* pushes as clause 1 index back 

1752 has_as_clause = ( 

1753 len(n.children) >= is_star_except + 3 

1754 and n.children[is_star_except + 2].type == token.NAME 

1755 and n.children[is_star_except + 2].value == "as" # type: ignore 

1756 ) 

1757 

1758 # If there's no 'as' clause and the except expression is a testlist. 

1759 if not has_as_clause and ( 

1760 (is_star_except and n.children[2].type == syms.testlist) 

1761 or (not is_star_except and n.children[1].type == syms.testlist) 

1762 ): 

1763 features.add(Feature.UNPARENTHESIZED_EXCEPT_TYPES) 

1764 

1765 return features 

1766 

1767 

1768def is_unpacking_comprehension(node: LN) -> bool: 

1769 if node.type not in { 

1770 syms.listmaker, 

1771 syms.testlist_gexp, 

1772 syms.dictsetmaker, 

1773 syms.argument, 

1774 }: 

1775 return False 

1776 

1777 if not any( 

1778 child.type in {syms.comp_for, syms.old_comp_for} for child in node.children 

1779 ): 

1780 return False 

1781 

1782 first_child = node.children[0] 

1783 return first_child.type in {syms.star_expr, token.STAR, token.DOUBLESTAR} 

1784 

1785 

1786def _contains_asexpr(node: Node | Leaf) -> bool: 

1787 """Return True if `node` contains an as-pattern.""" 

1788 if node.type == syms.asexpr_test: 

1789 return True 

1790 elif node.type == syms.atom: 

1791 if ( 

1792 len(node.children) == 3 

1793 and node.children[0].type == token.LPAR 

1794 and node.children[2].type == token.RPAR 

1795 ): 

1796 return _contains_asexpr(node.children[1]) 

1797 elif node.type == syms.testlist_gexp: 

1798 return any(_contains_asexpr(child) for child in node.children) 

1799 return False 

1800 

1801 

1802def detect_target_versions( 

1803 node: Node, *, future_imports: set[str] | None = None 

1804) -> set[TargetVersion]: 

1805 """Detect the version to target based on the nodes used.""" 

1806 features = get_features_used(node, future_imports=future_imports) 

1807 return { 

1808 version for version in TargetVersion if features <= VERSION_TO_FEATURES[version] 

1809 } 

1810 

1811 

1812def get_future_imports(node: Node) -> set[str]: 

1813 """Return a set of __future__ imports in the file.""" 

1814 imports: set[str] = set() 

1815 

1816 def get_imports_from_children(children: list[LN]) -> Generator[str, None, None]: 

1817 for child in children: 

1818 if isinstance(child, Leaf): 

1819 if child.type == token.NAME: 

1820 yield child.value 

1821 

1822 elif child.type == syms.import_as_name: 

1823 orig_name = child.children[0] 

1824 assert isinstance(orig_name, Leaf), "Invalid syntax parsing imports" 

1825 assert orig_name.type == token.NAME, "Invalid syntax parsing imports" 

1826 yield orig_name.value 

1827 

1828 elif child.type == syms.import_as_names: 

1829 yield from get_imports_from_children(child.children) 

1830 

1831 else: 

1832 raise AssertionError("Invalid syntax parsing imports") 

1833 

1834 for child in node.children: 

1835 if child.type != syms.simple_stmt: 

1836 break 

1837 

1838 first_child = child.children[0] 

1839 if isinstance(first_child, Leaf): 

1840 # Continue looking if we see a docstring; otherwise stop. 

1841 if ( 

1842 len(child.children) == 2 

1843 and first_child.type == token.STRING 

1844 and child.children[1].type == token.NEWLINE 

1845 ): 

1846 continue 

1847 

1848 break 

1849 

1850 elif first_child.type == syms.import_from: 

1851 if first_child.children[0].type == token.LAZY: 

1852 break 

1853 

1854 module_name = first_child.children[1] 

1855 if not isinstance(module_name, Leaf) or module_name.value != "__future__": 

1856 break 

1857 

1858 imports |= set(get_imports_from_children(first_child.children[3:])) 

1859 else: 

1860 break 

1861 

1862 return imports 

1863 

1864 

1865def _black_info() -> str: 

1866 return ( 

1867 f"Black {__version__} on " 

1868 f"Python ({platform.python_implementation()}) {platform.python_version()}" 

1869 ) 

1870 

1871 

1872def assert_equivalent(src: str, dst: str) -> None: 

1873 """Raise AssertionError if `src` and `dst` aren't equivalent.""" 

1874 try: 

1875 src_ast = parse_ast(src) 

1876 except Exception as exc: 

1877 raise SourceASTParseError( 

1878 "cannot use --safe with this file; failed to parse source file AST: " 

1879 f"{exc}\n" 

1880 "This could be caused by running Black with an older Python version " 

1881 "that does not support new syntax used in your source file." 

1882 ) from exc 

1883 

1884 try: 

1885 dst_ast = parse_ast(dst) 

1886 except Exception as exc: 

1887 log = dump_to_file("".join(traceback.format_tb(exc.__traceback__)), dst) 

1888 raise ASTSafetyError( 

1889 f"INTERNAL ERROR: {_black_info()} produced invalid code: {exc}. " 

1890 "Please report a bug on https://github.com/psf/black/issues. " 

1891 f"This invalid output might be helpful: {log}" 

1892 ) from None 

1893 

1894 src_ast_str = "\n".join(stringify_ast(src_ast)) 

1895 dst_ast_str = "\n".join(stringify_ast(dst_ast)) 

1896 if src_ast_str != dst_ast_str: 

1897 log = dump_to_file(diff(src_ast_str, dst_ast_str, "src", "dst")) 

1898 raise ASTSafetyError( 

1899 f"INTERNAL ERROR: {_black_info()} produced code that is not equivalent to" 

1900 " the source. Please report a bug on https://github.com/psf/black/issues." 

1901 f" This diff might be helpful: {log}" 

1902 ) from None 

1903 

1904 

1905def assert_stable( 

1906 src: str, dst: str, mode: Mode, *, lines: Collection[tuple[int, int]] = () 

1907) -> None: 

1908 """Raise AssertionError if `dst` reformats differently the second time.""" 

1909 if lines: 

1910 # Formatting specified lines requires `adjusted_lines` to map original lines 

1911 # to the formatted lines before re-formatting the previously formatted result. 

1912 # Due to less-ideal diff algorithm, some edge cases produce incorrect new line 

1913 # ranges. Hence for now, we skip the stable check. 

1914 # See https://github.com/psf/black/issues/4033 for context. 

1915 return 

1916 # We shouldn't call format_str() here, because that formats the string 

1917 # twice and may hide a bug where we bounce back and forth between two 

1918 # versions. 

1919 newdst = _format_str_once(dst, mode=mode, lines=lines) 

1920 if dst != newdst: 

1921 log = dump_to_file( 

1922 str(mode), 

1923 diff(src, dst, "source", "first pass"), 

1924 diff(dst, newdst, "first pass", "second pass"), 

1925 ) 

1926 raise AssertionError( 

1927 f"INTERNAL ERROR: {_black_info()} produced different code on the second" 

1928 " pass of the formatter. Please report a bug on" 

1929 f" https://github.com/psf/black/issues. This diff might be helpful: {log}" 

1930 ) from None 

1931 

1932 

1933def patched_main() -> None: 

1934 # PyInstaller patches multiprocessing to need freeze_support() even in non-Windows 

1935 # environments so just assume we always need to call it if frozen. 

1936 if getattr(sys, "frozen", False): 

1937 from multiprocessing import freeze_support 

1938 

1939 freeze_support() 

1940 

1941 main() 

1942 

1943 

1944if __name__ == "__main__": 

1945 patched_main()