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

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

653 statements  

1import io 

2import json 

3import platform 

4import re 

5import sys 

6import tokenize 

7import traceback 

8from collections.abc import ( 

9 Collection, 

10 Generator, 

11 MutableMapping, 

12 Sequence, 

13) 

14from contextlib import nullcontext 

15from dataclasses import replace 

16from datetime import datetime, timezone 

17from enum import Enum 

18from json.decoder import JSONDecodeError 

19from pathlib import Path 

20from re import Pattern 

21from typing import Any 

22 

23import click 

24from click.core import ParameterSource 

25from mypy_extensions import mypyc_attr 

26from pathspec import GitIgnoreSpec 

27from pathspec.patterns.gitignore import GitIgnorePatternError 

28 

29from _black_version import version as __version__ 

30from black.cache import Cache 

31from black.comments import normalize_fmt_off 

32from black.const import ( 

33 DEFAULT_EXCLUDES, 

34 DEFAULT_INCLUDES, 

35 DEFAULT_LINE_LENGTH, 

36 STDIN_PLACEHOLDER, 

37) 

38from black.files import ( 

39 best_effort_relative_path, 

40 find_project_root, 

41 find_pyproject_toml, 

42 find_user_pyproject_toml, 

43 gen_python_files, 

44 get_gitignore, 

45 parse_pyproject_toml, 

46 path_is_excluded, 

47 resolves_outside_root_or_cannot_stat, 

48 wrap_stream_for_windows, 

49) 

50from black.handle_ipynb_magics import ( 

51 PYTHON_CELL_MAGICS, 

52 jupyter_dependencies_are_installed, 

53 mask_cell, 

54 put_trailing_semicolon_back, 

55 remove_trailing_semicolon, 

56 unmask_cell, 

57 validate_cell, 

58) 

59from black.linegen import LN, LineGenerator, transform_line 

60from black.lines import EmptyLineTracker, LinesBlock 

61from black.mode import FUTURE_FLAG_TO_FEATURE, VERSION_TO_FEATURES, Feature 

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

63from black.mode import Preview, TargetVersion, supports_feature 

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

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

66from black.parsing import ( # noqa F401 

67 ASTSafetyError, 

68 InvalidInput, 

69 SourceASTParseError, 

70 lib2to3_parse, 

71 parse_ast, 

72 stringify_ast, 

73) 

74from black.ranges import ( 

75 adjusted_lines, 

76 convert_unchanged_lines, 

77 parse_line_ranges, 

78 sanitized_lines, 

79) 

80from black.report import Changed, NothingChanged, Report 

81from blib2to3.pgen2 import token 

82from blib2to3.pytree import Leaf, Node 

83 

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

85 

86# types 

87FileContent = str 

88Encoding = str 

89NewLine = str 

90 

91 

92class WriteBack(Enum): 

93 NO = 0 

94 YES = 1 

95 DIFF = 2 

96 CHECK = 3 

97 COLOR_DIFF = 4 

98 

99 @classmethod 

100 def from_configuration( 

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

102 ) -> "WriteBack": 

103 if check and not diff: 

104 return cls.CHECK 

105 

106 if diff and color: 

107 return cls.COLOR_DIFF 

108 

109 return cls.DIFF if diff else cls.YES 

110 

111 

112# Legacy name, left for integrations. 

113FileMode = Mode 

114 

115 

116def read_pyproject_toml( 

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

118) -> str | None: 

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

120 

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

122 otherwise. 

123 """ 

124 if not value: 

125 value = find_pyproject_toml( 

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

127 ) 

128 if value is None: 

129 return None 

130 

131 try: 

132 config = parse_pyproject_toml(value) 

133 except (OSError, ValueError) as e: 

134 raise click.FileError( 

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

136 ) from None 

137 

138 if not config: 

139 return None 

140 else: 

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

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

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

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

145 config = { 

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

147 for k, v in config.items() 

148 } 

149 

150 target_version = config.get("target_version") 

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

152 raise click.BadOptionUsage( 

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

154 ) 

155 

156 exclude = config.get("exclude") 

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

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

159 

160 extend_exclude = config.get("extend_exclude") 

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

162 raise click.BadOptionUsage( 

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

164 ) 

165 

166 line_ranges = config.get("line_ranges") 

167 if line_ranges is not None: 

168 raise click.BadOptionUsage( 

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

170 ) 

171 

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

173 if ctx.default_map: 

174 default_map.update(ctx.default_map) 

175 default_map.update(config) 

176 

177 ctx.default_map = default_map 

178 return value 

179 

180 

181def spellcheck_pyproject_toml_keys( 

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

183) -> None: 

184 invalid_keys: list[str] = [] 

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

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

187 if invalid_keys: 

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

189 out( 

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

191 fg="red", 

192 ) 

193 

194 

195def target_version_option_callback( 

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

197) -> list[TargetVersion]: 

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

199 

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

201 when it was a lambda, causing mypyc trouble. 

202 """ 

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

204 

205 

206def _target_versions_exceed_runtime( 

207 target_versions: set[TargetVersion], 

208) -> bool: 

209 if not target_versions: 

210 return False 

211 max_target_minor = max(tv.value for tv in target_versions) 

212 return max_target_minor > sys.version_info[1] 

213 

214 

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

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

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

218 return ( 

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

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

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

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

223 " check." 

224 ) 

225 

226 

227def enable_unstable_feature_callback( 

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

229) -> list[Preview]: 

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

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

232 

233 

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

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

236 

237 If it contains newlines, use verbose mode. 

238 """ 

239 if "\n" in regex: 

240 regex = "(?x)" + regex 

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

242 return compiled 

243 

244 

245def validate_regex( 

246 ctx: click.Context, 

247 param: click.Parameter, 

248 value: str | None, 

249) -> Pattern[str] | None: 

250 try: 

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

252 except re.error as e: 

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

254 

255 

256@click.command( 

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

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

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

260 help="The uncompromising code formatter.", 

261) 

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

263@click.option( 

264 "-l", 

265 "--line-length", 

266 type=int, 

267 default=DEFAULT_LINE_LENGTH, 

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

269 show_default=True, 

270) 

271@click.option( 

272 "-t", 

273 "--target-version", 

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

275 callback=target_version_option_callback, 

276 multiple=True, 

277 help=( 

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

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

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

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

282 ), 

283) 

284@click.option( 

285 "--pyi", 

286 is_flag=True, 

287 help=( 

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

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

290 ), 

291) 

292@click.option( 

293 "--ipynb", 

294 is_flag=True, 

295 help=( 

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

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

298 ), 

299) 

300@click.option( 

301 "--python-cell-magics", 

302 multiple=True, 

303 help=( 

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

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

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

307 ), 

308 default=[], 

309) 

310@click.option( 

311 "-x", 

312 "--skip-source-first-line", 

313 is_flag=True, 

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

315) 

316@click.option( 

317 "-S", 

318 "--skip-string-normalization", 

319 is_flag=True, 

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

321) 

322@click.option( 

323 "-C", 

324 "--skip-magic-trailing-comma", 

325 is_flag=True, 

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

327) 

328@click.option( 

329 "--preview", 

330 is_flag=True, 

331 help=( 

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

333 " functionality in the next major release." 

334 ), 

335) 

336@click.option( 

337 "--unstable", 

338 is_flag=True, 

339 help=( 

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

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

342 " release. Implies --preview." 

343 ), 

344) 

345@click.option( 

346 "--enable-unstable-feature", 

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

348 callback=enable_unstable_feature_callback, 

349 multiple=True, 

350 help=( 

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

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

353 " or existence of any unstable features." 

354 ), 

355) 

356@click.option( 

357 "--check", 

358 is_flag=True, 

359 help=( 

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

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

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

363 ), 

364) 

365@click.option( 

366 "--diff", 

367 is_flag=True, 

368 help=( 

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

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

371 ), 

372) 

373@click.option( 

374 "--color/--no-color", 

375 is_flag=True, 

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

377) 

378@click.option( 

379 "--line-ranges", 

380 multiple=True, 

381 metavar="START-END", 

382 help=( 

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

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

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

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

387 " inclusive on both ends." 

388 ), 

389 default=(), 

390) 

391@click.option( 

392 "--fast/--safe", 

393 is_flag=True, 

394 help=( 

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

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

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

398 ), 

399) 

400@click.option( 

401 "--required-version", 

402 type=str, 

403 help=( 

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

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

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

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

408 " results across environments." 

409 ), 

410) 

411@click.option( 

412 "--exclude", 

413 type=str, 

414 callback=validate_regex, 

415 help=( 

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

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

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

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

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

421 ), 

422 show_default=False, 

423) 

424@click.option( 

425 "--extend-exclude", 

426 type=str, 

427 callback=validate_regex, 

428 help=( 

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

430 " default values instead of overriding them." 

431 ), 

432) 

433@click.option( 

434 "--force-exclude", 

435 type=str, 

436 callback=validate_regex, 

437 help=( 

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

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

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

441 " hook or editor plugin." 

442 ), 

443) 

444@click.option( 

445 "--stdin-filename", 

446 type=str, 

447 is_eager=True, 

448 help=( 

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

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

451 " stdin." 

452 ), 

453) 

454@click.option( 

455 "--include", 

456 type=str, 

457 default=DEFAULT_INCLUDES, 

458 callback=validate_regex, 

459 help=( 

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

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

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

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

464 " command line options." 

465 ), 

466 show_default=True, 

467) 

468@click.option( 

469 "-W", 

470 "--workers", 

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

472 default=None, 

473 help=( 

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

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

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

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

478 ), 

479) 

480@click.option( 

481 "-q", 

482 "--quiet", 

483 is_flag=True, 

484 help=( 

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

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

487 ), 

488) 

489@click.option( 

490 "-v", 

491 "--verbose", 

492 is_flag=True, 

493 help=( 

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

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

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

497 ), 

498) 

499@click.version_option( 

500 version=__version__, 

501 message=( 

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

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

504 ), 

505) 

506@click.argument( 

507 "src", 

508 nargs=-1, 

509 type=click.Path( 

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

511 ), 

512 is_eager=True, 

513 metavar="SRC ...", 

514) 

515@click.option( 

516 "--config", 

517 type=click.Path( 

518 exists=True, 

519 file_okay=True, 

520 dir_okay=False, 

521 readable=True, 

522 allow_dash=False, 

523 path_type=str, 

524 ), 

525 is_eager=True, 

526 callback=read_pyproject_toml, 

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

528) 

529@click.option( 

530 "--no-cache", 

531 is_flag=True, 

532 help=( 

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

534 " included files." 

535 ), 

536) 

537@click.pass_context 

538def main( 

539 ctx: click.Context, 

540 code: str | None, 

541 line_length: int, 

542 target_version: list[TargetVersion], 

543 check: bool, 

544 diff: bool, 

545 line_ranges: Sequence[str], 

546 color: bool, 

547 fast: bool, 

548 pyi: bool, 

549 ipynb: bool, 

550 python_cell_magics: Sequence[str], 

551 skip_source_first_line: bool, 

552 skip_string_normalization: bool, 

553 skip_magic_trailing_comma: bool, 

554 preview: bool, 

555 unstable: bool, 

556 enable_unstable_feature: list[Preview], 

557 quiet: bool, 

558 verbose: bool, 

559 required_version: str | None, 

560 include: Pattern[str], 

561 exclude: Pattern[str] | None, 

562 extend_exclude: Pattern[str] | None, 

563 force_exclude: Pattern[str] | None, 

564 stdin_filename: str | None, 

565 workers: int | None, 

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

567 config: str | None, 

568 no_cache: bool, 

569) -> None: 

570 """The uncompromising code formatter.""" 

571 ctx.ensure_object(dict) 

572 

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

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

575 out( 

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

577 "AST safety checks to fail. " 

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

579 ) 

580 ctx.exit(1) 

581 

582 if src and code is not None: 

583 out( 

584 main.get_usage(ctx) 

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

586 ) 

587 ctx.exit(1) 

588 if not src and code is None: 

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

590 ctx.exit(1) 

591 

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

593 if enable_unstable_feature and not (preview or unstable): 

594 out( 

595 main.get_usage(ctx) 

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

597 ) 

598 ctx.exit(1) 

599 

600 root, method = ( 

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

602 ) 

603 ctx.obj["root"] = root 

604 

605 if verbose: 

606 if root: 

607 out( 

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

609 fg="blue", 

610 ) 

611 

612 if config: 

613 config_source = ctx.get_parameter_source("config") 

614 user_level_config = str(find_user_pyproject_toml()) 

615 if config == user_level_config: 

616 out( 

617 "Using configuration from user-level config at " 

618 f"'{user_level_config}'.", 

619 fg="blue", 

620 ) 

621 elif config_source in ( 

622 ParameterSource.DEFAULT, 

623 ParameterSource.DEFAULT_MAP, 

624 ): 

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

626 else: 

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

628 if ctx.default_map: 

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

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

631 

632 error_msg = "Oh no! 💥 💔 💥" 

633 if ( 

634 required_version 

635 and required_version != __version__ 

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

637 ): 

638 err( 

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

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

641 ) 

642 ctx.exit(1) 

643 if ipynb and pyi: 

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

645 ctx.exit(1) 

646 

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

648 if target_version: 

649 versions = set(target_version) 

650 else: 

651 # We'll autodetect later. 

652 versions = set() 

653 mode = Mode( 

654 target_versions=versions, 

655 line_length=line_length, 

656 is_pyi=pyi, 

657 is_ipynb=ipynb, 

658 skip_source_first_line=skip_source_first_line, 

659 string_normalization=not skip_string_normalization, 

660 magic_trailing_comma=not skip_magic_trailing_comma, 

661 preview=preview, 

662 unstable=unstable, 

663 python_cell_magics=set(python_cell_magics), 

664 enabled_features=set(enable_unstable_feature), 

665 ) 

666 

667 if not fast and _target_versions_exceed_runtime(versions): 

668 err( 

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

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

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

672 fg="yellow", 

673 ) 

674 

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

676 if line_ranges: 

677 if ipynb: 

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

679 ctx.exit(1) 

680 

681 try: 

682 lines = parse_line_ranges(line_ranges) 

683 except ValueError as e: 

684 err(str(e)) 

685 ctx.exit(1) 

686 

687 if code is not None: 

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

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

690 quiet = True 

691 

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

693 

694 if code is not None: 

695 reformat_code( 

696 content=code, 

697 fast=fast, 

698 write_back=write_back, 

699 mode=mode, 

700 report=report, 

701 lines=lines, 

702 ) 

703 else: 

704 assert root is not None # root is only None if code is not None 

705 try: 

706 sources = get_sources( 

707 root=root, 

708 src=src, 

709 quiet=quiet, 

710 verbose=verbose, 

711 include=include, 

712 exclude=exclude, 

713 extend_exclude=extend_exclude, 

714 force_exclude=force_exclude, 

715 report=report, 

716 stdin_filename=stdin_filename, 

717 ) 

718 except GitIgnorePatternError: 

719 ctx.exit(1) 

720 

721 if not sources: 

722 if verbose or not quiet: 

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

724 if "-" in src: 

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

726 ctx.exit(0) 

727 

728 if len(sources) == 1: 

729 reformat_one( 

730 src=sources.pop(), 

731 fast=fast, 

732 write_back=write_back, 

733 mode=mode, 

734 report=report, 

735 lines=lines, 

736 no_cache=no_cache, 

737 ) 

738 else: 

739 from black.concurrency import reformat_many 

740 

741 if lines: 

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

743 ctx.exit(1) 

744 reformat_many( 

745 sources=sources, 

746 fast=fast, 

747 write_back=write_back, 

748 mode=mode, 

749 report=report, 

750 workers=workers, 

751 no_cache=no_cache, 

752 ) 

753 

754 if verbose or not quiet: 

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

756 out() 

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

758 if code is None: 

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

760 ctx.exit(report.return_code) 

761 

762 

763def get_sources( 

764 *, 

765 root: Path, 

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

767 quiet: bool, 

768 verbose: bool, 

769 include: Pattern[str], 

770 exclude: Pattern[str] | None, 

771 extend_exclude: Pattern[str] | None, 

772 force_exclude: Pattern[str] | None, 

773 report: "Report", 

774 stdin_filename: str | None, 

775) -> set[Path]: 

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

777 sources: set[Path] = set() 

778 

779 assert root.is_absolute(), f"INTERNAL ERROR: `root` must be absolute but is {root}" 

780 using_default_exclude = exclude is None 

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

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

783 root_gitignore = get_gitignore(root) 

784 

785 for s in src: 

786 if s == "-" and stdin_filename: 

787 path = Path(stdin_filename) 

788 if path_is_excluded(stdin_filename, force_exclude): 

789 report.path_ignored( 

790 path, 

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

792 ) 

793 continue 

794 is_stdin = True 

795 else: 

796 path = Path(s) 

797 is_stdin = False 

798 

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

800 if is_stdin or path.is_file(): 

801 if resolves_outside_root_or_cannot_stat(path, root, report): 

802 if verbose: 

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

804 continue 

805 

806 root_relative_path = best_effort_relative_path(path, root).as_posix() 

807 root_relative_path = "/" + root_relative_path 

808 

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

810 if path_is_excluded(root_relative_path, force_exclude): 

811 report.path_ignored( 

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

813 ) 

814 continue 

815 

816 if is_stdin: 

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

818 

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

820 warn=verbose or not quiet 

821 ): 

822 continue 

823 

824 if verbose: 

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

826 sources.add(path) 

827 elif path.is_dir(): 

828 path = root / (path.resolve().relative_to(root)) 

829 if verbose: 

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

831 

832 if using_default_exclude: 

833 gitignore = { 

834 root: root_gitignore, 

835 path: get_gitignore(path), 

836 } 

837 sources.update( 

838 gen_python_files( 

839 path.iterdir(), 

840 root, 

841 include, 

842 exclude, 

843 extend_exclude, 

844 force_exclude, 

845 report, 

846 gitignore, 

847 verbose=verbose, 

848 quiet=quiet, 

849 ) 

850 ) 

851 elif s == "-": 

852 if verbose: 

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

854 sources.add(path) 

855 else: 

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

857 

858 return sources 

859 

860 

861def reformat_code( 

862 content: str, 

863 fast: bool, 

864 write_back: WriteBack, 

865 mode: Mode, 

866 report: Report, 

867 *, 

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

869) -> None: 

870 """ 

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

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

873 

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

875 :func:`format_file_in_place` or :func:`format_stdin_to_stdout`. 

876 """ 

877 path = Path("<string>") 

878 try: 

879 changed = Changed.NO 

880 if format_stdin_to_stdout( 

881 content=content, fast=fast, write_back=write_back, mode=mode, lines=lines 

882 ): 

883 changed = Changed.YES 

884 report.done(path, changed) 

885 except Exception as exc: 

886 if report.verbose: 

887 traceback.print_exc() 

888 report.failed(path, str(exc)) 

889 

890 

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

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

893@mypyc_attr(patchable=True) 

894def reformat_one( 

895 src: Path, 

896 fast: bool, 

897 write_back: WriteBack, 

898 mode: Mode, 

899 report: "Report", 

900 *, 

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

902 no_cache: bool = False, 

903) -> None: 

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

905 

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

907 :func:`format_file_in_place` or :func:`format_stdin_to_stdout`. 

908 """ 

909 try: 

910 changed = Changed.NO 

911 

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

913 is_stdin = True 

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

915 is_stdin = True 

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

917 # to the user 

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

919 else: 

920 is_stdin = False 

921 

922 if is_stdin: 

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

924 mode = replace(mode, is_pyi=True) 

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

926 mode = replace(mode, is_ipynb=True) 

927 if format_stdin_to_stdout( 

928 fast=fast, write_back=write_back, mode=mode, lines=lines 

929 ): 

930 changed = Changed.YES 

931 else: 

932 cache = None if no_cache else Cache.read(mode) 

933 if cache is not None and write_back not in ( 

934 WriteBack.DIFF, 

935 WriteBack.COLOR_DIFF, 

936 ): 

937 if not cache.is_changed(src): 

938 changed = Changed.CACHED 

939 if changed is not Changed.CACHED and format_file_in_place( 

940 src, fast=fast, write_back=write_back, mode=mode, lines=lines 

941 ): 

942 changed = Changed.YES 

943 if cache is not None and ( 

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

945 or (write_back is WriteBack.CHECK and changed is Changed.NO) 

946 ): 

947 cache.write([src]) 

948 report.done(src, changed) 

949 except Exception as exc: 

950 if report.verbose: 

951 traceback.print_exc() 

952 report.failed(src, str(exc)) 

953 

954 

955def format_file_in_place( 

956 src: Path, 

957 fast: bool, 

958 mode: Mode, 

959 write_back: WriteBack = WriteBack.NO, 

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

961 *, 

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

963) -> bool: 

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

965 

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

967 code to the file. 

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

969 """ 

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

971 mode = replace(mode, is_pyi=True) 

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

973 mode = replace(mode, is_ipynb=True) 

974 

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

976 header = b"" 

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

978 if mode.skip_source_first_line: 

979 header = buf.readline() 

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

981 try: 

982 dst_contents = format_file_contents( 

983 src_contents, fast=fast, mode=mode, lines=lines 

984 ) 

985 except NothingChanged: 

986 return False 

987 except JSONDecodeError: 

988 raise ValueError( 

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

990 ) from None 

991 src_contents = header.decode(encoding) + src_contents 

992 dst_contents = header.decode(encoding) + dst_contents 

993 

994 if write_back == WriteBack.YES: 

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

996 f.write(dst_contents) 

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

998 now = datetime.now(timezone.utc) 

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

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

1001 if mode.is_ipynb: 

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

1003 else: 

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

1005 

1006 if write_back == WriteBack.COLOR_DIFF: 

1007 diff_contents = color_diff(diff_contents) 

1008 

1009 with lock or nullcontext(): 

1010 f = io.TextIOWrapper( 

1011 sys.stdout.buffer, 

1012 encoding=encoding, 

1013 newline=newline, 

1014 write_through=True, 

1015 ) 

1016 f = wrap_stream_for_windows(f) 

1017 f.write(diff_contents) 

1018 f.detach() 

1019 

1020 return True 

1021 

1022 

1023def format_stdin_to_stdout( 

1024 fast: bool, 

1025 *, 

1026 content: str | None = None, 

1027 write_back: WriteBack = WriteBack.NO, 

1028 mode: Mode, 

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

1030) -> bool: 

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

1032 

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

1034 

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

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

1037 :func:`format_file_contents`. 

1038 """ 

1039 then = datetime.now(timezone.utc) 

1040 

1041 if content is None: 

1042 src, encoding, newline = decode_bytes(sys.stdin.buffer.read(), mode) 

1043 else: 

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

1045 

1046 dst = src 

1047 try: 

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

1049 return True 

1050 

1051 except NothingChanged: 

1052 return False 

1053 

1054 finally: 

1055 f = io.TextIOWrapper( 

1056 sys.stdout.buffer, encoding=encoding, newline=newline, write_through=True 

1057 ) 

1058 if write_back == WriteBack.YES: 

1059 # Make sure there's a newline after the content 

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

1061 dst += newline 

1062 f.write(dst) 

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

1064 now = datetime.now(timezone.utc) 

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

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

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

1068 if write_back == WriteBack.COLOR_DIFF: 

1069 d = color_diff(d) 

1070 f = wrap_stream_for_windows(f) 

1071 f.write(d) 

1072 f.detach() 

1073 

1074 

1075def check_stability_and_equivalence( 

1076 src_contents: str, 

1077 dst_contents: str, 

1078 *, 

1079 mode: Mode, 

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

1081) -> None: 

1082 """Perform stability and equivalence checks. 

1083 

1084 Raise AssertionError if source and destination contents are not 

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

1086 content differently. 

1087 """ 

1088 try: 

1089 assert_equivalent(src_contents, dst_contents) 

1090 except SourceASTParseError: 

1091 raise 

1092 except ASTSafetyError: 

1093 if _target_versions_exceed_runtime(mode.target_versions): 

1094 raise ASTSafetyError( 

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

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

1097 ) from None 

1098 raise 

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

1100 

1101 

1102def format_file_contents( 

1103 src_contents: str, 

1104 *, 

1105 fast: bool, 

1106 mode: Mode, 

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

1108) -> FileContent: 

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

1110 

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

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

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

1114 """ 

1115 if mode.is_ipynb: 

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

1117 else: 

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

1119 if src_contents == dst_contents: 

1120 raise NothingChanged 

1121 

1122 if not fast and not mode.is_ipynb: 

1123 # Jupyter notebooks will already have been checked above. 

1124 check_stability_and_equivalence( 

1125 src_contents, dst_contents, mode=mode, lines=lines 

1126 ) 

1127 return dst_contents 

1128 

1129 

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

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

1132 

1133 General idea is: 

1134 

1135 - if cell has trailing semicolon, remove it; 

1136 - if cell has IPython magics, mask them; 

1137 - format cell; 

1138 - reinstate IPython magics; 

1139 - reinstate trailing semicolon (if originally present); 

1140 - strip trailing newlines. 

1141 

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

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

1144 are currently not supported. 

1145 """ 

1146 validate_cell(src, mode) 

1147 src_without_trailing_semicolon, has_trailing_semicolon = remove_trailing_semicolon( 

1148 src 

1149 ) 

1150 try: 

1151 masked_src, replacements = mask_cell(src_without_trailing_semicolon) 

1152 except SyntaxError: 

1153 raise NothingChanged from None 

1154 masked_dst = format_str(masked_src, mode=mode) 

1155 if not fast: 

1156 check_stability_and_equivalence(masked_src, masked_dst, mode=mode) 

1157 dst_without_trailing_semicolon = unmask_cell(masked_dst, replacements) 

1158 dst = put_trailing_semicolon_back( 

1159 dst_without_trailing_semicolon, has_trailing_semicolon 

1160 ) 

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

1162 if dst == src: 

1163 raise NothingChanged from None 

1164 return dst 

1165 

1166 

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

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

1169 

1170 All notebook metadata fields are optional, see 

1171 https://nbformat.readthedocs.io/en/latest/format_description.html. So 

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

1173 """ 

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

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

1176 raise NothingChanged from None 

1177 

1178 

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

1180 """Format Jupyter notebook. 

1181 

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

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

1184 """ 

1185 if not src_contents: 

1186 raise NothingChanged 

1187 

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

1189 modified = False 

1190 nb = json.loads(src_contents) 

1191 validate_metadata(nb) 

1192 for cell in nb["cells"]: 

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

1194 try: 

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

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

1197 except NothingChanged: 

1198 pass 

1199 else: 

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

1201 modified = True 

1202 if modified: 

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

1204 if trailing_newline: 

1205 dst_contents = dst_contents + "\n" 

1206 return dst_contents 

1207 else: 

1208 raise NothingChanged 

1209 

1210 

1211def format_str( 

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

1213) -> str: 

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

1215 

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

1217 allowed. Example: 

1218 

1219 >>> import black 

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

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

1222 ... 

1223 

1224 A more complex example: 

1225 

1226 >>> print( 

1227 ... black.format_str( 

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

1229 ... mode=black.Mode( 

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

1231 ... line_length=10, 

1232 ... string_normalization=False, 

1233 ... is_pyi=False, 

1234 ... ), 

1235 ... ), 

1236 ... ) 

1237 def f( 

1238 arg: str = '', 

1239 ) -> None: 

1240 hey 

1241 

1242 """ 

1243 if lines: 

1244 lines = sanitized_lines(lines, src_contents) 

1245 if not lines: 

1246 return src_contents # Nothing to format 

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

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

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

1250 # parentheses. Admittedly ugly. 

1251 if src_contents != dst_contents: 

1252 if lines: 

1253 lines = adjusted_lines(lines, src_contents, dst_contents) 

1254 return _format_str_once(dst_contents, mode=mode, lines=lines) 

1255 return dst_contents 

1256 

1257 

1258def _format_str_once( 

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

1260) -> str: 

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

1262 # magic encoding comment than utf-8 

1263 normalized_contents, _, newline_type = decode_bytes( 

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

1265 ) 

1266 

1267 src_node = lib2to3_parse( 

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

1269 ) 

1270 

1271 dst_blocks: list[LinesBlock] = [] 

1272 if mode.target_versions: 

1273 versions = mode.target_versions 

1274 else: 

1275 future_imports = get_future_imports(src_node) 

1276 versions = detect_target_versions(src_node, future_imports=future_imports) 

1277 

1278 line_generation_features = { 

1279 feature 

1280 for feature in { 

1281 Feature.PARENTHESIZED_CONTEXT_MANAGERS, 

1282 Feature.UNPARENTHESIZED_EXCEPT_TYPES, 

1283 Feature.T_STRINGS, 

1284 } 

1285 if supports_feature(versions, feature) 

1286 } 

1287 normalize_fmt_off(src_node, mode, lines) 

1288 if lines: 

1289 # This should be called after normalize_fmt_off. 

1290 convert_unchanged_lines(src_node, lines) 

1291 

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

1293 elt = EmptyLineTracker(mode=mode) 

1294 split_line_features = { 

1295 feature 

1296 for feature in { 

1297 Feature.TRAILING_COMMA_IN_CALL, 

1298 Feature.TRAILING_COMMA_IN_DEF, 

1299 } 

1300 if supports_feature(versions, feature) 

1301 } 

1302 block: LinesBlock | None = None 

1303 for current_line in line_generator.visit(src_node): 

1304 block = elt.maybe_empty_lines(current_line) 

1305 dst_blocks.append(block) 

1306 for line in transform_line( 

1307 current_line, mode=mode, features=split_line_features 

1308 ): 

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

1310 if dst_blocks: 

1311 dst_blocks[-1].after = 0 

1312 dst_contents = [] 

1313 for block in dst_blocks: 

1314 dst_contents.extend(block.all_lines()) 

1315 if not dst_contents: 

1316 if "\n" in normalized_contents: 

1317 return newline_type 

1318 return "".join(dst_contents).replace("\n", newline_type) 

1319 

1320 

1321def decode_bytes( 

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

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

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

1325 

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

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

1328 

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

1330 differently to their possible encoding magic comment. 

1331 """ 

1332 srcbuf = io.BytesIO(src) 

1333 

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

1335 # might be different 

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

1337 if encoding_overwrite is not None: 

1338 encoding = encoding_overwrite 

1339 

1340 if not lines: 

1341 return "", encoding, "\n" 

1342 

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

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

1345 newline = "\r" 

1346 else: 

1347 newline = "\r\n" 

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

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

1350 newline = "\r" 

1351 else: 

1352 newline = "\n" 

1353 else: 

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

1355 newline = "\r" 

1356 else: 

1357 newline = "\n" 

1358 

1359 srcbuf.seek(0) 

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

1361 return tiow.read(), encoding, newline 

1362 

1363 

1364def get_features_used( 

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

1366) -> set[Feature]: 

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

1368 

1369 Currently looking for: 

1370 - f-strings; 

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

1372 - underscores in numeric literals; 

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

1374 - positional only arguments in function signatures and lambdas; 

1375 - assignment expression; 

1376 - relaxed decorator syntax; 

1377 - usage of __future__ flags (annotations); 

1378 - print / exec statements; 

1379 - parenthesized context managers; 

1380 - match statements; 

1381 - except* clause; 

1382 - variadic generics; 

1383 - lazy imports; 

1384 - starred or double-starred comprehensions. 

1385 """ 

1386 features: set[Feature] = set() 

1387 if future_imports: 

1388 features |= { 

1389 FUTURE_FLAG_TO_FEATURE[future_import] 

1390 for future_import in future_imports 

1391 if future_import in FUTURE_FLAG_TO_FEATURE 

1392 } 

1393 

1394 for n in node.pre_order(): 

1395 if n.type == token.FSTRING_START: 

1396 features.add(Feature.F_STRINGS) 

1397 elif n.type == token.TSTRING_START: 

1398 features.add(Feature.T_STRINGS) 

1399 elif ( 

1400 n.type == token.RBRACE 

1401 and n.parent is not None 

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

1403 ): 

1404 features.add(Feature.DEBUG_F_STRINGS) 

1405 

1406 elif is_number_token(n): 

1407 if "_" in n.value: 

1408 features.add(Feature.NUMERIC_UNDERSCORES) 

1409 

1410 elif n.type == token.SLASH: 

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

1412 syms.typedargslist, 

1413 syms.arglist, 

1414 syms.varargslist, 

1415 }: 

1416 features.add(Feature.POS_ONLY_ARGUMENTS) 

1417 

1418 elif n.type == token.COLONEQUAL: 

1419 features.add(Feature.ASSIGNMENT_EXPRESSIONS) 

1420 

1421 elif n.type == token.LAZY: 

1422 features.add(Feature.LAZY_IMPORTS) 

1423 

1424 elif n.type == syms.decorator: 

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

1426 n.children[1] 

1427 ): 

1428 features.add(Feature.RELAXED_DECORATORS) 

1429 

1430 elif is_unpacking_comprehension(n): 

1431 features.add(Feature.UNPACKING_IN_COMPREHENSIONS) 

1432 

1433 elif ( 

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

1435 and n.children 

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

1437 ): 

1438 if n.type == syms.typedargslist: 

1439 feature = Feature.TRAILING_COMMA_IN_DEF 

1440 else: 

1441 feature = Feature.TRAILING_COMMA_IN_CALL 

1442 

1443 for ch in n.children: 

1444 if ch.type in STARS: 

1445 features.add(feature) 

1446 

1447 if ch.type == syms.argument: 

1448 for argch in ch.children: 

1449 if argch.type in STARS: 

1450 features.add(feature) 

1451 

1452 elif ( 

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

1454 and len(n.children) >= 2 

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

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

1457 ): 

1458 features.add(Feature.UNPACKING_ON_FLOW) 

1459 

1460 elif ( 

1461 n.type == syms.annassign 

1462 and len(n.children) >= 4 

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

1464 ): 

1465 features.add(Feature.ANN_ASSIGN_EXTENDED_RHS) 

1466 

1467 elif ( 

1468 n.type == syms.with_stmt 

1469 and len(n.children) > 2 

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

1471 ): 

1472 atom_children = n.children[1].children 

1473 if ( 

1474 len(atom_children) == 3 

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

1476 and _contains_asexpr(atom_children[1]) 

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

1478 ): 

1479 features.add(Feature.PARENTHESIZED_CONTEXT_MANAGERS) 

1480 

1481 elif n.type == syms.match_stmt: 

1482 features.add(Feature.PATTERN_MATCHING) 

1483 

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

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

1486 ): 

1487 features.add(Feature.VARIADIC_GENERICS) 

1488 

1489 elif ( 

1490 n.type == syms.tname_star 

1491 and len(n.children) == 3 

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

1493 ): 

1494 features.add(Feature.VARIADIC_GENERICS) 

1495 

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

1497 features.add(Feature.TYPE_PARAMS) 

1498 

1499 elif ( 

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

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

1502 ): 

1503 features.add(Feature.TYPE_PARAM_DEFAULTS) 

1504 

1505 elif ( 

1506 n.type == syms.except_clause 

1507 and len(n.children) >= 2 

1508 and ( 

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

1510 ) 

1511 ): 

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

1513 

1514 if is_star_except: 

1515 features.add(Feature.EXCEPT_STAR) 

1516 

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

1518 has_as_clause = ( 

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

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

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

1522 ) 

1523 

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

1525 if not has_as_clause and ( 

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

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

1528 ): 

1529 features.add(Feature.UNPARENTHESIZED_EXCEPT_TYPES) 

1530 

1531 return features 

1532 

1533 

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

1535 if node.type not in {syms.listmaker, syms.testlist_gexp, syms.dictsetmaker}: 

1536 return False 

1537 

1538 if not any( 

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

1540 ): 

1541 return False 

1542 

1543 first_child = node.children[0] 

1544 return first_child.type == syms.star_expr or first_child.type == token.DOUBLESTAR 

1545 

1546 

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

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

1549 if node.type == syms.asexpr_test: 

1550 return True 

1551 elif node.type == syms.atom: 

1552 if ( 

1553 len(node.children) == 3 

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

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

1556 ): 

1557 return _contains_asexpr(node.children[1]) 

1558 elif node.type == syms.testlist_gexp: 

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

1560 return False 

1561 

1562 

1563def detect_target_versions( 

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

1565) -> set[TargetVersion]: 

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

1567 features = get_features_used(node, future_imports=future_imports) 

1568 return { 

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

1570 } 

1571 

1572 

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

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

1575 imports: set[str] = set() 

1576 

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

1578 for child in children: 

1579 if isinstance(child, Leaf): 

1580 if child.type == token.NAME: 

1581 yield child.value 

1582 

1583 elif child.type == syms.import_as_name: 

1584 orig_name = child.children[0] 

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

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

1587 yield orig_name.value 

1588 

1589 elif child.type == syms.import_as_names: 

1590 yield from get_imports_from_children(child.children) 

1591 

1592 else: 

1593 raise AssertionError("Invalid syntax parsing imports") 

1594 

1595 for child in node.children: 

1596 if child.type != syms.simple_stmt: 

1597 break 

1598 

1599 first_child = child.children[0] 

1600 if isinstance(first_child, Leaf): 

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

1602 if ( 

1603 len(child.children) == 2 

1604 and first_child.type == token.STRING 

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

1606 ): 

1607 continue 

1608 

1609 break 

1610 

1611 elif first_child.type == syms.import_from: 

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

1613 break 

1614 

1615 module_name = first_child.children[1] 

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

1617 break 

1618 

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

1620 else: 

1621 break 

1622 

1623 return imports 

1624 

1625 

1626def _black_info() -> str: 

1627 return ( 

1628 f"Black {__version__} on " 

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

1630 ) 

1631 

1632 

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

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

1635 try: 

1636 src_ast = parse_ast(src) 

1637 except Exception as exc: 

1638 raise SourceASTParseError( 

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

1640 f"{exc}\n" 

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

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

1643 ) from exc 

1644 

1645 try: 

1646 dst_ast = parse_ast(dst) 

1647 except Exception as exc: 

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

1649 raise ASTSafetyError( 

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

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

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

1653 ) from None 

1654 

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

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

1657 if src_ast_str != dst_ast_str: 

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

1659 raise ASTSafetyError( 

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

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

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

1663 ) from None 

1664 

1665 

1666def assert_stable( 

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

1668) -> None: 

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

1670 if lines: 

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

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

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

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

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

1676 return 

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

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

1679 # versions. 

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

1681 if dst != newdst: 

1682 log = dump_to_file( 

1683 str(mode), 

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

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

1686 ) 

1687 raise AssertionError( 

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

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

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

1691 ) from None 

1692 

1693 

1694def patched_main() -> None: 

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

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

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

1698 from multiprocessing import freeze_support 

1699 

1700 freeze_support() 

1701 

1702 main() 

1703 

1704 

1705if __name__ == "__main__": 

1706 patched_main()