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

637 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 } 

1252 if supports_feature(versions, feature) 

1253 } 

1254 normalize_fmt_off(src_node, mode, lines) 

1255 if lines: 

1256 # This should be called after normalize_fmt_off. 

1257 convert_unchanged_lines(src_node, lines) 

1258 

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

1260 elt = EmptyLineTracker(mode=mode) 

1261 split_line_features = { 

1262 feature 

1263 for feature in { 

1264 Feature.TRAILING_COMMA_IN_CALL, 

1265 Feature.TRAILING_COMMA_IN_DEF, 

1266 } 

1267 if supports_feature(versions, feature) 

1268 } 

1269 block: Optional[LinesBlock] = None 

1270 for current_line in line_generator.visit(src_node): 

1271 block = elt.maybe_empty_lines(current_line) 

1272 dst_blocks.append(block) 

1273 for line in transform_line( 

1274 current_line, mode=mode, features=split_line_features 

1275 ): 

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

1277 if dst_blocks: 

1278 dst_blocks[-1].after = 0 

1279 dst_contents = [] 

1280 for block in dst_blocks: 

1281 dst_contents.extend(block.all_lines()) 

1282 if not dst_contents: 

1283 if Preview.normalize_cr_newlines in mode: 

1284 if "\n" in normalized_contents: 

1285 return newline_type 

1286 else: 

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

1288 # and check if normalized_content has more than one line 

1289 normalized_content, _, newline = decode_bytes( 

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

1291 ) 

1292 if "\n" in normalized_content: 

1293 return newline 

1294 return "" 

1295 if Preview.normalize_cr_newlines in mode: 

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

1297 else: 

1298 return "".join(dst_contents) 

1299 

1300 

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

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

1303 

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

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

1306 """ 

1307 srcbuf = io.BytesIO(src) 

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

1309 if not lines: 

1310 return "", encoding, "\n" 

1311 

1312 if Preview.normalize_cr_newlines in mode: 

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

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

1315 newline = "\r" 

1316 else: 

1317 newline = "\r\n" 

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

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

1320 newline = "\r" 

1321 else: 

1322 newline = "\n" 

1323 else: 

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

1325 newline = "\r" 

1326 else: 

1327 newline = "\n" 

1328 else: 

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

1330 

1331 srcbuf.seek(0) 

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

1333 return tiow.read(), encoding, newline 

1334 

1335 

1336def get_features_used( # noqa: C901 

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

1338) -> set[Feature]: 

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

1340 

1341 Currently looking for: 

1342 - f-strings; 

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

1344 - underscores in numeric literals; 

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

1346 - positional only arguments in function signatures and lambdas; 

1347 - assignment expression; 

1348 - relaxed decorator syntax; 

1349 - usage of __future__ flags (annotations); 

1350 - print / exec statements; 

1351 - parenthesized context managers; 

1352 - match statements; 

1353 - except* clause; 

1354 - variadic generics; 

1355 """ 

1356 features: set[Feature] = set() 

1357 if future_imports: 

1358 features |= { 

1359 FUTURE_FLAG_TO_FEATURE[future_import] 

1360 for future_import in future_imports 

1361 if future_import in FUTURE_FLAG_TO_FEATURE 

1362 } 

1363 

1364 for n in node.pre_order(): 

1365 if n.type == token.FSTRING_START: 

1366 features.add(Feature.F_STRINGS) 

1367 elif ( 

1368 n.type == token.RBRACE 

1369 and n.parent is not None 

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

1371 ): 

1372 features.add(Feature.DEBUG_F_STRINGS) 

1373 

1374 elif is_number_token(n): 

1375 if "_" in n.value: 

1376 features.add(Feature.NUMERIC_UNDERSCORES) 

1377 

1378 elif n.type == token.SLASH: 

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

1380 syms.typedargslist, 

1381 syms.arglist, 

1382 syms.varargslist, 

1383 }: 

1384 features.add(Feature.POS_ONLY_ARGUMENTS) 

1385 

1386 elif n.type == token.COLONEQUAL: 

1387 features.add(Feature.ASSIGNMENT_EXPRESSIONS) 

1388 

1389 elif n.type == syms.decorator: 

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

1391 n.children[1] 

1392 ): 

1393 features.add(Feature.RELAXED_DECORATORS) 

1394 

1395 elif ( 

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

1397 and n.children 

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

1399 ): 

1400 if n.type == syms.typedargslist: 

1401 feature = Feature.TRAILING_COMMA_IN_DEF 

1402 else: 

1403 feature = Feature.TRAILING_COMMA_IN_CALL 

1404 

1405 for ch in n.children: 

1406 if ch.type in STARS: 

1407 features.add(feature) 

1408 

1409 if ch.type == syms.argument: 

1410 for argch in ch.children: 

1411 if argch.type in STARS: 

1412 features.add(feature) 

1413 

1414 elif ( 

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

1416 and len(n.children) >= 2 

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

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

1419 ): 

1420 features.add(Feature.UNPACKING_ON_FLOW) 

1421 

1422 elif ( 

1423 n.type == syms.annassign 

1424 and len(n.children) >= 4 

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

1426 ): 

1427 features.add(Feature.ANN_ASSIGN_EXTENDED_RHS) 

1428 

1429 elif ( 

1430 n.type == syms.with_stmt 

1431 and len(n.children) > 2 

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

1433 ): 

1434 atom_children = n.children[1].children 

1435 if ( 

1436 len(atom_children) == 3 

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

1438 and _contains_asexpr(atom_children[1]) 

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

1440 ): 

1441 features.add(Feature.PARENTHESIZED_CONTEXT_MANAGERS) 

1442 

1443 elif n.type == syms.match_stmt: 

1444 features.add(Feature.PATTERN_MATCHING) 

1445 

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

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

1448 ): 

1449 features.add(Feature.VARIADIC_GENERICS) 

1450 

1451 elif ( 

1452 n.type == syms.tname_star 

1453 and len(n.children) == 3 

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

1455 ): 

1456 features.add(Feature.VARIADIC_GENERICS) 

1457 

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

1459 features.add(Feature.TYPE_PARAMS) 

1460 

1461 elif ( 

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

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

1464 ): 

1465 features.add(Feature.TYPE_PARAM_DEFAULTS) 

1466 

1467 elif ( 

1468 n.type == syms.except_clause 

1469 and len(n.children) >= 2 

1470 and ( 

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

1472 ) 

1473 ): 

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

1475 

1476 if is_star_except: 

1477 features.add(Feature.EXCEPT_STAR) 

1478 

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

1480 has_as_clause = ( 

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

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

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

1484 ) 

1485 

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

1487 if not has_as_clause and ( 

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

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

1490 ): 

1491 features.add(Feature.UNPARENTHESIZED_EXCEPT_TYPES) 

1492 

1493 return features 

1494 

1495 

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

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

1498 if node.type == syms.asexpr_test: 

1499 return True 

1500 elif node.type == syms.atom: 

1501 if ( 

1502 len(node.children) == 3 

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

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

1505 ): 

1506 return _contains_asexpr(node.children[1]) 

1507 elif node.type == syms.testlist_gexp: 

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

1509 return False 

1510 

1511 

1512def detect_target_versions( 

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

1514) -> set[TargetVersion]: 

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

1516 features = get_features_used(node, future_imports=future_imports) 

1517 return { 

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

1519 } 

1520 

1521 

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

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

1524 imports: set[str] = set() 

1525 

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

1527 for child in children: 

1528 if isinstance(child, Leaf): 

1529 if child.type == token.NAME: 

1530 yield child.value 

1531 

1532 elif child.type == syms.import_as_name: 

1533 orig_name = child.children[0] 

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

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

1536 yield orig_name.value 

1537 

1538 elif child.type == syms.import_as_names: 

1539 yield from get_imports_from_children(child.children) 

1540 

1541 else: 

1542 raise AssertionError("Invalid syntax parsing imports") 

1543 

1544 for child in node.children: 

1545 if child.type != syms.simple_stmt: 

1546 break 

1547 

1548 first_child = child.children[0] 

1549 if isinstance(first_child, Leaf): 

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

1551 if ( 

1552 len(child.children) == 2 

1553 and first_child.type == token.STRING 

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

1555 ): 

1556 continue 

1557 

1558 break 

1559 

1560 elif first_child.type == syms.import_from: 

1561 module_name = first_child.children[1] 

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

1563 break 

1564 

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

1566 else: 

1567 break 

1568 

1569 return imports 

1570 

1571 

1572def _black_info() -> str: 

1573 return ( 

1574 f"Black {__version__} on " 

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

1576 ) 

1577 

1578 

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

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

1581 try: 

1582 src_ast = parse_ast(src) 

1583 except Exception as exc: 

1584 raise ASTSafetyError( 

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

1586 f"{exc}\n" 

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

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

1589 ) from exc 

1590 

1591 try: 

1592 dst_ast = parse_ast(dst) 

1593 except Exception as exc: 

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

1595 raise ASTSafetyError( 

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

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

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

1599 ) from None 

1600 

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

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

1603 if src_ast_str != dst_ast_str: 

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

1605 raise ASTSafetyError( 

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

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

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

1609 ) from None 

1610 

1611 

1612def assert_stable( 

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

1614) -> None: 

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

1616 if lines: 

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

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

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

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

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

1622 return 

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

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

1625 # versions. 

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

1627 if dst != newdst: 

1628 log = dump_to_file( 

1629 str(mode), 

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

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

1632 ) 

1633 raise AssertionError( 

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

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

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

1637 ) from None 

1638 

1639 

1640@contextmanager 

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

1642 """Return an empty context manager. 

1643 

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

1645 """ 

1646 yield 

1647 

1648 

1649def patched_main() -> None: 

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

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

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

1653 from multiprocessing import freeze_support 

1654 

1655 freeze_support() 

1656 

1657 main() 

1658 

1659 

1660if __name__ == "__main__": 

1661 patched_main()