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

639 statements  

1import io 

2import json 

3import platform 

4import re 

5import sys 

6import tokenize 

7import traceback 

8from collections.abc import ( 

9 Collection, 

10 Generator, 

11 Iterator, 

12 MutableMapping, 

13 Sequence, 

14) 

15from contextlib import contextmanager 

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, Optional, Union 

23 

24import click 

25from click.core import ParameterSource 

26from mypy_extensions import mypyc_attr 

27from pathspec import PathSpec 

28from pathspec.patterns.gitwildmatch import GitWildMatchPatternError 

29 

30from _black_version import version as __version__ 

31from black.cache import Cache 

32from black.comments import normalize_fmt_off 

33from black.const import ( 

34 DEFAULT_EXCLUDES, 

35 DEFAULT_INCLUDES, 

36 DEFAULT_LINE_LENGTH, 

37 STDIN_PLACEHOLDER, 

38) 

39from black.files import ( 

40 best_effort_relative_path, 

41 find_project_root, 

42 find_pyproject_toml, 

43 find_user_pyproject_toml, 

44 gen_python_files, 

45 get_gitignore, 

46 parse_pyproject_toml, 

47 path_is_excluded, 

48 resolves_outside_root_or_cannot_stat, 

49 wrap_stream_for_windows, 

50) 

51from black.handle_ipynb_magics import ( 

52 PYTHON_CELL_MAGICS, 

53 jupyter_dependencies_are_installed, 

54 mask_cell, 

55 put_trailing_semicolon_back, 

56 remove_trailing_semicolon, 

57 unmask_cell, 

58 validate_cell, 

59) 

60from black.linegen import LN, LineGenerator, transform_line 

61from black.lines import EmptyLineTracker, LinesBlock 

62from black.mode import FUTURE_FLAG_TO_FEATURE, VERSION_TO_FEATURES, Feature 

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

64from black.mode import Preview, TargetVersion, supports_feature 

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

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

67from black.parsing import ( # noqa F401 

68 ASTSafetyError, 

69 InvalidInput, 

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, value: Optional[str] 

118) -> Optional[str]: 

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: Union[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 enable_unstable_feature_callback( 

207 c: click.Context, p: Union[click.Option, click.Parameter], v: tuple[str, ...] 

208) -> list[Preview]: 

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

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

211 

212 

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

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

215 

216 If it contains newlines, use verbose mode. 

217 """ 

218 if "\n" in regex: 

219 regex = "(?x)" + regex 

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

221 return compiled 

222 

223 

224def validate_regex( 

225 ctx: click.Context, 

226 param: click.Parameter, 

227 value: Optional[str], 

228) -> Optional[Pattern[str]]: 

229 try: 

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

231 except re.error as e: 

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

233 

234 

235@click.command( 

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

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

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

239 help="The uncompromising code formatter.", 

240) 

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

242@click.option( 

243 "-l", 

244 "--line-length", 

245 type=int, 

246 default=DEFAULT_LINE_LENGTH, 

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

248 show_default=True, 

249) 

250@click.option( 

251 "-t", 

252 "--target-version", 

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

254 callback=target_version_option_callback, 

255 multiple=True, 

256 help=( 

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

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

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

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

261 ), 

262) 

263@click.option( 

264 "--pyi", 

265 is_flag=True, 

266 help=( 

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

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

269 ), 

270) 

271@click.option( 

272 "--ipynb", 

273 is_flag=True, 

274 help=( 

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

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

277 ), 

278) 

279@click.option( 

280 "--python-cell-magics", 

281 multiple=True, 

282 help=( 

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

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

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

286 ), 

287 default=[], 

288) 

289@click.option( 

290 "-x", 

291 "--skip-source-first-line", 

292 is_flag=True, 

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

294) 

295@click.option( 

296 "-S", 

297 "--skip-string-normalization", 

298 is_flag=True, 

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

300) 

301@click.option( 

302 "-C", 

303 "--skip-magic-trailing-comma", 

304 is_flag=True, 

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

306) 

307@click.option( 

308 "--preview", 

309 is_flag=True, 

310 help=( 

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

312 " functionality in the next major release." 

313 ), 

314) 

315@click.option( 

316 "--unstable", 

317 is_flag=True, 

318 help=( 

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

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

321 " release. Implies --preview." 

322 ), 

323) 

324@click.option( 

325 "--enable-unstable-feature", 

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

327 callback=enable_unstable_feature_callback, 

328 multiple=True, 

329 help=( 

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

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

332 " or existence of any unstable features." 

333 ), 

334) 

335@click.option( 

336 "--check", 

337 is_flag=True, 

338 help=( 

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

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

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

342 ), 

343) 

344@click.option( 

345 "--diff", 

346 is_flag=True, 

347 help=( 

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

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

350 ), 

351) 

352@click.option( 

353 "--color/--no-color", 

354 is_flag=True, 

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

356) 

357@click.option( 

358 "--line-ranges", 

359 multiple=True, 

360 metavar="START-END", 

361 help=( 

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

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

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

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

366 " inclusive on both ends." 

367 ), 

368 default=(), 

369) 

370@click.option( 

371 "--fast/--safe", 

372 is_flag=True, 

373 help=( 

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

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

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

377 ), 

378) 

379@click.option( 

380 "--required-version", 

381 type=str, 

382 help=( 

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

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

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

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

387 " results across environments." 

388 ), 

389) 

390@click.option( 

391 "--exclude", 

392 type=str, 

393 callback=validate_regex, 

394 help=( 

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

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

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

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

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

400 ), 

401 show_default=False, 

402) 

403@click.option( 

404 "--extend-exclude", 

405 type=str, 

406 callback=validate_regex, 

407 help=( 

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

409 " default values instead of overriding them." 

410 ), 

411) 

412@click.option( 

413 "--force-exclude", 

414 type=str, 

415 callback=validate_regex, 

416 help=( 

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

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

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

420 " hook or editor plugin." 

421 ), 

422) 

423@click.option( 

424 "--stdin-filename", 

425 type=str, 

426 is_eager=True, 

427 help=( 

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

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

430 " stdin." 

431 ), 

432) 

433@click.option( 

434 "--include", 

435 type=str, 

436 default=DEFAULT_INCLUDES, 

437 callback=validate_regex, 

438 help=( 

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

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

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

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

443 " command line options." 

444 ), 

445 show_default=True, 

446) 

447@click.option( 

448 "-W", 

449 "--workers", 

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

451 default=None, 

452 help=( 

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

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

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

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

457 ), 

458) 

459@click.option( 

460 "-q", 

461 "--quiet", 

462 is_flag=True, 

463 help=( 

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

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

466 ), 

467) 

468@click.option( 

469 "-v", 

470 "--verbose", 

471 is_flag=True, 

472 help=( 

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

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

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

476 ), 

477) 

478@click.version_option( 

479 version=__version__, 

480 message=( 

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

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

483 ), 

484) 

485@click.argument( 

486 "src", 

487 nargs=-1, 

488 type=click.Path( 

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

490 ), 

491 is_eager=True, 

492 metavar="SRC ...", 

493) 

494@click.option( 

495 "--config", 

496 type=click.Path( 

497 exists=True, 

498 file_okay=True, 

499 dir_okay=False, 

500 readable=True, 

501 allow_dash=False, 

502 path_type=str, 

503 ), 

504 is_eager=True, 

505 callback=read_pyproject_toml, 

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

507) 

508@click.option( 

509 "--no-cache", 

510 is_flag=True, 

511 help=( 

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

513 " included files." 

514 ), 

515) 

516@click.pass_context 

517def main( # noqa: C901 

518 ctx: click.Context, 

519 code: Optional[str], 

520 line_length: int, 

521 target_version: list[TargetVersion], 

522 check: bool, 

523 diff: bool, 

524 line_ranges: Sequence[str], 

525 color: bool, 

526 fast: bool, 

527 pyi: bool, 

528 ipynb: bool, 

529 python_cell_magics: Sequence[str], 

530 skip_source_first_line: bool, 

531 skip_string_normalization: bool, 

532 skip_magic_trailing_comma: bool, 

533 preview: bool, 

534 unstable: bool, 

535 enable_unstable_feature: list[Preview], 

536 quiet: bool, 

537 verbose: bool, 

538 required_version: Optional[str], 

539 include: Pattern[str], 

540 exclude: Optional[Pattern[str]], 

541 extend_exclude: Optional[Pattern[str]], 

542 force_exclude: Optional[Pattern[str]], 

543 stdin_filename: Optional[str], 

544 workers: Optional[int], 

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

546 config: Optional[str], 

547 no_cache: bool, 

548) -> None: 

549 """The uncompromising code formatter.""" 

550 ctx.ensure_object(dict) 

551 

552 assert sys.version_info >= (3, 9), "Black requires Python 3.9+" 

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

554 out( 

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

556 "AST safety checks to fail. " 

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

558 ) 

559 ctx.exit(1) 

560 

561 if src and code is not None: 

562 out( 

563 main.get_usage(ctx) 

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

565 ) 

566 ctx.exit(1) 

567 if not src and code is None: 

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

569 ctx.exit(1) 

570 

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

572 if enable_unstable_feature and not (preview or unstable): 

573 out( 

574 main.get_usage(ctx) 

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

576 ) 

577 ctx.exit(1) 

578 

579 root, method = ( 

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

581 ) 

582 ctx.obj["root"] = root 

583 

584 if verbose: 

585 if root: 

586 out( 

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

588 fg="blue", 

589 ) 

590 

591 if config: 

592 config_source = ctx.get_parameter_source("config") 

593 user_level_config = str(find_user_pyproject_toml()) 

594 if config == user_level_config: 

595 out( 

596 "Using configuration from user-level config at " 

597 f"'{user_level_config}'.", 

598 fg="blue", 

599 ) 

600 elif config_source in ( 

601 ParameterSource.DEFAULT, 

602 ParameterSource.DEFAULT_MAP, 

603 ): 

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

605 else: 

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

607 if ctx.default_map: 

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

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

610 

611 error_msg = "Oh no! 💥 💔 💥" 

612 if ( 

613 required_version 

614 and required_version != __version__ 

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

616 ): 

617 err( 

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

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

620 ) 

621 ctx.exit(1) 

622 if ipynb and pyi: 

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

624 ctx.exit(1) 

625 

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

627 if target_version: 

628 versions = set(target_version) 

629 else: 

630 # We'll autodetect later. 

631 versions = set() 

632 mode = Mode( 

633 target_versions=versions, 

634 line_length=line_length, 

635 is_pyi=pyi, 

636 is_ipynb=ipynb, 

637 skip_source_first_line=skip_source_first_line, 

638 string_normalization=not skip_string_normalization, 

639 magic_trailing_comma=not skip_magic_trailing_comma, 

640 preview=preview, 

641 unstable=unstable, 

642 python_cell_magics=set(python_cell_magics), 

643 enabled_features=set(enable_unstable_feature), 

644 ) 

645 

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

647 if line_ranges: 

648 if ipynb: 

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

650 ctx.exit(1) 

651 

652 try: 

653 lines = parse_line_ranges(line_ranges) 

654 except ValueError as e: 

655 err(str(e)) 

656 ctx.exit(1) 

657 

658 if code is not None: 

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

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

661 quiet = True 

662 

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

664 

665 if code is not None: 

666 reformat_code( 

667 content=code, 

668 fast=fast, 

669 write_back=write_back, 

670 mode=mode, 

671 report=report, 

672 lines=lines, 

673 ) 

674 else: 

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

676 try: 

677 sources = get_sources( 

678 root=root, 

679 src=src, 

680 quiet=quiet, 

681 verbose=verbose, 

682 include=include, 

683 exclude=exclude, 

684 extend_exclude=extend_exclude, 

685 force_exclude=force_exclude, 

686 report=report, 

687 stdin_filename=stdin_filename, 

688 ) 

689 except GitWildMatchPatternError: 

690 ctx.exit(1) 

691 

692 if not sources: 

693 if verbose or not quiet: 

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

695 if "-" in src: 

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

697 ctx.exit(0) 

698 

699 if len(sources) == 1: 

700 reformat_one( 

701 src=sources.pop(), 

702 fast=fast, 

703 write_back=write_back, 

704 mode=mode, 

705 report=report, 

706 lines=lines, 

707 no_cache=no_cache, 

708 ) 

709 else: 

710 from black.concurrency import reformat_many 

711 

712 if lines: 

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

714 ctx.exit(1) 

715 reformat_many( 

716 sources=sources, 

717 fast=fast, 

718 write_back=write_back, 

719 mode=mode, 

720 report=report, 

721 workers=workers, 

722 no_cache=no_cache, 

723 ) 

724 

725 if verbose or not quiet: 

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

727 out() 

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

729 if code is None: 

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

731 ctx.exit(report.return_code) 

732 

733 

734def get_sources( 

735 *, 

736 root: Path, 

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

738 quiet: bool, 

739 verbose: bool, 

740 include: Pattern[str], 

741 exclude: Optional[Pattern[str]], 

742 extend_exclude: Optional[Pattern[str]], 

743 force_exclude: Optional[Pattern[str]], 

744 report: "Report", 

745 stdin_filename: Optional[str], 

746) -> set[Path]: 

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

748 sources: set[Path] = set() 

749 

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

751 using_default_exclude = exclude is None 

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

753 gitignore: Optional[dict[Path, PathSpec]] = None 

754 root_gitignore = get_gitignore(root) 

755 

756 for s in src: 

757 if s == "-" and stdin_filename: 

758 path = Path(stdin_filename) 

759 if path_is_excluded(stdin_filename, force_exclude): 

760 report.path_ignored( 

761 path, 

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

763 ) 

764 continue 

765 is_stdin = True 

766 else: 

767 path = Path(s) 

768 is_stdin = False 

769 

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

771 if is_stdin or path.is_file(): 

772 if resolves_outside_root_or_cannot_stat(path, root, report): 

773 if verbose: 

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

775 continue 

776 

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

778 root_relative_path = "/" + root_relative_path 

779 

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

781 if path_is_excluded(root_relative_path, force_exclude): 

782 report.path_ignored( 

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

784 ) 

785 continue 

786 

787 if is_stdin: 

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

789 

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

791 warn=verbose or not quiet 

792 ): 

793 continue 

794 

795 if verbose: 

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

797 sources.add(path) 

798 elif path.is_dir(): 

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

800 if verbose: 

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

802 

803 if using_default_exclude: 

804 gitignore = { 

805 root: root_gitignore, 

806 path: get_gitignore(path), 

807 } 

808 sources.update( 

809 gen_python_files( 

810 path.iterdir(), 

811 root, 

812 include, 

813 exclude, 

814 extend_exclude, 

815 force_exclude, 

816 report, 

817 gitignore, 

818 verbose=verbose, 

819 quiet=quiet, 

820 ) 

821 ) 

822 elif s == "-": 

823 if verbose: 

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

825 sources.add(path) 

826 else: 

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

828 

829 return sources 

830 

831 

832def reformat_code( 

833 content: str, 

834 fast: bool, 

835 write_back: WriteBack, 

836 mode: Mode, 

837 report: Report, 

838 *, 

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

840) -> None: 

841 """ 

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

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

844 

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

846 :func:`format_file_in_place` or :func:`format_stdin_to_stdout`. 

847 """ 

848 path = Path("<string>") 

849 try: 

850 changed = Changed.NO 

851 if format_stdin_to_stdout( 

852 content=content, fast=fast, write_back=write_back, mode=mode, lines=lines 

853 ): 

854 changed = Changed.YES 

855 report.done(path, changed) 

856 except Exception as exc: 

857 if report.verbose: 

858 traceback.print_exc() 

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

860 

861 

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

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

864@mypyc_attr(patchable=True) 

865def reformat_one( 

866 src: Path, 

867 fast: bool, 

868 write_back: WriteBack, 

869 mode: Mode, 

870 report: "Report", 

871 *, 

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

873 no_cache: bool = False, 

874) -> None: 

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

876 

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

878 :func:`format_file_in_place` or :func:`format_stdin_to_stdout`. 

879 """ 

880 try: 

881 changed = Changed.NO 

882 

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

884 is_stdin = True 

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

886 is_stdin = True 

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

888 # to the user 

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

890 else: 

891 is_stdin = False 

892 

893 if is_stdin: 

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

895 mode = replace(mode, is_pyi=True) 

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

897 mode = replace(mode, is_ipynb=True) 

898 if format_stdin_to_stdout( 

899 fast=fast, write_back=write_back, mode=mode, lines=lines 

900 ): 

901 changed = Changed.YES 

902 else: 

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

904 if cache is not None and write_back not in ( 

905 WriteBack.DIFF, 

906 WriteBack.COLOR_DIFF, 

907 ): 

908 if not cache.is_changed(src): 

909 changed = Changed.CACHED 

910 if changed is not Changed.CACHED and format_file_in_place( 

911 src, fast=fast, write_back=write_back, mode=mode, lines=lines 

912 ): 

913 changed = Changed.YES 

914 if cache is not None and ( 

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

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

917 ): 

918 cache.write([src]) 

919 report.done(src, changed) 

920 except Exception as exc: 

921 if report.verbose: 

922 traceback.print_exc() 

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

924 

925 

926def format_file_in_place( 

927 src: Path, 

928 fast: bool, 

929 mode: Mode, 

930 write_back: WriteBack = WriteBack.NO, 

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

932 *, 

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

934) -> bool: 

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

936 

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

938 code to the file. 

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

940 """ 

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

942 mode = replace(mode, is_pyi=True) 

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

944 mode = replace(mode, is_ipynb=True) 

945 

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

947 header = b"" 

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

949 if mode.skip_source_first_line: 

950 header = buf.readline() 

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

952 try: 

953 dst_contents = format_file_contents( 

954 src_contents, fast=fast, mode=mode, lines=lines 

955 ) 

956 except NothingChanged: 

957 return False 

958 except JSONDecodeError: 

959 raise ValueError( 

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

961 ) from None 

962 src_contents = header.decode(encoding) + src_contents 

963 dst_contents = header.decode(encoding) + dst_contents 

964 

965 if write_back == WriteBack.YES: 

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

967 f.write(dst_contents) 

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

969 now = datetime.now(timezone.utc) 

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

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

972 if mode.is_ipynb: 

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

974 else: 

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

976 

977 if write_back == WriteBack.COLOR_DIFF: 

978 diff_contents = color_diff(diff_contents) 

979 

980 with lock or nullcontext(): 

981 f = io.TextIOWrapper( 

982 sys.stdout.buffer, 

983 encoding=encoding, 

984 newline=newline, 

985 write_through=True, 

986 ) 

987 f = wrap_stream_for_windows(f) 

988 f.write(diff_contents) 

989 f.detach() 

990 

991 return True 

992 

993 

994def format_stdin_to_stdout( 

995 fast: bool, 

996 *, 

997 content: Optional[str] = None, 

998 write_back: WriteBack = WriteBack.NO, 

999 mode: Mode, 

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

1001) -> bool: 

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

1003 

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

1005 

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

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

1008 :func:`format_file_contents`. 

1009 """ 

1010 then = datetime.now(timezone.utc) 

1011 

1012 if content is None: 

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

1014 elif Preview.normalize_cr_newlines in mode: 

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

1016 else: 

1017 src, encoding, newline = content, "utf-8", "" 

1018 

1019 dst = src 

1020 try: 

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

1022 return True 

1023 

1024 except NothingChanged: 

1025 return False 

1026 

1027 finally: 

1028 f = io.TextIOWrapper( 

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

1030 ) 

1031 if write_back == WriteBack.YES: 

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

1033 if Preview.normalize_cr_newlines in mode: 

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

1035 dst += newline 

1036 else: 

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

1038 dst += "\n" 

1039 f.write(dst) 

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

1041 now = datetime.now(timezone.utc) 

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

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

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

1045 if write_back == WriteBack.COLOR_DIFF: 

1046 d = color_diff(d) 

1047 f = wrap_stream_for_windows(f) 

1048 f.write(d) 

1049 f.detach() 

1050 

1051 

1052def check_stability_and_equivalence( 

1053 src_contents: str, 

1054 dst_contents: str, 

1055 *, 

1056 mode: Mode, 

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

1058) -> None: 

1059 """Perform stability and equivalence checks. 

1060 

1061 Raise AssertionError if source and destination contents are not 

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

1063 content differently. 

1064 """ 

1065 assert_equivalent(src_contents, dst_contents) 

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

1067 

1068 

1069def format_file_contents( 

1070 src_contents: str, 

1071 *, 

1072 fast: bool, 

1073 mode: Mode, 

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

1075) -> FileContent: 

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

1077 

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

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

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

1081 """ 

1082 if mode.is_ipynb: 

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

1084 else: 

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

1086 if src_contents == dst_contents: 

1087 raise NothingChanged 

1088 

1089 if not fast and not mode.is_ipynb: 

1090 # Jupyter notebooks will already have been checked above. 

1091 check_stability_and_equivalence( 

1092 src_contents, dst_contents, mode=mode, lines=lines 

1093 ) 

1094 return dst_contents 

1095 

1096 

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

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

1099 

1100 General idea is: 

1101 

1102 - if cell has trailing semicolon, remove it; 

1103 - if cell has IPython magics, mask them; 

1104 - format cell; 

1105 - reinstate IPython magics; 

1106 - reinstate trailing semicolon (if originally present); 

1107 - strip trailing newlines. 

1108 

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

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

1111 are currently not supported. 

1112 """ 

1113 validate_cell(src, mode) 

1114 src_without_trailing_semicolon, has_trailing_semicolon = remove_trailing_semicolon( 

1115 src 

1116 ) 

1117 try: 

1118 masked_src, replacements = mask_cell(src_without_trailing_semicolon) 

1119 except SyntaxError: 

1120 raise NothingChanged from None 

1121 masked_dst = format_str(masked_src, mode=mode) 

1122 if not fast: 

1123 check_stability_and_equivalence(masked_src, masked_dst, mode=mode) 

1124 dst_without_trailing_semicolon = unmask_cell(masked_dst, replacements) 

1125 dst = put_trailing_semicolon_back( 

1126 dst_without_trailing_semicolon, has_trailing_semicolon 

1127 ) 

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

1129 if dst == src: 

1130 raise NothingChanged from None 

1131 return dst 

1132 

1133 

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

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

1136 

1137 All notebook metadata fields are optional, see 

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

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

1140 """ 

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

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

1143 raise NothingChanged from None 

1144 

1145 

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

1147 """Format Jupyter notebook. 

1148 

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

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

1151 """ 

1152 if not src_contents: 

1153 raise NothingChanged 

1154 

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

1156 modified = False 

1157 nb = json.loads(src_contents) 

1158 validate_metadata(nb) 

1159 for cell in nb["cells"]: 

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

1161 try: 

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

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

1164 except NothingChanged: 

1165 pass 

1166 else: 

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

1168 modified = True 

1169 if modified: 

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

1171 if trailing_newline: 

1172 dst_contents = dst_contents + "\n" 

1173 return dst_contents 

1174 else: 

1175 raise NothingChanged 

1176 

1177 

1178def format_str( 

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

1180) -> str: 

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

1182 

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

1184 allowed. Example: 

1185 

1186 >>> import black 

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

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

1189 ... 

1190 

1191 A more complex example: 

1192 

1193 >>> print( 

1194 ... black.format_str( 

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

1196 ... mode=black.Mode( 

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

1198 ... line_length=10, 

1199 ... string_normalization=False, 

1200 ... is_pyi=False, 

1201 ... ), 

1202 ... ), 

1203 ... ) 

1204 def f( 

1205 arg: str = '', 

1206 ) -> None: 

1207 hey 

1208 

1209 """ 

1210 if lines: 

1211 lines = sanitized_lines(lines, src_contents) 

1212 if not lines: 

1213 return src_contents # Nothing to format 

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

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

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

1217 # parentheses. Admittedly ugly. 

1218 if src_contents != dst_contents: 

1219 if lines: 

1220 lines = adjusted_lines(lines, src_contents, dst_contents) 

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

1222 return dst_contents 

1223 

1224 

1225def _format_str_once( 

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

1227) -> str: 

1228 if Preview.normalize_cr_newlines in mode: 

1229 normalized_contents, _, newline_type = decode_bytes( 

1230 src_contents.encode("utf-8"), mode 

1231 ) 

1232 

1233 src_node = lib2to3_parse( 

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

1235 ) 

1236 else: 

1237 src_node = lib2to3_parse(src_contents.lstrip(), mode.target_versions) 

1238 

1239 dst_blocks: list[LinesBlock] = [] 

1240 if mode.target_versions: 

1241 versions = mode.target_versions 

1242 else: 

1243 future_imports = get_future_imports(src_node) 

1244 versions = detect_target_versions(src_node, future_imports=future_imports) 

1245 

1246 line_generation_features = { 

1247 feature 

1248 for feature in { 

1249 Feature.PARENTHESIZED_CONTEXT_MANAGERS, 

1250 Feature.UNPARENTHESIZED_EXCEPT_TYPES, 

1251 Feature.T_STRINGS, 

1252 } 

1253 if supports_feature(versions, feature) 

1254 } 

1255 normalize_fmt_off(src_node, mode, lines) 

1256 if lines: 

1257 # This should be called after normalize_fmt_off. 

1258 convert_unchanged_lines(src_node, lines) 

1259 

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

1261 elt = EmptyLineTracker(mode=mode) 

1262 split_line_features = { 

1263 feature 

1264 for feature in { 

1265 Feature.TRAILING_COMMA_IN_CALL, 

1266 Feature.TRAILING_COMMA_IN_DEF, 

1267 } 

1268 if supports_feature(versions, feature) 

1269 } 

1270 block: Optional[LinesBlock] = None 

1271 for current_line in line_generator.visit(src_node): 

1272 block = elt.maybe_empty_lines(current_line) 

1273 dst_blocks.append(block) 

1274 for line in transform_line( 

1275 current_line, mode=mode, features=split_line_features 

1276 ): 

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

1278 if dst_blocks: 

1279 dst_blocks[-1].after = 0 

1280 dst_contents = [] 

1281 for block in dst_blocks: 

1282 dst_contents.extend(block.all_lines()) 

1283 if not dst_contents: 

1284 if Preview.normalize_cr_newlines in mode: 

1285 if "\n" in normalized_contents: 

1286 return newline_type 

1287 else: 

1288 # Use decode_bytes to retrieve the correct source newline (CRLF or LF), 

1289 # and check if normalized_content has more than one line 

1290 normalized_content, _, newline = decode_bytes( 

1291 src_contents.encode("utf-8"), mode 

1292 ) 

1293 if "\n" in normalized_content: 

1294 return newline 

1295 return "" 

1296 if Preview.normalize_cr_newlines in mode: 

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

1298 else: 

1299 return "".join(dst_contents) 

1300 

1301 

1302def decode_bytes(src: bytes, mode: Mode) -> tuple[FileContent, Encoding, NewLine]: 

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

1304 

1305 `newline` is either CRLF or LF but `decoded_contents` is decoded with 

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

1307 """ 

1308 srcbuf = io.BytesIO(src) 

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

1310 if not lines: 

1311 return "", encoding, "\n" 

1312 

1313 if Preview.normalize_cr_newlines in mode: 

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

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

1316 newline = "\r" 

1317 else: 

1318 newline = "\r\n" 

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

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

1321 newline = "\r" 

1322 else: 

1323 newline = "\n" 

1324 else: 

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

1326 newline = "\r" 

1327 else: 

1328 newline = "\n" 

1329 else: 

1330 newline = "\r\n" if lines[0][-2:] == b"\r\n" else "\n" 

1331 

1332 srcbuf.seek(0) 

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

1334 return tiow.read(), encoding, newline 

1335 

1336 

1337def get_features_used( # noqa: C901 

1338 node: Node, *, future_imports: Optional[set[str]] = None 

1339) -> set[Feature]: 

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

1341 

1342 Currently looking for: 

1343 - f-strings; 

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

1345 - underscores in numeric literals; 

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

1347 - positional only arguments in function signatures and lambdas; 

1348 - assignment expression; 

1349 - relaxed decorator syntax; 

1350 - usage of __future__ flags (annotations); 

1351 - print / exec statements; 

1352 - parenthesized context managers; 

1353 - match statements; 

1354 - except* clause; 

1355 - variadic generics; 

1356 """ 

1357 features: set[Feature] = set() 

1358 if future_imports: 

1359 features |= { 

1360 FUTURE_FLAG_TO_FEATURE[future_import] 

1361 for future_import in future_imports 

1362 if future_import in FUTURE_FLAG_TO_FEATURE 

1363 } 

1364 

1365 for n in node.pre_order(): 

1366 if n.type == token.FSTRING_START: 

1367 features.add(Feature.F_STRINGS) 

1368 elif n.type == token.TSTRING_START: 

1369 features.add(Feature.T_STRINGS) 

1370 elif ( 

1371 n.type == token.RBRACE 

1372 and n.parent is not None 

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

1374 ): 

1375 features.add(Feature.DEBUG_F_STRINGS) 

1376 

1377 elif is_number_token(n): 

1378 if "_" in n.value: 

1379 features.add(Feature.NUMERIC_UNDERSCORES) 

1380 

1381 elif n.type == token.SLASH: 

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

1383 syms.typedargslist, 

1384 syms.arglist, 

1385 syms.varargslist, 

1386 }: 

1387 features.add(Feature.POS_ONLY_ARGUMENTS) 

1388 

1389 elif n.type == token.COLONEQUAL: 

1390 features.add(Feature.ASSIGNMENT_EXPRESSIONS) 

1391 

1392 elif n.type == syms.decorator: 

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

1394 n.children[1] 

1395 ): 

1396 features.add(Feature.RELAXED_DECORATORS) 

1397 

1398 elif ( 

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

1400 and n.children 

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

1402 ): 

1403 if n.type == syms.typedargslist: 

1404 feature = Feature.TRAILING_COMMA_IN_DEF 

1405 else: 

1406 feature = Feature.TRAILING_COMMA_IN_CALL 

1407 

1408 for ch in n.children: 

1409 if ch.type in STARS: 

1410 features.add(feature) 

1411 

1412 if ch.type == syms.argument: 

1413 for argch in ch.children: 

1414 if argch.type in STARS: 

1415 features.add(feature) 

1416 

1417 elif ( 

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

1419 and len(n.children) >= 2 

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

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

1422 ): 

1423 features.add(Feature.UNPACKING_ON_FLOW) 

1424 

1425 elif ( 

1426 n.type == syms.annassign 

1427 and len(n.children) >= 4 

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

1429 ): 

1430 features.add(Feature.ANN_ASSIGN_EXTENDED_RHS) 

1431 

1432 elif ( 

1433 n.type == syms.with_stmt 

1434 and len(n.children) > 2 

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

1436 ): 

1437 atom_children = n.children[1].children 

1438 if ( 

1439 len(atom_children) == 3 

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

1441 and _contains_asexpr(atom_children[1]) 

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

1443 ): 

1444 features.add(Feature.PARENTHESIZED_CONTEXT_MANAGERS) 

1445 

1446 elif n.type == syms.match_stmt: 

1447 features.add(Feature.PATTERN_MATCHING) 

1448 

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

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

1451 ): 

1452 features.add(Feature.VARIADIC_GENERICS) 

1453 

1454 elif ( 

1455 n.type == syms.tname_star 

1456 and len(n.children) == 3 

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

1458 ): 

1459 features.add(Feature.VARIADIC_GENERICS) 

1460 

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

1462 features.add(Feature.TYPE_PARAMS) 

1463 

1464 elif ( 

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

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

1467 ): 

1468 features.add(Feature.TYPE_PARAM_DEFAULTS) 

1469 

1470 elif ( 

1471 n.type == syms.except_clause 

1472 and len(n.children) >= 2 

1473 and ( 

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

1475 ) 

1476 ): 

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

1478 

1479 if is_star_except: 

1480 features.add(Feature.EXCEPT_STAR) 

1481 

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

1483 has_as_clause = ( 

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

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

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

1487 ) 

1488 

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

1490 if not has_as_clause and ( 

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

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

1493 ): 

1494 features.add(Feature.UNPARENTHESIZED_EXCEPT_TYPES) 

1495 

1496 return features 

1497 

1498 

1499def _contains_asexpr(node: Union[Node, Leaf]) -> bool: 

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

1501 if node.type == syms.asexpr_test: 

1502 return True 

1503 elif node.type == syms.atom: 

1504 if ( 

1505 len(node.children) == 3 

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

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

1508 ): 

1509 return _contains_asexpr(node.children[1]) 

1510 elif node.type == syms.testlist_gexp: 

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

1512 return False 

1513 

1514 

1515def detect_target_versions( 

1516 node: Node, *, future_imports: Optional[set[str]] = None 

1517) -> set[TargetVersion]: 

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

1519 features = get_features_used(node, future_imports=future_imports) 

1520 return { 

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

1522 } 

1523 

1524 

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

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

1527 imports: set[str] = set() 

1528 

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

1530 for child in children: 

1531 if isinstance(child, Leaf): 

1532 if child.type == token.NAME: 

1533 yield child.value 

1534 

1535 elif child.type == syms.import_as_name: 

1536 orig_name = child.children[0] 

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

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

1539 yield orig_name.value 

1540 

1541 elif child.type == syms.import_as_names: 

1542 yield from get_imports_from_children(child.children) 

1543 

1544 else: 

1545 raise AssertionError("Invalid syntax parsing imports") 

1546 

1547 for child in node.children: 

1548 if child.type != syms.simple_stmt: 

1549 break 

1550 

1551 first_child = child.children[0] 

1552 if isinstance(first_child, Leaf): 

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

1554 if ( 

1555 len(child.children) == 2 

1556 and first_child.type == token.STRING 

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

1558 ): 

1559 continue 

1560 

1561 break 

1562 

1563 elif first_child.type == syms.import_from: 

1564 module_name = first_child.children[1] 

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

1566 break 

1567 

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

1569 else: 

1570 break 

1571 

1572 return imports 

1573 

1574 

1575def _black_info() -> str: 

1576 return ( 

1577 f"Black {__version__} on " 

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

1579 ) 

1580 

1581 

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

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

1584 try: 

1585 src_ast = parse_ast(src) 

1586 except Exception as exc: 

1587 raise ASTSafetyError( 

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

1589 f"{exc}\n" 

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

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

1592 ) from exc 

1593 

1594 try: 

1595 dst_ast = parse_ast(dst) 

1596 except Exception as exc: 

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

1598 raise ASTSafetyError( 

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

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

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

1602 ) from None 

1603 

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

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

1606 if src_ast_str != dst_ast_str: 

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

1608 raise ASTSafetyError( 

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

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

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

1612 ) from None 

1613 

1614 

1615def assert_stable( 

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

1617) -> None: 

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

1619 if lines: 

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

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

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

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

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

1625 return 

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

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

1628 # versions. 

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

1630 if dst != newdst: 

1631 log = dump_to_file( 

1632 str(mode), 

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

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

1635 ) 

1636 raise AssertionError( 

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

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

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

1640 ) from None 

1641 

1642 

1643@contextmanager 

1644def nullcontext() -> Iterator[None]: 

1645 """Return an empty context manager. 

1646 

1647 To be used like `nullcontext` in Python 3.7. 

1648 """ 

1649 yield 

1650 

1651 

1652def patched_main() -> None: 

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

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

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

1656 from multiprocessing import freeze_support 

1657 

1658 freeze_support() 

1659 

1660 main() 

1661 

1662 

1663if __name__ == "__main__": 

1664 patched_main()